From ebed8a603cc166fd7ccd50d7bae60d688c5de46c Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 31 Mar 2026 14:07:09 -0700 Subject: [PATCH 01/27] Replacing COM reference with nuget reference --- dotnet/autoShell/UIAutomation.cs | 1 + dotnet/autoShell/autoShell.csproj | 12 +----------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/dotnet/autoShell/UIAutomation.cs b/dotnet/autoShell/UIAutomation.cs index 7bc24e272a..5a78c70a8d 100644 --- a/dotnet/autoShell/UIAutomation.cs +++ b/dotnet/autoShell/UIAutomation.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using UIAutomationClient = Interop.UIAutomationClient; namespace autoShell; diff --git a/dotnet/autoShell/autoShell.csproj b/dotnet/autoShell/autoShell.csproj index ee0a275771..0a9e9d1cb9 100644 --- a/dotnet/autoShell/autoShell.csproj +++ b/dotnet/autoShell/autoShell.csproj @@ -10,17 +10,7 @@ false - - tlbimp - 0 - 1 - 944de083-8fb8-45cf-bcb7-c477acb2f897 - 0 - false - true - - - + From 7e029dddb7edaff89bee566e9f0187b8951780a6 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 31 Mar 2026 17:05:53 -0700 Subject: [PATCH 02/27] Initial refactoring for testability --- dotnet/autoShell/AutoShell.cs | 646 ++++++++---------- dotnet/autoShell/AutoShell_Settings.cs | 205 ++++-- dotnet/autoShell/AutoShell_Themes.cs | 569 ++++++++------- dotnet/autoShell/AutoShell_Win32.cs | 41 +- .../autoShell/Handlers/AppCommandHandler.cs | 27 + .../autoShell/Handlers/AudioCommandHandler.cs | 54 ++ .../autoShell/Handlers/CommandDispatcher.cs | 64 ++ .../Handlers/DisplayCommandHandler.cs | 28 + dotnet/autoShell/Handlers/ICommandHandler.cs | 26 + .../Handlers/NetworkCommandHandler.cs | 32 + .../Handlers/SettingsCommandHandler.cs | 101 +++ .../Handlers/SystemCommandHandler.cs | 26 + .../autoShell/Handlers/ThemeCommandHandler.cs | 29 + .../Handlers/VirtualDesktopCommandHandler.cs | 32 + .../Handlers/WindowCommandHandler.cs | 28 + dotnet/autoShell/Services/IAudioService.cs | 30 + dotnet/autoShell/Services/IProcessService.cs | 27 + dotnet/autoShell/Services/IRegistryService.cs | 27 + .../Services/ISystemParametersService.cs | 39 ++ .../autoShell/Services/WindowsAudioService.cs | 89 +++ .../Services/WindowsProcessService.cs | 34 + .../Services/WindowsRegistryService.cs | 26 + .../WindowsSystemParametersService.cs | 45 ++ .../agents/desktop/src/actionsSchema.ts | 52 +- ts/packages/agents/desktop/src/connector.ts | 68 +- .../src/windows/displayActionsSchema.ts | 2 +- .../desktop/src/windows/inputActionsSchema.ts | 4 +- .../desktop/src/windows/powerActionsSchema.ts | 2 +- .../src/windows/systemActionsSchema.ts | 6 +- 29 files changed, 1532 insertions(+), 827 deletions(-) create mode 100644 dotnet/autoShell/Handlers/AppCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/AudioCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/CommandDispatcher.cs create mode 100644 dotnet/autoShell/Handlers/DisplayCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/ICommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/NetworkCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/SystemCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/ThemeCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/WindowCommandHandler.cs create mode 100644 dotnet/autoShell/Services/IAudioService.cs create mode 100644 dotnet/autoShell/Services/IProcessService.cs create mode 100644 dotnet/autoShell/Services/IRegistryService.cs create mode 100644 dotnet/autoShell/Services/ISystemParametersService.cs create mode 100644 dotnet/autoShell/Services/WindowsAudioService.cs create mode 100644 dotnet/autoShell/Services/WindowsProcessService.cs create mode 100644 dotnet/autoShell/Services/WindowsRegistryService.cs create mode 100644 dotnet/autoShell/Services/WindowsSystemParametersService.cs diff --git a/dotnet/autoShell/AutoShell.cs b/dotnet/autoShell/AutoShell.cs index c3d72cbd6f..78a7985339 100644 --- a/dotnet/autoShell/AutoShell.cs +++ b/dotnet/autoShell/AutoShell.cs @@ -6,19 +6,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.IO.Packaging; using System.Linq; -using System.Reflection; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; -using System.Windows.Controls; +using autoShell.Handlers; +using autoShell.Services; using Microsoft.VisualBasic; -using Microsoft.VisualBasic.ApplicationServices; using Microsoft.WindowsAPICodePack.Shell; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using static autoShell.AutoShell; namespace autoShell; @@ -26,17 +22,19 @@ namespace autoShell; internal partial class AutoShell { // create a map of friendly names to executable paths - static Hashtable s_friendlyNameToPath = []; - static Hashtable s_friendlyNameToId = []; - static double s_savedVolumePct = 0.0; - static SortedList s_sortedList; + private static Hashtable s_friendlyNameToPath = []; + private static Hashtable s_friendlyNameToId = []; + private static double s_savedVolumePct = 0.0; + private static SortedList s_sortedList; - static IServiceProvider10 s_shell; - static IVirtualDesktopManager s_virtualDesktopManager; - static IVirtualDesktopManagerInternal s_virtualDesktopManagerInternal; - static IVirtualDesktopManagerInternal_BUGBUG s_virtualDesktopManagerInternal_BUGBUG; - static IApplicationViewCollection s_applicationViewCollection; - static IVirtualDesktopPinnedApps s_virtualDesktopPinnedApps; + private static IServiceProvider10 s_shell; + private static IVirtualDesktopManager s_virtualDesktopManager; + private static IVirtualDesktopManagerInternal s_virtualDesktopManagerInternal; + private static IVirtualDesktopManagerInternal_BUGBUG s_virtualDesktopManagerInternal_BUGBUG; + private static IApplicationViewCollection s_applicationViewCollection; + private static IVirtualDesktopPinnedApps s_virtualDesktopPinnedApps; + + private static CommandDispatcher s_dispatcher; /// @@ -99,13 +97,32 @@ static AutoShell() s_virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_VirtualDesktopManager)); s_applicationViewCollection = (IApplicationViewCollection)s_shell.QueryService(typeof(IApplicationViewCollection).GUID, typeof(IApplicationViewCollection).GUID); s_virtualDesktopPinnedApps = (IVirtualDesktopPinnedApps)s_shell.QueryService(CLSID_VirtualDesktopPinnedApps, typeof(IVirtualDesktopPinnedApps).GUID); + + // Initialize command dispatcher with all handlers + var registry = new WindowsRegistryService(); + var systemParams = new WindowsSystemParametersService(); + var process = new WindowsProcessService(); + var audio = new WindowsAudioService(); + + s_dispatcher = new CommandDispatcher(); + s_dispatcher.Register( + new AudioCommandHandler(audio), + new AppCommandHandler(), + new WindowCommandHandler(), + new ThemeCommandHandler(), + new VirtualDesktopCommandHandler(), + new NetworkCommandHandler(), + new DisplayCommandHandler(), + new SettingsCommandHandler(registry, systemParams, process), + new SystemCommandHandler() + ); } /// /// Program entry point /// /// Any command line arguments - static void Main(string[] args) + private static void Main(string[] args) { string rawCmdLine = Marshal.PtrToStringUni(GetCommandLineW()); @@ -192,7 +209,7 @@ internal static void LogWarning(string message) Console.ForegroundColor = previousColor; } - static SortedList GetAllInstalledAppsIds() + private static SortedList GetAllInstalledAppsIds() { // GUID taken from https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid var FOLDERID_AppsFolder = new Guid("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}"); @@ -216,7 +233,7 @@ static SortedList GetAllInstalledAppsIds() return appIds; } - static void SetMasterVolume(int pct) + private static void SetMasterVolume(int pct) { // Using Windows Core Audio API via COM interop try @@ -236,7 +253,7 @@ static void SetMasterVolume(int pct) } } - static void RestoreMasterVolume() + private static void RestoreMasterVolume() { // Using Windows Core Audio API via COM interop try @@ -254,7 +271,7 @@ static void RestoreMasterVolume() } } - static void SetMasterMute(bool mute) + private static void SetMasterMute(bool mute) { // Using Windows Core Audio API via COM interop try @@ -274,20 +291,13 @@ static void SetMasterMute(bool mute) } } - static string ResolveProcessNameFromFriendlyName(string friendlyName) + private static string ResolveProcessNameFromFriendlyName(string friendlyName) { string path = (string)s_friendlyNameToPath[friendlyName.ToLowerInvariant()]; - if (path != null) - { - return Path.GetFileNameWithoutExtension(path); - } - else - { - return friendlyName; - } + return path != null ? Path.GetFileNameWithoutExtension(path) : friendlyName; } - static IntPtr FindProcessWindowHandle(string processName) + private static IntPtr FindProcessWindowHandle(string processName) { processName = ResolveProcessNameFromFriendlyName(processName); Process[] processes = Process.GetProcessesByName(processName); @@ -305,7 +315,7 @@ static IntPtr FindProcessWindowHandle(string processName) } // given part of a process name, raise the window of that process to the top level - static void RaiseWindow(string processName) + private static void RaiseWindow(string processName) { processName = ResolveProcessNameFromFriendlyName(processName); Process[] processes = Process.GetProcessesByName(processName); @@ -339,7 +349,7 @@ static void RaiseWindow(string processName) } } - static void MaximizeWindow(string processName) + private static void MaximizeWindow(string processName) { processName = ResolveProcessNameFromFriendlyName(processName); Process[] processes = Process.GetProcessesByName(processName); @@ -369,7 +379,7 @@ static void MaximizeWindow(string processName) } } - static void MinimizeWindow(string processName) + private static void MinimizeWindow(string processName) { processName = ResolveProcessNameFromFriendlyName(processName); Process[] processes = Process.GetProcessesByName(processName); @@ -397,7 +407,7 @@ static void MinimizeWindow(string processName) } } - static void TileWindowPair(string processName1, string processName2) + private static void TileWindowPair(string processName1, string processName2) { // find both processes // TODO: Update this to account for UWP apps (e.g. calculator). UWPs are hosted by ApplicationFrameHost.exe @@ -501,7 +511,7 @@ static void TileWindowPair(string processName1, string processName2) /// /// The text to search for in window titles (case-insensitive). /// A tuple containing the window handle and process ID, or (IntPtr.Zero, -1) if not found. - static (IntPtr hWnd, int pid) FindWindowByTitle(string titleSearch) + private static (IntPtr hWnd, int pid) FindWindowByTitle(string titleSearch) { IntPtr foundHandle = IntPtr.Zero; int foundPid = -1; @@ -536,7 +546,7 @@ static void TileWindowPair(string processName1, string processName2) } // given a friendly name, check if it's running and if not, start it; if it's running raise it to the top level - static void OpenApplication(string friendlyName) + private static void OpenApplication(string friendlyName) { // check to see if the application is running Process[] processes = Process.GetProcessesByName(friendlyName); @@ -599,7 +609,7 @@ static void OpenApplication(string friendlyName) } // close application - static void CloseApplication(string friendlyName) + private static void CloseApplication(string friendlyName) { // check to see if the application is running string processName = ResolveProcessNameFromFriendlyName(friendlyName); @@ -627,7 +637,7 @@ private static void SetDesktopWallpaper(string imagePath) /// Creates virtual desktops from a JSON array of desktop names. /// /// JSON array containing desktop names, e.g., ["Work", "Personal", "Gaming"] - static void CreateDesktop(string jsonValue) + private static void CreateDesktop(string jsonValue) { try { @@ -688,7 +698,7 @@ static void CreateDesktop(string jsonValue) } } - static void SwitchDesktop(string desktopIdentifier) + private static void SwitchDesktop(string desktopIdentifier) { if (!int.TryParse(desktopIdentifier, out int index)) { @@ -701,7 +711,7 @@ static void SwitchDesktop(string desktopIdentifier) } } - static void SwitchDesktop(int index) + private static void SwitchDesktop(int index) { s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); desktops.GetAt(index, typeof(IVirtualDesktop).GUID, out object od); @@ -728,7 +738,7 @@ static void SwitchDesktop(int index) Marshal.ReleaseComObject(desktops); } - static void BumpDesktopIndex(int bump) + private static void BumpDesktopIndex(int bump) { IVirtualDesktop desktop = s_virtualDesktopManagerInternal.GetCurrentDesktop(); int index = GetDesktopIndex(desktop); @@ -754,7 +764,7 @@ static void BumpDesktopIndex(int bump) SwitchDesktop(index); } - static IVirtualDesktop FindDesktopByName(string name) + private static IVirtualDesktop FindDesktopByName(string name) { int count = s_virtualDesktopManagerInternal.GetCount(); @@ -775,7 +785,7 @@ static IVirtualDesktop FindDesktopByName(string name) return null; } - static int GetDesktopIndex(IVirtualDesktop desktop) + private static int GetDesktopIndex(IVirtualDesktop desktop) { int index = -1; int count = s_virtualDesktopManagerInternal.GetCount(); @@ -802,7 +812,7 @@ static int GetDesktopIndex(IVirtualDesktop desktop) /// /// /// Currently not working correction, returns ACCESS_DENIED // TODO: investigate - static void MoveWindowToDesktop(JToken value) + private static void MoveWindowToDesktop(JToken value) { string process = value.SelectToken("process").ToString(); string desktop = value.SelectToken("desktop").ToString(); @@ -844,7 +854,7 @@ static void MoveWindowToDesktop(JToken value) } } - static void PinWindow(string processName) + private static void PinWindow(string processName) { IntPtr hWnd = FindProcessWindowHandle(processName); @@ -883,333 +893,14 @@ static IVirtualDesktopManagerInternal GetVirtualDesktopManagerInternal() } } - static bool execLine(JObject root) - { - var quit = false; - foreach (var kvp in root) - { - string key = kvp.Key; - string value = kvp.Value.ToString(); - switch (key) - { - case "launchProgram": - OpenApplication(value); - break; - case "closeProgram": - CloseApplication(value); - break; - case "maximize": - MaximizeWindow(value); - break; - case "minimize": - MinimizeWindow(value); - break; - case "switchTo": - RaiseWindow(value); - break; - case "quit": - quit = true; - break; - case "tile": - string[] apps = value.Split(','); - if (apps.Length == 2) - { - TileWindowPair(apps[0], apps[1]); - } - break; - case "volume": - int pct = 0; - if (int.TryParse(value, out pct)) - { - SetMasterVolume(pct); - } - break; - case "restoreVolume": - RestoreMasterVolume(); - break; - case "mute": - bool mute = false; - if (bool.TryParse(value, out mute)) - { - SetMasterMute(mute); - } - break; - case "listAppNames": - var installedApps = GetAllInstalledAppsIds(); - Console.WriteLine(JsonConvert.SerializeObject(installedApps.Keys)); - break; - case "setWallpaper": - SetDesktopWallpaper(value); - break; - case "applyTheme": - bool result = ApplyTheme(value); - break; - case "listThemes": - var themes = GetInstalledThemes(); - Console.WriteLine(JsonConvert.SerializeObject(themes)); - break; - case "setThemeMode": - // value can be "light", "dark", "toggle", or boolean - if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase)) - { - ToggleLightDarkMode(); - } - else - { - bool useLightMode; - if (bool.TryParse(value, out useLightMode)) - { - SetLightDarkMode(useLightMode); - } - else if (value.Equals("light", StringComparison.OrdinalIgnoreCase)) - { - SetLightDarkMode(true); - } - else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase)) - { - SetLightDarkMode(false); - } - } - break; - case "createDesktop": - CreateDesktop(value); - break; - case "switchDesktop": - SwitchDesktop(value); - break; - case "nextDesktop": - BumpDesktopIndex(1); - break; - case "previousDesktop": - BumpDesktopIndex(-1); - break; - case "moveWindowToDesktop": - MoveWindowToDesktop(kvp.Value); - break; - case "pinWindow": - PinWindow(value); - break; - case "toggleNotifications": - ShellExecute(IntPtr.Zero, "open", "ms-actioncenter:", null, null, 1); - break; - case "debug": - Debugger.Launch(); - break; - case "toggleAirplaneMode": - SetAirplaneMode(bool.Parse(value)); - break; - case "listWifiNetworks": - ListWifiNetworks(); - break; - case "connectWifi": - JObject netInfo = JObject.Parse(value); - string ssid = netInfo.Value("ssid"); - string password = netInfo["password"] is not null ? netInfo.Value("password") : ""; - ConnectToWifi(ssid, password); - break; - case "disconnectWifi": - DisconnectFromWifi(); - break; - case "setTextSize": - if (int.TryParse(value, out int textSizePct)) - { - SetTextSize(textSizePct); - } - break; - case "setScreenResolution": - SetDisplayResolution(kvp.Value); - break; - case "listResolutions": - ListDisplayResolutions(); - break; - - // ===== Settings Actions (50 new handlers) ===== - - // Network Settings - case "BluetoothToggle": - HandleBluetoothToggle(value); - break; - case "enableWifi": - HandleEnableWifi(value); - break; - case "enableMeteredConnections": - HandleEnableMeteredConnections(value); - break; - - // Display Settings - case "AdjustScreenBrightness": - HandleAdjustScreenBrightness(value); - break; - case "EnableBlueLightFilterSchedule": - HandleEnableBlueLightFilterSchedule(value); - break; - case "adjustColorTemperature": - HandleAdjustColorTemperature(value); - break; - case "DisplayScaling": - HandleDisplayScaling(value); - break; - case "AdjustScreenOrientation": - HandleAdjustScreenOrientation(value); - break; - case "DisplayResolutionAndAspectRatio": - HandleDisplayResolutionAndAspectRatio(value); - break; - case "RotationLock": - HandleRotationLock(value); - break; - - // Personalization Settings - case "SystemThemeMode": - HandleSystemThemeMode(value); - break; - case "EnableTransparency": - HandleEnableTransparency(value); - break; - case "ApplyColorToTitleBar": - HandleApplyColorToTitleBar(value); - break; - case "HighContrastTheme": - HandleHighContrastTheme(value); - break; - - // Taskbar Settings - case "AutoHideTaskbar": - HandleAutoHideTaskbar(value); - break; - case "TaskbarAlignment": - HandleTaskbarAlignment(value); - break; - case "TaskViewVisibility": - HandleTaskViewVisibility(value); - break; - case "ToggleWidgetsButtonVisibility": - HandleToggleWidgetsButtonVisibility(value); - break; - case "ShowBadgesOnTaskbar": - HandleShowBadgesOnTaskbar(value); - break; - case "DisplayTaskbarOnAllMonitors": - HandleDisplayTaskbarOnAllMonitors(value); - break; - case "DisplaySecondsInSystrayClock": - HandleDisplaySecondsInSystrayClock(value); - break; - - // Mouse Settings - case "MouseCursorSpeed": - HandleMouseCursorSpeed(value); - break; - case "MouseWheelScrollLines": - HandleMouseWheelScrollLines(value); - break; - case "setPrimaryMouseButton": - HandleSetPrimaryMouseButton(value); - break; - case "EnhancePointerPrecision": - HandleEnhancePointerPrecision(value); - break; - case "AdjustMousePointerSize": - HandleAdjustMousePointerSize(value); - break; - case "mousePointerCustomization": - HandleMousePointerCustomization(value); - break; - - // Touchpad Settings - case "EnableTouchPad": - HandleEnableTouchPad(value); - break; - case "TouchpadCursorSpeed": - HandleTouchpadCursorSpeed(value); - break; - - // Privacy Settings - case "ManageMicrophoneAccess": - HandleManageMicrophoneAccess(value); - break; - case "ManageCameraAccess": - HandleManageCameraAccess(value); - break; - case "ManageLocationAccess": - HandleManageLocationAccess(value); - break; - - // Power Settings - case "BatterySaverActivationLevel": - HandleBatterySaverActivationLevel(value); - break; - case "setPowerModePluggedIn": - HandleSetPowerModePluggedIn(value); - break; - case "SetPowerModeOnBattery": - HandleSetPowerModeOnBattery(value); - break; - - // Gaming Settings - case "enableGameMode": - HandleEnableGameMode(value); - break; - - // Accessibility Settings - case "EnableNarratorAction": - HandleEnableNarratorAction(value); - break; - case "EnableMagnifier": - HandleEnableMagnifier(value); - break; - case "enableStickyKeys": - HandleEnableStickyKeysAction(value); - break; - case "EnableFilterKeysAction": - HandleEnableFilterKeysAction(value); - break; - case "MonoAudioToggle": - HandleMonoAudioToggle(value); - break; - - // File Explorer Settings - case "ShowFileExtensions": - HandleShowFileExtensions(value); - break; - case "ShowHiddenAndSystemFiles": - HandleShowHiddenAndSystemFiles(value); - break; - - // Time & Region Settings - case "AutomaticTimeSettingAction": - HandleAutomaticTimeSettingAction(value); - break; - case "AutomaticDSTAdjustment": - HandleAutomaticDSTAdjustment(value); - break; - - // Focus Assist Settings - case "EnableQuietHours": - HandleEnableQuietHours(value); - break; - - // Multi-Monitor Settings - case "RememberWindowLocations": - HandleRememberWindowLocationsAction(value); - break; - case "MinimizeWindowsOnMonitorDisconnectAction": - HandleMinimizeWindowsOnMonitorDisconnectAction(value); - break; - - default: - Debug.WriteLine("Unknown command: " + key); - break; - } - } - return quit; - } + private static bool execLine(JObject root) + => s_dispatcher.Dispatch(root); /// /// Sets the airplane mode state using the Radio Management API. /// /// True to enable airplane mode, false to disable. - static void SetAirplaneMode(bool enable) + private static void SetAirplaneMode(bool enable) { IRadioManager radioManager = null; try @@ -1275,7 +966,7 @@ static void SetAirplaneMode(bool enable) /// /// Lists all WiFi networks currently in range. /// - static void ListWifiNetworks() + private static void ListWifiNetworks() { IntPtr clientHandle = IntPtr.Zero; IntPtr wlanInterfaceList = IntPtr.Zero; @@ -1375,11 +1066,19 @@ static void ListWifiNetworks() finally { if (networkList != IntPtr.Zero) + { WlanFreeMemory(networkList); + } + if (wlanInterfaceList != IntPtr.Zero) + { WlanFreeMemory(wlanInterfaceList); + } + if (clientHandle != IntPtr.Zero) + { WlanCloseHandle(clientHandle, IntPtr.Zero); + } } } @@ -1389,7 +1088,7 @@ static void ListWifiNetworks() /// /// The SSID of the network to connect to. /// Optional password for secured networks. - static void ConnectToWifi(string ssid, string password = null) + private static void ConnectToWifi(string ssid, string password = null) { IntPtr clientHandle = IntPtr.Zero; IntPtr wlanInterfaceList = IntPtr.Zero; @@ -1464,16 +1163,21 @@ static void ConnectToWifi(string ssid, string password = null) finally { if (wlanInterfaceList != IntPtr.Zero) + { WlanFreeMemory(wlanInterfaceList); + } + if (clientHandle != IntPtr.Zero) + { WlanCloseHandle(clientHandle, IntPtr.Zero); + } } } /// /// Generates a WiFi profile XML for WPA2-Personal (PSK) networks. /// - static string GenerateWifiProfileXml(string ssid, string password) + private static string GenerateWifiProfileXml(string ssid, string password) { // Convert SSID to hex string ssidHex = BitConverter.ToString(Encoding.UTF8.GetBytes(ssid)).Replace("-", ""); @@ -1513,7 +1217,7 @@ static string GenerateWifiProfileXml(string ssid, string password) /// Sets the system text scaling factor (percentage). /// /// The text scaling percentage (100-225). - static void SetTextSize(int percentage) + private static void SetTextSize(int percentage) { try { @@ -1551,7 +1255,7 @@ static void SetTextSize(int percentage) /// /// Lists all available display resolutions for the primary monitor. /// - static void ListDisplayResolutions() + private static void ListDisplayResolutions() { try { @@ -1593,7 +1297,7 @@ static void ListDisplayResolutions() /// Sets the display resolution. /// /// JSON object with "width" and "height" properties, or a string like "1920x1080". - static void SetDisplayResolution(JToken value) + private static void SetDisplayResolution(JToken value) { try { @@ -1716,7 +1420,7 @@ static void SetDisplayResolution(JToken value) } } - static void DisconnectFromWifi() + private static void DisconnectFromWifi() { IntPtr clientHandle = IntPtr.Zero; IntPtr wlanInterfaceList = IntPtr.Zero; @@ -1771,9 +1475,201 @@ static void DisconnectFromWifi() finally { if (wlanInterfaceList != IntPtr.Zero) + { WlanFreeMemory(wlanInterfaceList); + } + if (clientHandle != IntPtr.Zero) + { WlanCloseHandle(clientHandle, IntPtr.Zero); + } + } + } + + #region Bridge methods for command handlers + + /// + /// Bridge method for AppCommandHandler. + /// + internal static void HandleAppCommand(string key, string value) + { + switch (key) + { + case "LaunchProgram": + OpenApplication(value); + break; + case "CloseProgram": + CloseApplication(value); + break; + case "ListAppNames": + var installedApps = GetAllInstalledAppsIds(); + Console.WriteLine(JsonConvert.SerializeObject(installedApps.Keys)); + break; + } + } + + /// + /// Bridge method for WindowCommandHandler. + /// + internal static void HandleWindowCommand(string key, string value) + { + switch (key) + { + case "Maximize": + MaximizeWindow(value); + break; + case "Minimize": + MinimizeWindow(value); + break; + case "SwitchTo": + RaiseWindow(value); + break; + case "Tile": + string[] apps = value.Split(','); + if (apps.Length == 2) + { + TileWindowPair(apps[0], apps[1]); + } + break; + } + } + + /// + /// Bridge method for ThemeCommandHandler. + /// + internal static void HandleThemeCommand(string key, string value) + { + switch (key) + { + case "SetWallpaper": + SetDesktopWallpaper(value); + break; + case "ApplyTheme": + ApplyTheme(value); + break; + case "ListThemes": + var themes = GetInstalledThemes(); + Console.WriteLine(JsonConvert.SerializeObject(themes)); + break; + case "SetThemeMode": + if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase)) + { + ToggleLightDarkMode(); + } + else + { + if (bool.TryParse(value, out bool useLightMode)) + { + SetLightDarkMode(useLightMode); + } + else if (value.Equals("light", StringComparison.OrdinalIgnoreCase)) + { + SetLightDarkMode(true); + } + else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase)) + { + SetLightDarkMode(false); + } + } + break; + } + } + + /// + /// Bridge method for VirtualDesktopCommandHandler. + /// + internal static void HandleVirtualDesktopCommand(string key, string value, JToken rawValue) + { + switch (key) + { + case "CreateDesktop": + CreateDesktop(value); + break; + case "SwitchDesktop": + SwitchDesktop(value); + break; + case "NextDesktop": + BumpDesktopIndex(1); + break; + case "PreviousDesktop": + BumpDesktopIndex(-1); + break; + case "MoveWindowToDesktop": + MoveWindowToDesktop(rawValue); + break; + case "PinWindow": + PinWindow(value); + break; + } + } + + /// + /// Bridge method for NetworkCommandHandler. + /// + internal static void HandleNetworkCommand(string key, string value) + { + switch (key) + { + case "ToggleAirplaneMode": + SetAirplaneMode(bool.Parse(value)); + break; + case "ListWifiNetworks": + ListWifiNetworks(); + break; + case "ConnectWifi": + JObject netInfo = JObject.Parse(value); + string ssid = netInfo.Value("ssid"); + string password = netInfo["password"] is not null ? netInfo.Value("password") : ""; + ConnectToWifi(ssid, password); + break; + case "DisconnectWifi": + DisconnectFromWifi(); + break; + case "BluetoothToggle": + case "EnableWifi": + case "EnableMeteredConnections": + HandleSettingsCommand(key, value); + break; + } + } + + /// + /// Bridge method for DisplayCommandHandler. + /// + internal static void HandleDisplayCommand(string key, string value, JToken rawValue) + { + switch (key) + { + case "SetTextSize": + if (int.TryParse(value, out int textSizePct)) + { + SetTextSize(textSizePct); + } + break; + case "SetScreenResolution": + SetDisplayResolution(rawValue); + break; + case "ListResolutions": + ListDisplayResolutions(); + break; } } + + /// + /// Bridge method for SystemCommandHandler. + /// + internal static void HandleSystemCommand(string key, string value) + { + switch (key) + { + case "ToggleNotifications": + ShellExecute(IntPtr.Zero, "open", "ms-actioncenter:", null, null, 1); + break; + case "Debug": + Debugger.Launch(); + break; + } + } + + #endregion } diff --git a/dotnet/autoShell/AutoShell_Settings.cs b/dotnet/autoShell/AutoShell_Settings.cs index 4462fd6f4d..83431edeb7 100644 --- a/dotnet/autoShell/AutoShell_Settings.cs +++ b/dotnet/autoShell/AutoShell_Settings.cs @@ -21,7 +21,7 @@ internal partial class AutoShell /// Toggles Bluetooth radio on or off /// Command: {"BluetoothToggle": "{\"enableBluetooth\":true}"} /// - static void HandleBluetoothToggle(string jsonParams) + private static void HandleBluetoothToggle(string jsonParams) { try { @@ -67,9 +67,9 @@ static void HandleBluetoothToggle(string jsonParams) /// /// Enables or disables WiFi - /// Command: {"enableWifi": "{\"enable\":true}"} + /// Command: {"EnableWifi": "{\"enable\":true}"} /// - static void HandleEnableWifi(string jsonParams) + private static void HandleEnableWifi(string jsonParams) { try { @@ -99,9 +99,9 @@ static void HandleEnableWifi(string jsonParams) /// /// Enables or disables metered connection - /// Command: {"enableMeteredConnections": "{\"enable\":true}"} + /// Command: {"EnableMeteredConnections": "{\"enable\":true}"} /// - static void HandleEnableMeteredConnections(string jsonParams) + private static void HandleEnableMeteredConnections(string jsonParams) { try { @@ -131,7 +131,7 @@ static void HandleEnableMeteredConnections(string jsonParams) /// Adjusts screen brightness (increase or decrease) /// Command: {"AdjustScreenBrightness": "{\"brightnessLevel\":\"increase\"}"} /// - static void HandleAdjustScreenBrightness(string jsonParams) + private static void HandleAdjustScreenBrightness(string jsonParams) { try { @@ -158,7 +158,7 @@ static void HandleAdjustScreenBrightness(string jsonParams) /// Enables or configures Night Light (blue light filter) schedule /// Command: {"EnableBlueLightFilterSchedule": "{\"schedule\":\"sunset to sunrise\",\"nightLightScheduleDisabled\":false}"} /// - static void HandleEnableBlueLightFilterSchedule(string jsonParams) + private static void HandleEnableBlueLightFilterSchedule(string jsonParams) { try { @@ -186,9 +186,9 @@ static void HandleEnableBlueLightFilterSchedule(string jsonParams) /// /// Adjusts the color temperature for Night Light - /// Command: {"adjustColorTemperature": "{\"filterEffect\":\"reduce\"}"} + /// Command: {"AdjustColorTemperature": "{\"filterEffect\":\"reduce\"}"} /// - static void HandleAdjustColorTemperature(string jsonParams) + private static void HandleAdjustColorTemperature(string jsonParams) { try { @@ -214,7 +214,7 @@ static void HandleAdjustColorTemperature(string jsonParams) /// Sets display scaling percentage /// Command: {"DisplayScaling": "{\"sizeOverride\":\"125\"}"} /// - static void HandleDisplayScaling(string jsonParams) + private static void HandleDisplayScaling(string jsonParams) { try { @@ -248,7 +248,7 @@ static void HandleDisplayScaling(string jsonParams) /// Adjusts screen orientation /// Command: {"AdjustScreenOrientation": "{\"orientation\":\"landscape\"}"} /// - static void HandleAdjustScreenOrientation(string jsonParams) + private static void HandleAdjustScreenOrientation(string jsonParams) { try { @@ -274,7 +274,7 @@ static void HandleAdjustScreenOrientation(string jsonParams) /// Adjusts display resolution /// Command: {"DisplayResolutionAndAspectRatio": "{\"resolutionChange\":\"increase\"}"} /// - static void HandleDisplayResolutionAndAspectRatio(string jsonParams) + private static void HandleDisplayResolutionAndAspectRatio(string jsonParams) { try { @@ -300,7 +300,7 @@ static void HandleDisplayResolutionAndAspectRatio(string jsonParams) /// Locks or unlocks screen rotation /// Command: {"RotationLock": "{\"enable\":true}"} /// - static void HandleRotationLock(string jsonParams) + private static void HandleRotationLock(string jsonParams) { try { @@ -332,7 +332,7 @@ static void HandleRotationLock(string jsonParams) /// Sets system theme mode (dark or light) /// Command: {"SystemThemeMode": "{\"mode\":\"dark\"}"} /// - static void HandleSystemThemeMode(string jsonParams) + private static void HandleSystemThemeMode(string jsonParams) { try { @@ -353,7 +353,7 @@ static void HandleSystemThemeMode(string jsonParams) /// Enables or disables transparency effects /// Command: {"EnableTransparency": "{\"enable\":true}"} /// - static void HandleEnableTransparency(string jsonParams) + private static void HandleEnableTransparency(string jsonParams) { try { @@ -380,7 +380,7 @@ static void HandleEnableTransparency(string jsonParams) /// Applies accent color to title bars /// Command: {"ApplyColorToTitleBar": "{\"enableColor\":true}"} /// - static void HandleApplyColorToTitleBar(string jsonParams) + private static void HandleApplyColorToTitleBar(string jsonParams) { try { @@ -407,7 +407,7 @@ static void HandleApplyColorToTitleBar(string jsonParams) /// Enables high contrast theme /// Command: {"HighContrastTheme": "{}"} /// - static void HandleHighContrastTheme(string jsonParams) + private static void HandleHighContrastTheme(string jsonParams) { try { @@ -434,7 +434,7 @@ static void HandleHighContrastTheme(string jsonParams) /// Auto-hides the taskbar /// Command: {"AutoHideTaskbar": "{\"hideWhenNotUsing\":true,\"alwaysShow\":false}"} /// - static void HandleAutoHideTaskbar(string jsonParams) + private static void HandleAutoHideTaskbar(string jsonParams) { try { @@ -474,7 +474,7 @@ static void HandleAutoHideTaskbar(string jsonParams) /// Sets taskbar alignment (left or center) /// Command: {"TaskbarAlignment": "{\"alignment\":\"center\"}"} /// - static void HandleTaskbarAlignment(string jsonParams) + private static void HandleTaskbarAlignment(string jsonParams) { try { @@ -504,7 +504,7 @@ static void HandleTaskbarAlignment(string jsonParams) /// Shows or hides the Task View button /// Command: {"TaskViewVisibility": "{\"visibility\":true}"} /// - static void HandleTaskViewVisibility(string jsonParams) + private static void HandleTaskViewVisibility(string jsonParams) { try { @@ -532,7 +532,7 @@ static void HandleTaskViewVisibility(string jsonParams) /// Shows or hides the Widgets button /// Command: {"ToggleWidgetsButtonVisibility": "{\"visibility\":\"show\"}"} /// - static void HandleToggleWidgetsButtonVisibility(string jsonParams) + private static void HandleToggleWidgetsButtonVisibility(string jsonParams) { try { @@ -561,7 +561,7 @@ static void HandleToggleWidgetsButtonVisibility(string jsonParams) /// Shows or hides badges on taskbar icons /// Command: {"ShowBadgesOnTaskbar": "{\"enableBadging\":true}"} /// - static void HandleShowBadgesOnTaskbar(string jsonParams) + private static void HandleShowBadgesOnTaskbar(string jsonParams) { try { @@ -589,7 +589,7 @@ static void HandleShowBadgesOnTaskbar(string jsonParams) /// Shows taskbar on all monitors /// Command: {"DisplayTaskbarOnAllMonitors": "{\"enable\":true}"} /// - static void HandleDisplayTaskbarOnAllMonitors(string jsonParams) + private static void HandleDisplayTaskbarOnAllMonitors(string jsonParams) { try { @@ -617,7 +617,7 @@ static void HandleDisplayTaskbarOnAllMonitors(string jsonParams) /// Shows seconds in the system tray clock /// Command: {"DisplaySecondsInSystrayClock": "{\"enable\":true}"} /// - static void HandleDisplaySecondsInSystrayClock(string jsonParams) + private static void HandleDisplaySecondsInSystrayClock(string jsonParams) { try { @@ -649,7 +649,7 @@ static void HandleDisplaySecondsInSystrayClock(string jsonParams) /// Adjusts mouse cursor speed /// Command: {"MouseCursorSpeed": "{\"speedLevel\":10}"} /// - static void HandleMouseCursorSpeed(string jsonParams) + private static void HandleMouseCursorSpeed(string jsonParams) { try { @@ -672,7 +672,7 @@ static void HandleMouseCursorSpeed(string jsonParams) /// Sets the number of lines to scroll per mouse wheel notch /// Command: {"MouseWheelScrollLines": "{\"scrollLines\":3}"} /// - static void HandleMouseWheelScrollLines(string jsonParams) + private static void HandleMouseWheelScrollLines(string jsonParams) { try { @@ -692,9 +692,9 @@ static void HandleMouseWheelScrollLines(string jsonParams) /// /// Sets the primary mouse button - /// Command: {"setPrimaryMouseButton": "{\"primaryButton\":\"left\"}"} + /// Command: {"SetPrimaryMouseButton": "{\"primaryButton\":\"left\"}"} /// - static void HandleSetPrimaryMouseButton(string jsonParams) + private static void HandleSetPrimaryMouseButton(string jsonParams) { try { @@ -715,7 +715,7 @@ static void HandleSetPrimaryMouseButton(string jsonParams) /// Enables or disables enhanced pointer precision (mouse acceleration) /// Command: {"EnhancePointerPrecision": "{\"enable\":true}"} /// - static void HandleEnhancePointerPrecision(string jsonParams) + private static void HandleEnhancePointerPrecision(string jsonParams) { try { @@ -741,7 +741,7 @@ static void HandleEnhancePointerPrecision(string jsonParams) /// Adjusts mouse pointer size /// Command: {"AdjustMousePointerSize": "{\"sizeAdjustment\":\"increase\"}"} /// - static void HandleAdjustMousePointerSize(string jsonParams) + private static void HandleAdjustMousePointerSize(string jsonParams) { try { @@ -765,9 +765,9 @@ static void HandleAdjustMousePointerSize(string jsonParams) /// /// Customizes mouse pointer color - /// Command: {"mousePointerCustomization": "{\"color\":\"#FF0000\"}"} + /// Command: {"MousePointerCustomization": "{\"color\":\"#FF0000\"}"} /// - static void HandleMousePointerCustomization(string jsonParams) + private static void HandleMousePointerCustomization(string jsonParams) { try { @@ -797,7 +797,7 @@ static void HandleMousePointerCustomization(string jsonParams) /// Enables or disables the touchpad /// Command: {"EnableTouchPad": "{\"enable\":true}"} /// - static void HandleEnableTouchPad(string jsonParams) + private static void HandleEnableTouchPad(string jsonParams) { try { @@ -823,7 +823,7 @@ static void HandleEnableTouchPad(string jsonParams) /// Adjusts touchpad cursor speed /// Command: {"TouchpadCursorSpeed": "{\"speed\":5}"} /// - static void HandleTouchpadCursorSpeed(string jsonParams) + private static void HandleTouchpadCursorSpeed(string jsonParams) { try { @@ -853,7 +853,7 @@ static void HandleTouchpadCursorSpeed(string jsonParams) /// Manages microphone access for apps /// Command: {"ManageMicrophoneAccess": "{\"accessSetting\":\"allow\"}"} /// - static void HandleManageMicrophoneAccess(string jsonParams) + private static void HandleManageMicrophoneAccess(string jsonParams) { try { @@ -881,7 +881,7 @@ static void HandleManageMicrophoneAccess(string jsonParams) /// Manages camera access for apps /// Command: {"ManageCameraAccess": "{\"accessSetting\":\"allow\"}"} /// - static void HandleManageCameraAccess(string jsonParams) + private static void HandleManageCameraAccess(string jsonParams) { try { @@ -909,7 +909,7 @@ static void HandleManageCameraAccess(string jsonParams) /// Manages location access for apps /// Command: {"ManageLocationAccess": "{\"accessSetting\":\"allow\"}"} /// - static void HandleManageLocationAccess(string jsonParams) + private static void HandleManageLocationAccess(string jsonParams) { try { @@ -941,7 +941,7 @@ static void HandleManageLocationAccess(string jsonParams) /// Sets the battery saver activation threshold /// Command: {"BatterySaverActivationLevel": "{\"thresholdValue\":20}"} /// - static void HandleBatterySaverActivationLevel(string jsonParams) + private static void HandleBatterySaverActivationLevel(string jsonParams) { try { @@ -968,9 +968,9 @@ static void HandleBatterySaverActivationLevel(string jsonParams) /// /// Sets power mode when plugged in - /// Command: {"setPowerModePluggedIn": "{\"powerMode\":\"bestPerformance\"}"} + /// Command: {"SetPowerModePluggedIn": "{\"powerMode\":\"bestPerformance\"}"} /// - static void HandleSetPowerModePluggedIn(string jsonParams) + private static void HandleSetPowerModePluggedIn(string jsonParams) { try { @@ -996,7 +996,7 @@ static void HandleSetPowerModePluggedIn(string jsonParams) /// Sets power mode when on battery /// Command: {"SetPowerModeOnBattery": "{\"mode\":\"balanced\"}"} /// - static void HandleSetPowerModeOnBattery(string jsonParams) + private static void HandleSetPowerModeOnBattery(string jsonParams) { try { @@ -1024,9 +1024,9 @@ static void HandleSetPowerModeOnBattery(string jsonParams) /// /// Enables or disables Game Mode - /// Command: {"enableGameMode": "{}"} + /// Command: {"EnableGameMode": "{}"} /// - static void HandleEnableGameMode(string jsonParams) + private static void HandleEnableGameMode(string jsonParams) { try { @@ -1053,7 +1053,7 @@ static void HandleEnableGameMode(string jsonParams) /// Enables or disables Narrator /// Command: {"EnableNarratorAction": "{\"enable\":true}"} /// - static void HandleEnableNarratorAction(string jsonParams) + private static void HandleEnableNarratorAction(string jsonParams) { try { @@ -1086,7 +1086,7 @@ static void HandleEnableNarratorAction(string jsonParams) /// Enables or disables Magnifier /// Command: {"EnableMagnifier": "{\"enable\":true}"} /// - static void HandleEnableMagnifier(string jsonParams) + private static void HandleEnableMagnifier(string jsonParams) { try { @@ -1117,9 +1117,9 @@ static void HandleEnableMagnifier(string jsonParams) /// /// Enables or disables Sticky Keys - /// Command: {"enableStickyKeys": "{\"enable\":true}"} + /// Command: {"EnableStickyKeys": "{\"enable\":true}"} /// - static void HandleEnableStickyKeysAction(string jsonParams) + private static void HandleEnableStickyKeysAction(string jsonParams) { try { @@ -1146,7 +1146,7 @@ static void HandleEnableStickyKeysAction(string jsonParams) /// Enables or disables Filter Keys /// Command: {"EnableFilterKeysAction": "{\"enable\":true}"} /// - static void HandleEnableFilterKeysAction(string jsonParams) + private static void HandleEnableFilterKeysAction(string jsonParams) { try { @@ -1173,7 +1173,7 @@ static void HandleEnableFilterKeysAction(string jsonParams) /// Enables or disables mono audio /// Command: {"MonoAudioToggle": "{\"enable\":true}"} /// - static void HandleMonoAudioToggle(string jsonParams) + private static void HandleMonoAudioToggle(string jsonParams) { try { @@ -1204,7 +1204,7 @@ static void HandleMonoAudioToggle(string jsonParams) /// Shows or hides file extensions in File Explorer /// Command: {"ShowFileExtensions": "{\"enable\":true}"} /// - static void HandleShowFileExtensions(string jsonParams) + private static void HandleShowFileExtensions(string jsonParams) { try { @@ -1233,7 +1233,7 @@ static void HandleShowFileExtensions(string jsonParams) /// Shows or hides hidden and system files in File Explorer /// Command: {"ShowHiddenAndSystemFiles": "{\"enable\":true}"} /// - static void HandleShowHiddenAndSystemFiles(string jsonParams) + private static void HandleShowHiddenAndSystemFiles(string jsonParams) { try { @@ -1268,7 +1268,7 @@ static void HandleShowHiddenAndSystemFiles(string jsonParams) /// Enables or disables automatic time synchronization /// Command: {"AutomaticTimeSettingAction": "{\"enableAutoTimeSync\":true}"} /// - static void HandleAutomaticTimeSettingAction(string jsonParams) + private static void HandleAutomaticTimeSettingAction(string jsonParams) { try { @@ -1294,7 +1294,7 @@ static void HandleAutomaticTimeSettingAction(string jsonParams) /// Enables or disables automatic DST adjustment /// Command: {"AutomaticDSTAdjustment": "{\"enable\":true}"} /// - static void HandleAutomaticDSTAdjustment(string jsonParams) + private static void HandleAutomaticDSTAdjustment(string jsonParams) { try { @@ -1325,7 +1325,7 @@ static void HandleAutomaticDSTAdjustment(string jsonParams) /// Enables or disables Focus Assist (Quiet Hours) /// Command: {"EnableQuietHours": "{\"startHour\":22,\"endHour\":7}"} /// - static void HandleEnableQuietHours(string jsonParams) + private static void HandleEnableQuietHours(string jsonParams) { try { @@ -1356,7 +1356,7 @@ static void HandleEnableQuietHours(string jsonParams) /// Remembers window locations based on monitor configuration /// Command: {"RememberWindowLocations": "{\"enable\":true}"} /// - static void HandleRememberWindowLocationsAction(string jsonParams) + private static void HandleRememberWindowLocationsAction(string jsonParams) { try { @@ -1382,7 +1382,7 @@ static void HandleRememberWindowLocationsAction(string jsonParams) /// Minimizes windows when a monitor is disconnected /// Command: {"MinimizeWindowsOnMonitorDisconnectAction": "{\"enable\":true}"} /// - static void HandleMinimizeWindowsOnMonitorDisconnectAction(string jsonParams) + private static void HandleMinimizeWindowsOnMonitorDisconnectAction(string jsonParams) { try { @@ -1411,7 +1411,7 @@ static void HandleMinimizeWindowsOnMonitorDisconnectAction(string jsonParams) /// /// Gets the current brightness level /// - static byte GetCurrentBrightness() + private static byte GetCurrentBrightness() { try { @@ -1435,7 +1435,7 @@ static byte GetCurrentBrightness() /// /// Sets the brightness level /// - static void SetBrightness(byte brightness) + private static void SetBrightness(byte brightness) { try { @@ -1460,7 +1460,7 @@ static void SetBrightness(byte brightness) /// /// Sets DPI scaling percentage /// - static void SetDpiScaling(int percentage) + private static void SetDpiScaling(int percentage) { try { @@ -1480,7 +1480,7 @@ static void SetDpiScaling(int percentage) /// /// Refreshes the taskbar to apply changes /// - static void RefreshTaskbar() + private static void RefreshTaskbar() { try { @@ -1493,7 +1493,7 @@ static void RefreshTaskbar() /// /// Refreshes File Explorer to apply changes /// - static void RefreshExplorer() + private static void RefreshExplorer() { try { @@ -1510,7 +1510,7 @@ static void RefreshExplorer() /// /// Sets a registry value /// - static void SetRegistryValue(string keyPath, string valueName, object value) + private static void SetRegistryValue(string keyPath, string valueName, object value) { try { @@ -1524,27 +1524,92 @@ static void SetRegistryValue(string keyPath, string valueName, object value) #endregion + #region Bridge Method + + /// + /// Bridge method for SettingsCommandHandler — dispatches to individual settings handlers. + /// + internal static void HandleSettingsCommand(string key, string value) + { + switch (key) + { + case "BluetoothToggle": HandleBluetoothToggle(value); break; + case "EnableWifi": HandleEnableWifi(value); break; + case "EnableMeteredConnections": HandleEnableMeteredConnections(value); break; + case "AdjustScreenBrightness": HandleAdjustScreenBrightness(value); break; + case "EnableBlueLightFilterSchedule": HandleEnableBlueLightFilterSchedule(value); break; + case "AdjustColorTemperature": HandleAdjustColorTemperature(value); break; + case "DisplayScaling": HandleDisplayScaling(value); break; + case "AdjustScreenOrientation": HandleAdjustScreenOrientation(value); break; + case "DisplayResolutionAndAspectRatio": HandleDisplayResolutionAndAspectRatio(value); break; + case "RotationLock": HandleRotationLock(value); break; + case "SystemThemeMode": HandleSystemThemeMode(value); break; + case "EnableTransparency": HandleEnableTransparency(value); break; + case "ApplyColorToTitleBar": HandleApplyColorToTitleBar(value); break; + case "HighContrastTheme": HandleHighContrastTheme(value); break; + case "AutoHideTaskbar": HandleAutoHideTaskbar(value); break; + case "TaskbarAlignment": HandleTaskbarAlignment(value); break; + case "TaskViewVisibility": HandleTaskViewVisibility(value); break; + case "ToggleWidgetsButtonVisibility": HandleToggleWidgetsButtonVisibility(value); break; + case "ShowBadgesOnTaskbar": HandleShowBadgesOnTaskbar(value); break; + case "DisplayTaskbarOnAllMonitors": HandleDisplayTaskbarOnAllMonitors(value); break; + case "DisplaySecondsInSystrayClock": HandleDisplaySecondsInSystrayClock(value); break; + case "MouseCursorSpeed": HandleMouseCursorSpeed(value); break; + case "MouseWheelScrollLines": HandleMouseWheelScrollLines(value); break; + case "SetPrimaryMouseButton": HandleSetPrimaryMouseButton(value); break; + case "EnhancePointerPrecision": HandleEnhancePointerPrecision(value); break; + case "AdjustMousePointerSize": HandleAdjustMousePointerSize(value); break; + case "MousePointerCustomization": HandleMousePointerCustomization(value); break; + case "EnableTouchPad": HandleEnableTouchPad(value); break; + case "TouchpadCursorSpeed": HandleTouchpadCursorSpeed(value); break; + case "ManageMicrophoneAccess": HandleManageMicrophoneAccess(value); break; + case "ManageCameraAccess": HandleManageCameraAccess(value); break; + case "ManageLocationAccess": HandleManageLocationAccess(value); break; + case "BatterySaverActivationLevel": HandleBatterySaverActivationLevel(value); break; + case "SetPowerModePluggedIn": HandleSetPowerModePluggedIn(value); break; + case "SetPowerModeOnBattery": HandleSetPowerModeOnBattery(value); break; + case "EnableGameMode": HandleEnableGameMode(value); break; + case "EnableNarratorAction": HandleEnableNarratorAction(value); break; + case "EnableMagnifier": HandleEnableMagnifier(value); break; + case "EnableStickyKeys": HandleEnableStickyKeysAction(value); break; + case "EnableFilterKeysAction": HandleEnableFilterKeysAction(value); break; + case "MonoAudioToggle": HandleMonoAudioToggle(value); break; + case "ShowFileExtensions": HandleShowFileExtensions(value); break; + case "ShowHiddenAndSystemFiles": HandleShowHiddenAndSystemFiles(value); break; + case "AutomaticTimeSettingAction": HandleAutomaticTimeSettingAction(value); break; + case "AutomaticDSTAdjustment": HandleAutomaticDSTAdjustment(value); break; + case "EnableQuietHours": HandleEnableQuietHours(value); break; + case "RememberWindowLocations": HandleRememberWindowLocationsAction(value); break; + case "MinimizeWindowsOnMonitorDisconnectAction": HandleMinimizeWindowsOnMonitorDisconnectAction(value); break; + default: + Debug.WriteLine($"Unknown settings command: {key}"); + break; + } + } + + #endregion + #region Win32 API Declarations for Settings // SystemParametersInfo constants (additional ones not in AutoShell_Win32.cs) - const int SPI_SETMOUSESPEED = 0x0071; - const int SPI_GETMOUSE = 0x0003; - const int SPI_SETMOUSE = 0x0004; - const int SPI_SETWHEELSCROLLLINES = 0x0069; + private const int SPI_SETMOUSESPEED = 0x0071; + private const int SPI_GETMOUSE = 0x0003; + private const int SPI_SETMOUSE = 0x0004; + private const int SPI_SETWHEELSCROLLLINES = 0x0069; // Note: SPIF_UPDATEINIFILE, SPIF_SENDCHANGE, WM_SETTINGCHANGE, HWND_BROADCAST // are already defined in AutoShell_Win32.cs [DllImport("user32.dll", SetLastError = true)] - static extern bool SystemParametersInfo(int uiAction, int uiParam, IntPtr pvParam, int fWinIni); + private static extern bool SystemParametersInfo(int uiAction, int uiParam, IntPtr pvParam, int fWinIni); [DllImport("user32.dll", SetLastError = true)] - static extern bool SystemParametersInfo(int uiAction, int uiParam, int[] pvParam, int fWinIni); + private static extern bool SystemParametersInfo(int uiAction, int uiParam, int[] pvParam, int fWinIni); [DllImport("user32.dll")] - static extern bool SwapMouseButton(int fSwap); + private static extern bool SwapMouseButton(int fSwap); [DllImport("user32.dll", CharSet = CharSet.Auto)] - static extern IntPtr SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + private static extern IntPtr SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); #endregion } diff --git a/dotnet/autoShell/AutoShell_Themes.cs b/dotnet/autoShell/AutoShell_Themes.cs index c57c9898b2..e62bd611d7 100644 --- a/dotnet/autoShell/AutoShell_Themes.cs +++ b/dotnet/autoShell/AutoShell_Themes.cs @@ -5,393 +5,376 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.Win32; -namespace autoShell +namespace autoShell; + +/// +/// AutoShell class partial for managing Windows themes. +/// +/// This code was mostly generated by AI under the guidance of a human. +internal partial class AutoShell { - /// - /// AutoShell class partial for managing Windows themes. - /// - /// This code was mostly generated by AI under the guidance of a human. - internal partial class AutoShell + private static string s_previousTheme = null; + private static Dictionary s_themeDictionary = null; + private static Dictionary s_themeDisplayNameDictionary = null; + + private static void LoadThemes() { - private static string s_previousTheme = null; - private static Dictionary s_themeDictionary = null; - private static Dictionary s_themeDisplayNameDictionary = null; + s_themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + s_themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - private static void LoadThemes() - { - s_themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - s_themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + string[] themePaths = + [ + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Themes"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Ease of Access Themes"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Themes") + ]; - string[] themePaths = new string[] - { - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Themes"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Ease of Access Themes"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Themes") - }; - - foreach (string themesFolder in themePaths) + foreach (string themesFolder in themePaths) + { + if (Directory.Exists(themesFolder)) { - if (Directory.Exists(themesFolder)) + foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme")) { - foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme")) + string themeName = Path.GetFileNameWithoutExtension(themeFile); + if (!s_themeDictionary.ContainsKey(themeName)) { - string themeName = Path.GetFileNameWithoutExtension(themeFile); - if (!s_themeDictionary.ContainsKey(themeName)) - { - s_themeDictionary[themeName] = themeFile; + s_themeDictionary[themeName] = themeFile; - // Parse display name from theme file - string displayName = GetThemeDisplayName(themeFile); - if (!string.IsNullOrEmpty(displayName) && !s_themeDisplayNameDictionary.ContainsKey(displayName)) - { - s_themeDisplayNameDictionary[displayName] = themeName; - } + // Parse display name from theme file + string displayName = GetThemeDisplayName(themeFile); + if (!string.IsNullOrEmpty(displayName) && !s_themeDisplayNameDictionary.ContainsKey(displayName)) + { + s_themeDisplayNameDictionary[displayName] = themeName; } } } } - - s_themeDictionary["previous"] = GetCurrentTheme(); } - /// - /// Parses the display name from a .theme file. - /// - /// The full path to the .theme file. - /// The display name, or null if not found. - private static string GetThemeDisplayName(string themeFilePath) + s_themeDictionary["previous"] = GetCurrentTheme(); + } + + /// + /// Parses the display name from a .theme file. + /// + /// The full path to the .theme file. + /// The display name, or null if not found. + private static string GetThemeDisplayName(string themeFilePath) + { + try { - try + foreach (string line in File.ReadLines(themeFilePath)) { - foreach (string line in File.ReadLines(themeFilePath)) + if (line.StartsWith("DisplayName=", StringComparison.OrdinalIgnoreCase)) { - if (line.StartsWith("DisplayName=", StringComparison.OrdinalIgnoreCase)) + string displayName = line["DisplayName=".Length..].Trim(); + // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013) + if (displayName.StartsWith("@")) { - string displayName = line.Substring("DisplayName=".Length).Trim(); - // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013) - if (displayName.StartsWith("@")) - { - displayName = ResolveLocalizedString(displayName); - } - return displayName; + displayName = ResolveLocalizedString(displayName); } + return displayName; } } - catch - { - // Ignore errors reading theme file - } - return null; } + catch + { + // Ignore errors reading theme file + } + return null; + } - /// - /// Resolves a localized string resource reference. - /// - /// The localized string reference (e.g., @%SystemRoot%\System32\themeui.dll,-2013). - /// The resolved string, or the original string if resolution fails. - private static string ResolveLocalizedString(string localizedString) + /// + /// Resolves a localized string resource reference. + /// + /// The localized string reference (e.g., @%SystemRoot%\System32\themeui.dll,-2013). + /// The resolved string, or the original string if resolution fails. + private static string ResolveLocalizedString(string localizedString) + { + try { - try + // Remove the @ prefix + string resourcePath = localizedString[1..]; + // Expand environment variables + int commaIndex = resourcePath.LastIndexOf(','); + if (commaIndex > 0) { - // Remove the @ prefix - string resourcePath = localizedString.Substring(1); - // Expand environment variables - int commaIndex = resourcePath.LastIndexOf(','); - if (commaIndex > 0) + string dllPath = Environment.ExpandEnvironmentVariables(resourcePath[..commaIndex]); + string resourceIdStr = resourcePath[(commaIndex + 1)..]; + if (int.TryParse(resourceIdStr, out int resourceId)) { - string dllPath = Environment.ExpandEnvironmentVariables(resourcePath.Substring(0, commaIndex)); - string resourceIdStr = resourcePath.Substring(commaIndex + 1); - if (int.TryParse(resourceIdStr, out int resourceId)) + StringBuilder buffer = new StringBuilder(256); + IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); + if (hModule != IntPtr.Zero) { - StringBuilder buffer = new StringBuilder(256); - IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); - if (hModule != IntPtr.Zero) + try { - try - { - int result = LoadString(hModule, (uint)Math.Abs(resourceId), buffer, buffer.Capacity); - if (result > 0) - { - return buffer.ToString(); - } - } - finally + int result = LoadString(hModule, (uint)Math.Abs(resourceId), buffer, buffer.Capacity); + if (result > 0) { - FreeLibrary(hModule); + return buffer.ToString(); } } + finally + { + FreeLibrary(hModule); + } } } } - catch - { - // Ignore errors resolving localized string - } - return localizedString; } - - /// - /// Returns a list of all installed Windows themes. - /// - /// A list of theme names (without the .theme extension). - public static List GetInstalledThemes() + catch { - HashSet themes = new HashSet(); + // Ignore errors resolving localized string + } + return localizedString; + } - themes.UnionWith(s_themeDictionary.Keys); - themes.UnionWith(s_themeDisplayNameDictionary.Keys); + /// + /// Returns a list of all installed Windows themes. + /// + /// A list of theme names (without the .theme extension). + public static List GetInstalledThemes() + { + HashSet themes = []; - return themes.ToList(); - } + themes.UnionWith(s_themeDictionary.Keys); + themes.UnionWith(s_themeDisplayNameDictionary.Keys); - /// - /// Gets the current Windows theme name. - /// - /// The current theme name, or null if it cannot be determined. - public static string GetCurrentTheme() + return [.. themes]; + } + + /// + /// Gets the current Windows theme name. + /// + /// The current theme name, or null if it cannot be determined. + public static string GetCurrentTheme() + { + try { - try + using RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes"); + if (key != null) { - using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes")) + string currentThemePath = key.GetValue("CurrentTheme") as string; + if (!string.IsNullOrEmpty(currentThemePath)) { - if (key != null) - { - string currentThemePath = key.GetValue("CurrentTheme") as string; - if (!string.IsNullOrEmpty(currentThemePath)) - { - return Path.GetFileNameWithoutExtension(currentThemePath); - } - } + return Path.GetFileNameWithoutExtension(currentThemePath); } } - catch - { - // Ignore errors reading registry - } - return null; + } + catch + { + // Ignore errors reading registry + } + return null; + } + + /// + /// Applies a Windows theme by name. + /// + /// The name of the theme to apply (without .theme extension). + /// True if the theme was applied successfully, false otherwise. + public static bool ApplyTheme(string themeName) + { + string themePath = FindThemePath(themeName); + if (string.IsNullOrEmpty(themePath)) + { + return false; } - /// - /// Applies a Windows theme by name. - /// - /// The name of the theme to apply (without .theme extension). - /// True if the theme was applied successfully, false otherwise. - public static bool ApplyTheme(string themeName) + try { - string themePath = FindThemePath(themeName); - if (string.IsNullOrEmpty(themePath)) + string previous = GetCurrentTheme(); + + if (!themeName.Equals("previous", StringComparison.InvariantCultureIgnoreCase)) { - return false; - } + // Apply theme by opening the .theme file + Process p = Process.Start(themePath); + s_previousTheme = previous; + p.Exited += P_Exited; - try + return true; + } + else { - string previous = GetCurrentTheme(); + bool success = RevertToPreviousTheme(); - if (themeName.ToLowerInvariant() != "previous") + if (success) { - // Apply theme by opening the .theme file - Process p = Process.Start(themePath); s_previousTheme = previous; - p.Exited += P_Exited; - - return true; } - else - { - bool success = RevertToPreviousTheme(); - if (success) - { - s_previousTheme = previous; - } - - return success; - } - } - catch - { - return false; + return success; } } - - private static void P_Exited(object sender, EventArgs e) + catch { - Debug.WriteLine(((Process)sender).ExitCode); + return false; } + } - /// - /// Reverts to the previous Windows theme. - /// - /// True if the previous theme was applied successfully, false otherwise. - public static bool RevertToPreviousTheme() - { - if (string.IsNullOrEmpty(s_previousTheme)) - { - return false; - } - - string themePath = FindThemePath(s_previousTheme); - if (string.IsNullOrEmpty(themePath)) - { - return false; - } + private static void P_Exited(object sender, EventArgs e) + { + Debug.WriteLine(((Process)sender).ExitCode); + } - try - { - Process.Start(themePath); - return true; - } - catch - { - return false; - } + /// + /// Reverts to the previous Windows theme. + /// + /// True if the previous theme was applied successfully, false otherwise. + public static bool RevertToPreviousTheme() + { + if (string.IsNullOrEmpty(s_previousTheme)) + { + return false; } - /// - /// Gets the name of the previous theme. - /// - /// The previous theme name, or null if no theme change has been made. - public static string GetPreviousTheme() + string themePath = FindThemePath(s_previousTheme); + if (string.IsNullOrEmpty(themePath)) { - return s_previousTheme; + return false; } - /// - /// Finds the full path to a theme file by name or display name. - /// - /// The name of the theme (file name without extension or display name). - /// The full path to the theme file, or null if not found. - private static string FindThemePath(string themeName) + try { - // First check by file name - if (s_themeDictionary.TryGetValue(themeName, out string themePath)) - { - return themePath; - } + Process.Start(themePath); + return true; + } + catch + { + return false; + } + } - // Then check by display name - if (s_themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) - { - if (s_themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) - { - return themePathFromDisplay; - } - } + /// + /// Gets the name of the previous theme. + /// + /// The previous theme name, or null if no theme change has been made. + public static string GetPreviousTheme() + { + return s_previousTheme; + } - return null; + /// + /// Finds the full path to a theme file by name or display name. + /// + /// The name of the theme (file name without extension or display name). + /// The full path to the theme file, or null if not found. + private static string FindThemePath(string themeName) + { + // First check by file name + if (s_themeDictionary.TryGetValue(themeName, out string themePath)) + { + return themePath; } - /// - /// Sets the Windows light or dark mode by modifying registry keys. - /// - /// True for light mode, false for dark mode. - /// True if the mode was set successfully, false otherwise. - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - public static bool SetLightDarkMode(bool useLightMode) + // Then check by display name + if (s_themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) { - try + if (s_themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) { - const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - int value = useLightMode ? 1 : 0; + return themePathFromDisplay; + } + } - using (RegistryKey key = Registry.CurrentUser.OpenSubKey(personalizePath, true)) - { - if (key == null) - { - return false; - } + return null; + } - // Set apps theme - key.SetValue("AppsUseLightTheme", value, RegistryValueKind.DWord); + /// + /// Sets the Windows light or dark mode by modifying registry keys. + /// + /// True for light mode, false for dark mode. + /// True if the mode was set successfully, false otherwise. + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + public static bool SetLightDarkMode(bool useLightMode) + { + try + { + const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + int value = useLightMode ? 1 : 0; - // Set system theme (taskbar, Start menu, etc.) - key.SetValue("SystemUsesLightTheme", value, RegistryValueKind.DWord); + using (RegistryKey key = Registry.CurrentUser.OpenSubKey(personalizePath, true)) + { + if (key == null) + { + return false; } - // Broadcast settings change notification to update UI - BroadcastSettingsChange(); + // Set apps theme + key.SetValue("AppsUseLightTheme", value, RegistryValueKind.DWord); - return true; - } - catch - { - return false; + // Set system theme (taskbar, Start menu, etc.) + key.SetValue("SystemUsesLightTheme", value, RegistryValueKind.DWord); } - } - /// - /// Toggles between light and dark mode. - /// - /// True if the toggle was successful, false otherwise. - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - public static bool ToggleLightDarkMode() - { - bool? currentMode = GetCurrentLightMode(); - if (currentMode.HasValue) - { - return SetLightDarkMode(!currentMode.Value); - } - return false; - } + // Broadcast settings change notification to update UI + BroadcastSettingsChange(); - /// - /// Broadcasts a WM_SETTINGCHANGE message to notify the system of theme changes. - /// - private static void BroadcastSettingsChange() + return true; + } + catch { - const int HWND_BROADCAST = 0xffff; - const int WM_SETTINGCHANGE = 0x001A; - const uint SMTO_ABORTIFHUNG = 0x0002; - IntPtr result; - SendMessageTimeout( - (IntPtr)HWND_BROADCAST, - WM_SETTINGCHANGE, - IntPtr.Zero, - "ImmersiveColorSet", - SMTO_ABORTIFHUNG, - 1000, - out result); + return false; } + } - /// - /// Gets the current light/dark mode setting from the registry. - /// - /// True if light mode, false if dark mode, null if unable to determine. - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - private static bool? GetCurrentLightMode() - { - try - { - const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + /// + /// Toggles between light and dark mode. + /// + /// True if the toggle was successful, false otherwise. + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + public static bool ToggleLightDarkMode() + { + bool? currentMode = GetCurrentLightMode(); + return currentMode.HasValue ? SetLightDarkMode(!currentMode.Value) : false; + } - using (RegistryKey key = Registry.CurrentUser.OpenSubKey(personalizePath, false)) - { - if (key == null) - { - return null; - } + /// + /// Broadcasts a WM_SETTINGCHANGE message to notify the system of theme changes. + /// + private static void BroadcastSettingsChange() + { + const int HWND_BROADCAST = 0xffff; + const int WM_SETTINGCHANGE = 0x001A; + const uint SMTO_ABORTIFHUNG = 0x0002; + SendMessageTimeout( + (IntPtr)HWND_BROADCAST, + WM_SETTINGCHANGE, + IntPtr.Zero, + "ImmersiveColorSet", + SMTO_ABORTIFHUNG, + 1000, + out nint result); + } - // Read AppsUseLightTheme value (0 = dark, 1 = light) - object value = key.GetValue("AppsUseLightTheme"); - if (value is int intValue) - { - return intValue == 1; - } + /// + /// Gets the current light/dark mode setting from the registry. + /// + /// True if light mode, false if dark mode, null if unable to determine. + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + private static bool? GetCurrentLightMode() + { + try + { + const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - return null; - } - } - catch + using RegistryKey key = Registry.CurrentUser.OpenSubKey(personalizePath, false); + if (key == null) { return null; } - } + // Read AppsUseLightTheme value (0 = dark, 1 = light) + object value = key.GetValue("AppsUseLightTheme"); + return value is int intValue ? intValue == 1 : null; + } + catch + { + return null; + } } + } diff --git a/dotnet/autoShell/AutoShell_Win32.cs b/dotnet/autoShell/AutoShell_Win32.cs index c2794a625e..7e2656bcae 100644 --- a/dotnet/autoShell/AutoShell_Win32.cs +++ b/dotnet/autoShell/AutoShell_Win32.cs @@ -2,11 +2,8 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; using System.Windows; namespace autoShell @@ -39,25 +36,25 @@ internal struct Size // import GetWindowRect [DllImport("user32.dll")] - static extern bool GetWindowRect(IntPtr hWnd, ref RECT Rect); + private static extern bool GetWindowRect(IntPtr hWnd, ref RECT Rect); // import GetShellWindow [DllImport("user32.dll")] - static extern IntPtr GetShellWindow(); + private static extern IntPtr GetShellWindow(); // import GetDesktopWindow [DllImport("user32.dll")] - static extern IntPtr GetDesktopWindow(); + private static extern IntPtr GetDesktopWindow(); // import SetForegroundWindow [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll", EntryPoint = "SendMessage", SetLastError = true)] - static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, UInt32 wParam, IntPtr lParam); + private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, UInt32 wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - static extern IntPtr SendMessageTimeout( + private static extern IntPtr SendMessageTimeout( IntPtr hWnd, uint Msg, IntPtr wParam, @@ -68,11 +65,11 @@ static extern IntPtr SendMessageTimeout( // import SetWindowPos [DllImport("user32.dll")] - static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); // import ShowWindow [DllImport("user32.dll")] - static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow); + private static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow); // import FindWindowEx [DllImport("user32.dll")] @@ -334,17 +331,17 @@ internal interface IServiceProvider10 [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] - static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] - static extern bool IsWindowVisible(IntPtr hWnd); + private static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] - static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); // get handle of active window [DllImport("user32.dll")] @@ -405,31 +402,31 @@ internal interface IRadioManager // WLAN API P/Invoke declarations [DllImport("wlanapi.dll")] - static extern int WlanOpenHandle(uint dwClientVersion, IntPtr pReserved, out uint pdwNegotiatedVersion, out IntPtr phClientHandle); + private static extern int WlanOpenHandle(uint dwClientVersion, IntPtr pReserved, out uint pdwNegotiatedVersion, out IntPtr phClientHandle); [DllImport("wlanapi.dll")] - static extern int WlanCloseHandle(IntPtr hClientHandle, IntPtr pReserved); + private static extern int WlanCloseHandle(IntPtr hClientHandle, IntPtr pReserved); [DllImport("wlanapi.dll")] - static extern int WlanEnumInterfaces(IntPtr hClientHandle, IntPtr pReserved, out IntPtr ppInterfaceList); + private static extern int WlanEnumInterfaces(IntPtr hClientHandle, IntPtr pReserved, out IntPtr ppInterfaceList); [DllImport("wlanapi.dll")] - static extern int WlanGetAvailableNetworkList(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, IntPtr pReserved, out IntPtr ppAvailableNetworkList); + private static extern int WlanGetAvailableNetworkList(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, IntPtr pReserved, out IntPtr ppAvailableNetworkList); [DllImport("wlanapi.dll")] - static extern int WlanScan(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pDot11Ssid, IntPtr pIeData, IntPtr pReserved); + private static extern int WlanScan(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pDot11Ssid, IntPtr pIeData, IntPtr pReserved); [DllImport("wlanapi.dll")] - static extern void WlanFreeMemory(IntPtr pMemory); + private static extern void WlanFreeMemory(IntPtr pMemory); [DllImport("wlanapi.dll")] - static extern int WlanConnect(IntPtr hClientHandle, ref Guid pInterfaceGuid, ref WLAN_CONNECTION_PARAMETERS pConnectionParameters, IntPtr pReserved); + private static extern int WlanConnect(IntPtr hClientHandle, ref Guid pInterfaceGuid, ref WLAN_CONNECTION_PARAMETERS pConnectionParameters, IntPtr pReserved); [DllImport("wlanapi.dll")] - static extern int WlanDisconnect(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pReserved); + private static extern int WlanDisconnect(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pReserved); [DllImport("wlanapi.dll")] - static extern int WlanSetProfile(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string strProfileXml, [MarshalAs(UnmanagedType.LPWStr)] string strAllUserProfileSecurity, bool bOverwrite, IntPtr pReserved, out uint pdwReasonCode); + private static extern int WlanSetProfile(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string strProfileXml, [MarshalAs(UnmanagedType.LPWStr)] string strAllUserProfileSecurity, bool bOverwrite, IntPtr pReserved, out uint pdwReasonCode); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct WLAN_INTERFACE_INFO diff --git a/dotnet/autoShell/Handlers/AppCommandHandler.cs b/dotnet/autoShell/Handlers/AppCommandHandler.cs new file mode 100644 index 0000000000..8487105892 --- /dev/null +++ b/dotnet/autoShell/Handlers/AppCommandHandler.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles application lifecycle commands: launchProgram, closeProgram, listAppNames. +/// +internal class AppCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "CloseProgram", + "LaunchProgram", + "ListAppNames", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + AutoShell.HandleAppCommand(key, value); + } +} diff --git a/dotnet/autoShell/Handlers/AudioCommandHandler.cs b/dotnet/autoShell/Handlers/AudioCommandHandler.cs new file mode 100644 index 0000000000..92b9f85f40 --- /dev/null +++ b/dotnet/autoShell/Handlers/AudioCommandHandler.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles volume, mute, and restoreVolume commands. +/// +internal class AudioCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "Mute", + "RestoreVolume", + "Volume", + ]; + + private readonly IAudioService _audio; + private double _savedVolumePct; + + public AudioCommandHandler(IAudioService audio) + { + _audio = audio; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "Volume": + if (int.TryParse(value, out int pct)) + { + _savedVolumePct = _audio.GetVolume(); + _audio.SetVolume(pct); + } + break; + case "RestoreVolume": + _audio.SetVolume((int)_savedVolumePct); + break; + case "Mute": + if (bool.TryParse(value, out bool mute)) + { + _audio.SetMute(mute); + } + break; + } + } +} diff --git a/dotnet/autoShell/Handlers/CommandDispatcher.cs b/dotnet/autoShell/Handlers/CommandDispatcher.cs new file mode 100644 index 0000000000..f52428188d --- /dev/null +++ b/dotnet/autoShell/Handlers/CommandDispatcher.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Routes incoming JSON commands to the appropriate handler via a direct dictionary lookup. +/// +internal class CommandDispatcher +{ + private readonly Dictionary _handlers = []; + + public void Register(params ICommandHandler[] handlers) + { + foreach (var handler in handlers) + { + foreach (string command in handler.SupportedCommands) + { + _handlers[command] = handler; + } + } + } + + /// + /// Dispatches all commands in a JSON object to their handlers. + /// Returns true if the application should quit. + /// + public bool Dispatch(JObject root) + { + foreach (var kvp in root) + { + string key = kvp.Key; + + if (key == "quit") + { + return true; + } + + string value = kvp.Value.ToString(); + + try + { + if (_handlers.TryGetValue(key, out ICommandHandler handler)) + { + handler.Handle(key, value, kvp.Value); + } + else + { + Debug.WriteLine("Unknown command: " + key); + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + return false; + } +} diff --git a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs new file mode 100644 index 0000000000..3347fe37ff --- /dev/null +++ b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles display commands: setScreenResolution, listResolutions, setTextSize. +/// Delegates to existing static methods in AutoShell. +/// +internal class DisplayCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "ListResolutions", + "SetScreenResolution", + "SetTextSize", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + AutoShell.HandleDisplayCommand(key, value, rawValue); + } +} diff --git a/dotnet/autoShell/Handlers/ICommandHandler.cs b/dotnet/autoShell/Handlers/ICommandHandler.cs new file mode 100644 index 0000000000..de5587f8a6 --- /dev/null +++ b/dotnet/autoShell/Handlers/ICommandHandler.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Interface for command handlers that process autoShell actions. +/// +internal interface ICommandHandler +{ + /// + /// Returns the set of command keys this handler supports. + /// + IEnumerable SupportedCommands { get; } + + /// + /// Handles the command identified by . + /// + /// The command key from the incoming JSON object. + /// The string representation of the command's value. + /// The original JToken value for commands that need structured data. + void Handle(string key, string value, JToken rawValue); +} diff --git a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs new file mode 100644 index 0000000000..a0385e1d7c --- /dev/null +++ b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles network commands: toggleAirplaneMode, listWifiNetworks, connectWifi, disconnectWifi. +/// Delegates to existing static methods in AutoShell. +/// +internal class NetworkCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "BluetoothToggle", + "ConnectWifi", + "DisconnectWifi", + "EnableMeteredConnections", + "EnableWifi", + "ListWifiNetworks", + "ToggleAirplaneMode", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + AutoShell.HandleNetworkCommand(key, value); + } +} diff --git a/dotnet/autoShell/Handlers/SettingsCommandHandler.cs b/dotnet/autoShell/Handlers/SettingsCommandHandler.cs new file mode 100644 index 0000000000..d9c7ff036b --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsCommandHandler.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles all Windows Settings actions (50+ registry/SystemParametersInfo-based toggles). +/// Groups: Network, Display, Personalization, Taskbar, Mouse, Touchpad, Privacy, Power, +/// Gaming, Accessibility, File Explorer, Time/Region, Focus Assist, Multi-Monitor. +/// +internal class SettingsCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + // Accessibility + "EnableFilterKeysAction", + "EnableMagnifier", + "EnableNarratorAction", + "EnableStickyKeys", + "MonoAudioToggle", + // Display + "AdjustScreenBrightness", + "AdjustScreenOrientation", + "AdjustColorTemperature", + "DisplayResolutionAndAspectRatio", + "DisplayScaling", + "EnableBlueLightFilterSchedule", + "RotationLock", + // File Explorer + "ShowFileExtensions", + "ShowHiddenAndSystemFiles", + // Focus Assist + "EnableQuietHours", + // Gaming + "EnableGameMode", + // Mouse + "AdjustMousePointerSize", + "EnhancePointerPrecision", + "MouseCursorSpeed", + "MousePointerCustomization", + "MouseWheelScrollLines", + "SetPrimaryMouseButton", + // Multi-Monitor + "MinimizeWindowsOnMonitorDisconnectAction", + "RememberWindowLocations", + // Personalization + "ApplyColorToTitleBar", + "EnableTransparency", + "HighContrastTheme", + "SystemThemeMode", + // Power + "BatterySaverActivationLevel", + "SetPowerModeOnBattery", + "SetPowerModePluggedIn", + // Privacy + "ManageCameraAccess", + "ManageLocationAccess", + "ManageMicrophoneAccess", + // Taskbar + "AutoHideTaskbar", + "DisplaySecondsInSystrayClock", + "DisplayTaskbarOnAllMonitors", + "ShowBadgesOnTaskbar", + "TaskbarAlignment", + "TaskViewVisibility", + "ToggleWidgetsButtonVisibility", + // Time & Region + "AutomaticDSTAdjustment", + "AutomaticTimeSettingAction", + // Touchpad + "EnableTouchPad", + "TouchpadCursorSpeed", + ]; + + private readonly IRegistryService _registry; + private readonly ISystemParametersService _systemParams; + private readonly IProcessService _process; + + public SettingsCommandHandler( + IRegistryService registry, + ISystemParametersService systemParams, + IProcessService process) + { + _registry = registry; + _systemParams = systemParams; + _process = process; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + // Delegate to the existing static handlers in AutoShell_Settings.cs + // This preserves all existing behavior during Phase 1 + AutoShell.HandleSettingsCommand(key, value); + } +} diff --git a/dotnet/autoShell/Handlers/SystemCommandHandler.cs b/dotnet/autoShell/Handlers/SystemCommandHandler.cs new file mode 100644 index 0000000000..9bf7fce55b --- /dev/null +++ b/dotnet/autoShell/Handlers/SystemCommandHandler.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles system/utility commands: quit, debug, toggleNotifications. +/// +internal class SystemCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "Debug", + "ToggleNotifications", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + AutoShell.HandleSystemCommand(key, value); + } +} diff --git a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs new file mode 100644 index 0000000000..bb7bbc3873 --- /dev/null +++ b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles theme-related commands: applyTheme, listThemes, setThemeMode, setWallpaper. +/// Delegates to existing static methods in AutoShell. +/// +internal class ThemeCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "ApplyTheme", + "ListThemes", + "SetThemeMode", + "SetWallpaper", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + AutoShell.HandleThemeCommand(key, value); + } +} diff --git a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs new file mode 100644 index 0000000000..d6caca3d10 --- /dev/null +++ b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles virtual desktop commands: createDesktop, switchDesktop, nextDesktop, previousDesktop, +/// moveWindowToDesktop, pinWindow. +/// Delegates to existing static methods in AutoShell. +/// +internal class VirtualDesktopCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "CreateDesktop", + "MoveWindowToDesktop", + "NextDesktop", + "PinWindow", + "PreviousDesktop", + "SwitchDesktop", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + AutoShell.HandleVirtualDesktopCommand(key, value, rawValue); + } +} diff --git a/dotnet/autoShell/Handlers/WindowCommandHandler.cs b/dotnet/autoShell/Handlers/WindowCommandHandler.cs new file mode 100644 index 0000000000..3516e1491b --- /dev/null +++ b/dotnet/autoShell/Handlers/WindowCommandHandler.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles window management commands: maximize, minimize, switchTo, tile. +/// +internal class WindowCommandHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "Maximize", + "Minimize", + "SwitchTo", + "Tile", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + AutoShell.HandleWindowCommand(key, value); + } +} diff --git a/dotnet/autoShell/Services/IAudioService.cs b/dotnet/autoShell/Services/IAudioService.cs new file mode 100644 index 0000000000..d7ddde370c --- /dev/null +++ b/dotnet/autoShell/Services/IAudioService.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace autoShell.Services; + +/// +/// Abstracts Windows Core Audio API operations for testability. +/// +internal interface IAudioService +{ + /// + /// Sets the system volume to the specified percentage (0–100). + /// + void SetVolume(int percent); + + /// + /// Gets the current system volume as a percentage (0–100). + /// + int GetVolume(); + + /// + /// Sets or clears the system mute state. + /// + void SetMute(bool mute); + + /// + /// Gets the current system mute state. + /// + bool GetMute(); +} diff --git a/dotnet/autoShell/Services/IProcessService.cs b/dotnet/autoShell/Services/IProcessService.cs new file mode 100644 index 0000000000..d104dbc7c0 --- /dev/null +++ b/dotnet/autoShell/Services/IProcessService.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace autoShell.Services; + +/// +/// Abstracts process management operations for testability. +/// +internal interface IProcessService +{ + /// + /// Returns all processes with the specified name. + /// + Process[] GetProcessesByName(string name); + + /// + /// Starts a new process with the specified start info. + /// + Process Start(ProcessStartInfo startInfo); + + /// + /// Starts a process using the OS shell (e.g., opening a URL or file). + /// + void StartShellExecute(string fileName); +} diff --git a/dotnet/autoShell/Services/IRegistryService.cs b/dotnet/autoShell/Services/IRegistryService.cs new file mode 100644 index 0000000000..d126d827b4 --- /dev/null +++ b/dotnet/autoShell/Services/IRegistryService.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace autoShell.Services; + +/// +/// Abstracts Windows Registry operations for testability. +/// +internal interface IRegistryService +{ + /// + /// Gets a value from the registry under HKEY_CURRENT_USER. + /// + /// The registry subkey path. + /// The name of the value to retrieve. + /// The value to return if the key or value does not exist. + object GetValue(string keyPath, string valueName, object defaultValue = null); + + /// + /// Sets a value in the registry under HKEY_CURRENT_USER. + /// + /// The registry subkey path (created if it does not exist). + /// The name of the value to set. + /// The data to store. + /// The registry data type. + void SetValue(string keyPath, string valueName, object value, Microsoft.Win32.RegistryValueKind valueKind); +} diff --git a/dotnet/autoShell/Services/ISystemParametersService.cs b/dotnet/autoShell/Services/ISystemParametersService.cs new file mode 100644 index 0000000000..261d8f8c05 --- /dev/null +++ b/dotnet/autoShell/Services/ISystemParametersService.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace autoShell.Services; + +/// +/// Abstracts SystemParametersInfo and related Win32 system parameter calls for testability. +/// +internal interface ISystemParametersService +{ + /// + /// Sets a system parameter via SystemParametersInfo with an IntPtr value. + /// + /// The system parameter action constant (SPI_SET*). + /// Additional parameter whose meaning depends on the action. + /// Pointer to the value to set. + /// Flags controlling persistence and notification (SPIF_*). + bool SetParameter(int action, int param, IntPtr vparam, int flags); + + /// + /// Sets a system parameter via SystemParametersInfo with a string value. + /// + /// The system parameter action constant (SPI_SET*). + /// Additional parameter whose meaning depends on the action. + /// The string value to set. + /// Flags controlling persistence and notification (SPIF_*). + bool SetParameter(int action, int param, string vparam, int flags); + + /// + /// Gets a system parameter via SystemParametersInfo into an int array. + /// + /// The system parameter action constant (SPI_GET*). + /// Additional parameter whose meaning depends on the action. + /// Array to receive the value. + /// Flags (typically 0 for get operations). + bool GetParameter(int action, int param, int[] vparam, int flags); +} diff --git a/dotnet/autoShell/Services/WindowsAudioService.cs b/dotnet/autoShell/Services/WindowsAudioService.cs new file mode 100644 index 0000000000..12954d055c --- /dev/null +++ b/dotnet/autoShell/Services/WindowsAudioService.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IAudioService using Windows Core Audio COM API. +/// +internal class WindowsAudioService : IAudioService +{ + /// + public void SetVolume(int percent) + { + try + { + var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator(); + deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device); + var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID; + device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj); + var audioEndpointVolume = (IAudioEndpointVolume)obj; + audioEndpointVolume.SetMasterVolumeLevelScalar(percent / 100.0f, Guid.Empty); + } + catch (Exception ex) + { + Debug.WriteLine("Failed to set volume: " + ex.Message); + } + } + + /// + public int GetVolume() + { + try + { + var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator(); + deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device); + var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID; + device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj); + var audioEndpointVolume = (IAudioEndpointVolume)obj; + audioEndpointVolume.GetMasterVolumeLevelScalar(out float currentVolume); + return (int)(currentVolume * 100.0); + } + catch (Exception ex) + { + Debug.WriteLine("Failed to get volume: " + ex.Message); + return 0; + } + } + + /// + public void SetMute(bool mute) + { + try + { + var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator(); + deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device); + var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID; + device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj); + var audioEndpointVolume = (IAudioEndpointVolume)obj; + audioEndpointVolume.SetMute(mute, Guid.Empty); + } + catch (Exception ex) + { + Debug.WriteLine("Failed to set mute: " + ex.Message); + } + } + + /// + public bool GetMute() + { + try + { + var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator(); + deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device); + var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID; + device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj); + var audioEndpointVolume = (IAudioEndpointVolume)obj; + audioEndpointVolume.GetMute(out bool currentMute); + return currentMute; + } + catch (Exception ex) + { + Debug.WriteLine("Failed to get mute: " + ex.Message); + return false; + } + } +} diff --git a/dotnet/autoShell/Services/WindowsProcessService.cs b/dotnet/autoShell/Services/WindowsProcessService.cs new file mode 100644 index 0000000000..aee7529fe1 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsProcessService.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IProcessService using System.Diagnostics.Process. +/// +internal class WindowsProcessService : IProcessService +{ + /// + public Process[] GetProcessesByName(string name) + { + return Process.GetProcessesByName(name); + } + + /// + public Process Start(ProcessStartInfo startInfo) + { + return Process.Start(startInfo); + } + + /// + public void StartShellExecute(string fileName) + { + Process.Start(new ProcessStartInfo + { + FileName = fileName, + UseShellExecute = true + }); + } +} diff --git a/dotnet/autoShell/Services/WindowsRegistryService.cs b/dotnet/autoShell/Services/WindowsRegistryService.cs new file mode 100644 index 0000000000..bafce5de41 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsRegistryService.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Win32; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IRegistryService using Windows Registry. +/// +internal class WindowsRegistryService : IRegistryService +{ + /// + public object GetValue(string keyPath, string valueName, object defaultValue = null) + { + using var key = Registry.CurrentUser.OpenSubKey(keyPath); + return key?.GetValue(valueName, defaultValue) ?? defaultValue; + } + + /// + public void SetValue(string keyPath, string valueName, object value, RegistryValueKind valueKind) + { + using var key = Registry.CurrentUser.CreateSubKey(keyPath); + key?.SetValue(valueName, value, valueKind); + } +} diff --git a/dotnet/autoShell/Services/WindowsSystemParametersService.cs b/dotnet/autoShell/Services/WindowsSystemParametersService.cs new file mode 100644 index 0000000000..08ea001840 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsSystemParametersService.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; + +namespace autoShell.Services; + +/// +/// Concrete implementation of ISystemParametersService using Win32 P/Invoke. +/// +internal partial class WindowsSystemParametersService : ISystemParametersService +{ + private const int SPIF_UPDATEINIFILE = 0x01; + private const int SPIF_SENDCHANGE = 0x02; + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SystemParametersInfo(int uiAction, int uiParam, IntPtr pvParam, int fWinIni); + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SystemParametersInfo(int uiAction, int uiParam, int[] pvParam, int fWinIni); + + [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)] + private static partial int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); + + /// + public bool SetParameter(int action, int param, IntPtr vparam, int flags) + { + return SystemParametersInfo(action, param, vparam, flags); + } + + /// + public bool SetParameter(int action, int param, string vparam, int flags) + { + return SystemParametersInfo(action, param, vparam, flags) != 0; + } + + /// + public bool GetParameter(int action, int param, int[] vparam, int flags) + { + return SystemParametersInfo(action, param, vparam, flags); + } +} diff --git a/ts/packages/agents/desktop/src/actionsSchema.ts b/ts/packages/agents/desktop/src/actionsSchema.ts index 9abdecc107..dd08a77d0b 100644 --- a/ts/packages/agents/desktop/src/actionsSchema.ts +++ b/ts/packages/agents/desktop/src/actionsSchema.ts @@ -35,13 +35,13 @@ export type DesktopActions = // Example: // user: Launch edge // agent: { -// "actionName": "launchProgram", +// "actionName": "LaunchProgram", // "parameters": { // "name": "edge" // } // } export type LaunchProgramAction = { - actionName: "launchProgram"; + actionName: "LaunchProgram"; parameters: { name: KnownPrograms | string; // The name of the software application }; @@ -49,7 +49,7 @@ export type LaunchProgramAction = { // Closes a program window on a Windows Desktop export type CloseProgramAction = { - actionName: "closeProgram"; + actionName: "CloseProgram"; parameters: { name: KnownPrograms | string; // The name of the software application }; @@ -57,7 +57,7 @@ export type CloseProgramAction = { // Maximizes a program window on a Windows Desktop export type MaximizeWindowAction = { - actionName: "maximize"; + actionName: "Maximize"; parameters: { name: KnownPrograms | string; // The name of the software application }; @@ -65,7 +65,7 @@ export type MaximizeWindowAction = { // Minimizes a program window on a Windows Desktop export type MinimizeWindowAction = { - actionName: "minimize"; + actionName: "Minimize"; parameters: { name: KnownPrograms | string; // The name of the software application }; @@ -73,7 +73,7 @@ export type MinimizeWindowAction = { // Sets focus to a program window on a Windows Desktop export type SwitchToWindowAction = { - actionName: "switchTo"; + actionName: "SwitchTo"; parameters: { name: KnownPrograms | string; // The name of the software application }; @@ -81,7 +81,7 @@ export type SwitchToWindowAction = { // Positions program windows on a program window on a Windows Desktop export type TileWindowsAction = { - actionName: "tile"; + actionName: "Tile"; parameters: { leftWindow: KnownPrograms | string; // The name of the software application rightWindow: KnownPrograms | string; // The name of the software application @@ -89,25 +89,25 @@ export type TileWindowsAction = { }; export type SetVolumeAction = { - actionName: "volume"; + actionName: "Volume"; parameters: { targetVolume: number; // value between 0 and 100 }; }; export type RestoreVolumeAction = { - actionName: "restoreVolume"; + actionName: "RestoreVolume"; }; export type MuteVolumeAction = { - actionName: "mute"; + actionName: "Mute"; parameters: { on: boolean; }; }; export type SetWallpaperAction = { - actionName: "setWallpaper"; + actionName: "SetWallpaper"; parameters: { filePath?: string; // The path to the file url?: string; // The url to the image @@ -116,14 +116,14 @@ export type SetWallpaperAction = { // Sets the theme mode of the current [windows] desktop export type ChangeThemeModeAction = { - actionName: "setThemeMode"; + actionName: "SetThemeMode"; parameters: { mode: "light" | "dark" | "toggle"; // the theme mode }; }; export type ConnectWifiAction = { - actionName: "connectWifi"; + actionName: "ConnectWifi"; parameters: { ssid: string; // The SSID of the wifi network password?: string; // The password of the wifi network, if required @@ -132,14 +132,14 @@ export type ConnectWifiAction = { // Disconnects from the current wifi network export type DisconnectWifiAction = { - actionName: "disconnectWifi"; + actionName: "DisconnectWifi"; parameters: { // No parameters required }; }; export type ToggleAirplaneModeAction = { - actionName: "toggleAirplaneMode"; + actionName: "ToggleAirplaneMode"; parameters: { enable: boolean; // true to enable, false to disable }; @@ -147,14 +147,14 @@ export type ToggleAirplaneModeAction = { // creates a new Windows Desktop export type CreateDesktopAction = { - actionName: "createDesktop"; + actionName: "CreateDesktop"; parameters: { names: string[]; // The name(s) of the desktop(s) to create (default: Desktop 1, Desktop 2, etc.) }; }; export type MoveWindowToDesktopAction = { - actionName: "moveWindowToDesktop"; + actionName: "MoveWindowToDesktop"; parameters: { name: KnownPrograms | string; // The name of the software application desktopId: number; // The ID of the desktop to move the window to @@ -162,14 +162,14 @@ export type MoveWindowToDesktopAction = { }; export type PinWindowToAllDesktopsAction = { - actionName: "pinWindow"; + actionName: "PinWindow"; parameters: { name: KnownPrograms | string; // The name of the software application }; }; export type SwitchDesktopAction = { - actionName: "switchDesktop"; + actionName: "SwitchDesktop"; parameters: { desktopId: number; // The ID of the desktop to switch to }; @@ -177,7 +177,7 @@ export type SwitchDesktopAction = { // switches to the next Windows Desktop export type NextDesktopAction = { - actionName: "nextDesktop"; + actionName: "NextDesktop"; parameters: { // No parameters required }; @@ -185,7 +185,7 @@ export type NextDesktopAction = { // switches to the previous Windows Desktop export type PreviousDesktopAction = { - actionName: "previousDesktop"; + actionName: "PreviousDesktop"; parameters: { // No parameters required }; @@ -193,7 +193,7 @@ export type PreviousDesktopAction = { // Shows/hides windows notification center export type ToggleNotificationsAction = { - actionName: "toggleNotifications"; + actionName: "ToggleNotifications"; parameters: { enable: boolean; // true to enable, false to disable }; @@ -201,13 +201,13 @@ export type ToggleNotificationsAction = { // Attaches the debugger to the AutoShell process export type DebugAutoShellAction = { - actionName: "debug"; + actionName: "Debug"; parameters: {}; }; // Changes the text size that appears throughout Windows and your apps export type SetTextSizeAction = { - actionName: "setTextSize"; + actionName: "SetTextSize"; parameters: { // small changes are 5% increments, large changes are 25% increments size: number; // size in percentage (100% is default) (range is 100 - 225) @@ -216,7 +216,7 @@ export type SetTextSizeAction = { // Change screen resolution export type SetScreenResolutionAction = { - actionName: "setScreenResolution"; + actionName: "SetScreenResolution"; parameters: { width: number; // width in pixels height: number; // height in pixels @@ -235,7 +235,7 @@ export type BluetoothToggleAction = { // Enables or disables WiFi adapter export type EnableWifiAction = { - actionName: "enableWifi"; + actionName: "EnableWifi"; parameters: { enable: boolean; // true to enable, false to disable }; diff --git a/ts/packages/agents/desktop/src/connector.ts b/ts/packages/agents/desktop/src/connector.ts index b95c0e662b..72e00df3e5 100644 --- a/ts/packages/agents/desktop/src/connector.ts +++ b/ts/packages/agents/desktop/src/connector.ts @@ -5,7 +5,7 @@ import child_process from "node:child_process"; import { fileURLToPath } from "node:url"; import { ProgramNameIndex, loadProgramNameIndex } from "./programNameIndex.js"; import { Storage } from "@typeagent/agent-sdk"; -import registerDebug from "debug"; +import registerDebug from "Debug"; import { AllDesktopActions } from "./allActionsSchema.js"; import fs from "node:fs"; import path from "node:path"; @@ -81,7 +81,7 @@ export async function runDesktopActions( debug(`Executing action '${actionName}' from schema '${schemaName}'`); } switch (actionName) { - case "setWallpaper": { + case "SetWallpaper": { let file = action.parameters.filePath; const rootTypeAgentDir = path.join(os.homedir(), ".typeagent"); @@ -150,7 +150,7 @@ export async function runDesktopActions( confirmationMessage = "Set wallpaper to " + actionData; break; } - case "launchProgram": { + case "LaunchProgram": { actionData = await mapInputToAppName( action.parameters.name, agentContext, @@ -158,7 +158,7 @@ export async function runDesktopActions( confirmationMessage = "Launched " + action.parameters.name; break; } - case "closeProgram": { + case "CloseProgram": { actionData = await mapInputToAppName( action.parameters.name, agentContext, @@ -166,7 +166,7 @@ export async function runDesktopActions( confirmationMessage = "Closed " + action.parameters.name; break; } - case "maximize": { + case "Maximize": { actionData = await mapInputToAppName( action.parameters.name, agentContext, @@ -174,7 +174,7 @@ export async function runDesktopActions( confirmationMessage = "Maximized " + action.parameters.name; break; } - case "minimize": { + case "Minimize": { actionData = await mapInputToAppName( action.parameters.name, agentContext, @@ -182,7 +182,7 @@ export async function runDesktopActions( confirmationMessage = "Minimized " + action.parameters.name; break; } - case "switchTo": { + case "SwitchTo": { actionData = await mapInputToAppName( action.parameters.name, agentContext, @@ -190,7 +190,7 @@ export async function runDesktopActions( confirmationMessage = "Switched to " + action.parameters.name; break; } - case "tile": { + case "Tile": { const left = await mapInputToAppName( action.parameters.leftWindow, agentContext, @@ -203,24 +203,24 @@ export async function runDesktopActions( confirmationMessage = `Tiled ${left} on the left and ${right} on the right`; break; } - case "volume": { + case "Volume": { actionData = action.parameters.targetVolume.toString(); break; } - case "restoreVolume": { + case "RestoreVolume": { actionData = ""; break; } - case "mute": { + case "Mute": { actionData = String(action.parameters.on); break; } - case "setThemeMode": { + case "SetThemeMode": { actionData = action.parameters!.mode; confirmationMessage = `Changed theme to '${action.parameters.mode}'`; break; } - case "connectWifi": { + case "ConnectWifi": { actionData = { ssid: action.parameters.ssid, password: action.parameters.password @@ -230,17 +230,17 @@ export async function runDesktopActions( confirmationMessage = `Connecting to WiFi network '${action.parameters.ssid}'`; break; } - case "disconnectWifi": { + case "DisconnectWifi": { actionData = ""; confirmationMessage = `Disconnecting from current WiFi network`; break; } - case "toggleAirplaneMode": { + case "ToggleAirplaneMode": { actionData = action.parameters.enable.toString(); confirmationMessage = `Turning airplane mode ${action.parameters.enable ? "on" : "off"}`; break; } - case "createDesktop": { + case "CreateDesktop": { actionData = action.parameters?.names !== undefined ? JSON.stringify(action.parameters.names) @@ -248,7 +248,7 @@ export async function runDesktopActions( confirmationMessage = `Creating new desktop`; break; } - case "moveWindowToDesktop": { + case "MoveWindowToDesktop": { const app = { process: await mapInputToAppName( action.parameters.name, @@ -260,42 +260,42 @@ export async function runDesktopActions( confirmationMessage = `Moving ${app.process} to desktop ${action.parameters.desktopId}`; break; } - case "pinWindow": { + case "PinWindow": { actionData = action.parameters.name; confirmationMessage = `Pinning '${action.parameters.name}' to all desktops`; break; } - case "switchDesktop": { + case "SwitchDesktop": { actionData = action.parameters.desktopId.toString(); confirmationMessage = `Switching to desktop ${action.parameters.desktopId}`; break; } - case "nextDesktop": { + case "NextDesktop": { actionData = ""; confirmationMessage = `Switching to next desktop`; break; } - case "previousDesktop": { + case "PreviousDesktop": { actionData = ""; confirmationMessage = `Switching to previous desktop`; break; } - case "toggleNotifications": { + case "ToggleNotifications": { actionData = action.parameters.enable.toString(); confirmationMessage = `Toggling Action Center ${action.parameters.enable ? "on" : "off"}`; break; } - case "debug": { + case "Debug": { actionData = ""; confirmationMessage = `Debug action executed`; break; } - case "setTextSize": { + case "SetTextSize": { actionData = action.parameters.size.toString(); confirmationMessage = `Set text size to ${action.parameters.size}%`; break; } - case "setScreenResolution": { + case "SetScreenResolution": { actionData = { width: action.parameters.width, height: action.parameters.height, @@ -314,12 +314,12 @@ export async function runDesktopActions( confirmationMessage = `Bluetooth ${action.parameters.enableBluetooth !== false ? "enabled" : "disabled"}`; break; } - case "enableWifi": { + case "EnableWifi": { actionData = JSON.stringify({ enable: action.parameters.enable }); confirmationMessage = `WiFi ${action.parameters.enable ? "enabled" : "disabled"}`; break; } - case "enableMeteredConnections": { + case "EnableMeteredConnections": { actionData = JSON.stringify({ enable: action.parameters.enable }); confirmationMessage = `Metered connections ${action.parameters.enable ? "enabled" : "disabled"}`; break; @@ -342,7 +342,7 @@ export async function runDesktopActions( confirmationMessage = `Night Light schedule ${action.parameters.nightLightScheduleDisabled ? "disabled" : "enabled"}`; break; } - case "adjustColorTemperature": { + case "AdjustColorTemperature": { actionData = JSON.stringify({ filterEffect: action.parameters.filterEffect, }); @@ -458,7 +458,7 @@ export async function runDesktopActions( confirmationMessage = `Mouse wheel scroll lines set to ${action.parameters.scrollLines}`; break; } - case "setPrimaryMouseButton": { + case "SetPrimaryMouseButton": { actionData = JSON.stringify({ primaryButton: action.parameters.primaryButton, }); @@ -479,7 +479,7 @@ export async function runDesktopActions( confirmationMessage = `Mouse pointer size adjusted`; break; } - case "mousePointerCustomization": { + case "MousePointerCustomization": { actionData = JSON.stringify({ color: action.parameters.color, style: action.parameters.style, @@ -531,7 +531,7 @@ export async function runDesktopActions( confirmationMessage = `Battery saver threshold set to ${action.parameters.thresholdValue}%`; break; } - case "setPowerModePluggedIn": { + case "SetPowerModePluggedIn": { actionData = JSON.stringify({ powerMode: action.parameters.powerMode, }); @@ -545,7 +545,7 @@ export async function runDesktopActions( } // Gaming Settings - case "enableGameMode": { + case "EnableGameMode": { actionData = JSON.stringify({}); confirmationMessage = `Opening Game Mode settings`; break; @@ -566,7 +566,7 @@ export async function runDesktopActions( confirmationMessage = `Magnifier ${action.parameters.enable !== false ? "enabled" : "disabled"}`; break; } - case "enableStickyKeys": { + case "EnableStickyKeys": { actionData = JSON.stringify({ enable: action.parameters.enable }); confirmationMessage = `Sticky Keys ${action.parameters.enable ? "enabled" : "disabled"}`; break; @@ -668,7 +668,7 @@ async function fetchInstalledApps(desktopProcess: child_process.ChildProcess) { const appsPromise = new Promise((resolve, reject) => { let message: Record = {}; - message["listAppNames"] = ""; + message["ListAppNames"] = ""; let allOutput = ""; const dataCallBack = (data: any) => { diff --git a/ts/packages/agents/desktop/src/windows/displayActionsSchema.ts b/ts/packages/agents/desktop/src/windows/displayActionsSchema.ts index 4ceaacdc2f..906826b16c 100644 --- a/ts/packages/agents/desktop/src/windows/displayActionsSchema.ts +++ b/ts/packages/agents/desktop/src/windows/displayActionsSchema.ts @@ -19,7 +19,7 @@ export type EnableBlueLightFilterScheduleAction = { // Adjusts the color temperature for Night Light export type AdjustColorTemperatureAction = { - actionName: "adjustColorTemperature"; + actionName: "AdjustColorTemperature"; parameters: { filterEffect?: "reduce" | "increase"; }; diff --git a/ts/packages/agents/desktop/src/windows/inputActionsSchema.ts b/ts/packages/agents/desktop/src/windows/inputActionsSchema.ts index 195324d144..1f7b7ef785 100644 --- a/ts/packages/agents/desktop/src/windows/inputActionsSchema.ts +++ b/ts/packages/agents/desktop/src/windows/inputActionsSchema.ts @@ -30,7 +30,7 @@ export type MouseWheelScrollLinesAction = { // Sets the primary mouse button export type SetPrimaryMouseButtonAction = { - actionName: "setPrimaryMouseButton"; + actionName: "SetPrimaryMouseButton"; parameters: { primaryButton: "left" | "right"; }; @@ -54,7 +54,7 @@ export type AdjustMousePointerSizeAction = { // Customizes mouse pointer color export type MousePointerCustomizationAction = { - actionName: "mousePointerCustomization"; + actionName: "MousePointerCustomization"; parameters: { color: string; style?: string; diff --git a/ts/packages/agents/desktop/src/windows/powerActionsSchema.ts b/ts/packages/agents/desktop/src/windows/powerActionsSchema.ts index e026eb99b3..0c07622f9a 100644 --- a/ts/packages/agents/desktop/src/windows/powerActionsSchema.ts +++ b/ts/packages/agents/desktop/src/windows/powerActionsSchema.ts @@ -16,7 +16,7 @@ export type BatterySaverActivationLevelAction = { // Sets power mode when plugged in export type SetPowerModePluggedInAction = { - actionName: "setPowerModePluggedIn"; + actionName: "SetPowerModePluggedIn"; parameters: { powerMode: "bestPerformance" | "balanced" | "bestPowerEfficiency"; }; diff --git a/ts/packages/agents/desktop/src/windows/systemActionsSchema.ts b/ts/packages/agents/desktop/src/windows/systemActionsSchema.ts index 8077059a73..97ef074692 100644 --- a/ts/packages/agents/desktop/src/windows/systemActionsSchema.ts +++ b/ts/packages/agents/desktop/src/windows/systemActionsSchema.ts @@ -19,7 +19,7 @@ export type DesktopSystemActions = // Enables or disables metered connection settings export type EnableMeteredConnectionsAction = { - actionName: "enableMeteredConnections"; + actionName: "EnableMeteredConnections"; parameters: { enable: boolean; }; @@ -27,7 +27,7 @@ export type EnableMeteredConnectionsAction = { // Enables or disables Game Mode export type EnableGameModeAction = { - actionName: "enableGameMode"; + actionName: "EnableGameMode"; parameters: {}; }; @@ -49,7 +49,7 @@ export type EnableMagnifierAction = { // Enables or disables Sticky Keys export type EnableStickyKeysAction = { - actionName: "enableStickyKeys"; + actionName: "EnableStickyKeys"; parameters: { enable: boolean; }; From 66dc1a2f7dd9c14e403dfe706716193554fa3e3f Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 31 Mar 2026 21:07:40 -0700 Subject: [PATCH 03/27] removed some dead code --- dotnet/autoShell/AutoShell.cs | 59 ----------------------------------- 1 file changed, 59 deletions(-) diff --git a/dotnet/autoShell/AutoShell.cs b/dotnet/autoShell/AutoShell.cs index 78a7985339..193c110364 100644 --- a/dotnet/autoShell/AutoShell.cs +++ b/dotnet/autoShell/AutoShell.cs @@ -24,7 +24,6 @@ internal partial class AutoShell // create a map of friendly names to executable paths private static Hashtable s_friendlyNameToPath = []; private static Hashtable s_friendlyNameToId = []; - private static double s_savedVolumePct = 0.0; private static SortedList s_sortedList; private static IServiceProvider10 s_shell; @@ -233,64 +232,6 @@ private static SortedList GetAllInstalledAppsIds() return appIds; } - private static void SetMasterVolume(int pct) - { - // Using Windows Core Audio API via COM interop - try - { - var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator(); - deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device); - var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID; - device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj); - var audioEndpointVolume = (IAudioEndpointVolume)obj; - audioEndpointVolume.GetMasterVolumeLevelScalar(out float currentVolume); - s_savedVolumePct = currentVolume * 100.0; - audioEndpointVolume.SetMasterVolumeLevelScalar(pct / 100.0f, Guid.Empty); - } - catch (Exception ex) - { - Debug.WriteLine("Failed to set volume: " + ex.Message); - } - } - - private static void RestoreMasterVolume() - { - // Using Windows Core Audio API via COM interop - try - { - var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator(); - deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device); - var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID; - device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj); - var audioEndpointVolume = (IAudioEndpointVolume)obj; - audioEndpointVolume.SetMasterVolumeLevelScalar((float)(s_savedVolumePct / 100.0), Guid.Empty); - } - catch (Exception ex) - { - Debug.WriteLine("Failed to restore volume: " + ex.Message); - } - } - - private static void SetMasterMute(bool mute) - { - // Using Windows Core Audio API via COM interop - try - { - var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator(); - deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out IMMDevice device); - var audioEndpointVolumeGuid = typeof(IAudioEndpointVolume).GUID; - device.Activate(ref audioEndpointVolumeGuid, 0, IntPtr.Zero, out object obj); - var audioEndpointVolume = (IAudioEndpointVolume)obj; - audioEndpointVolume.GetMute(out bool currentMute); - Debug.WriteLine("Current Mute:" + currentMute); - audioEndpointVolume.SetMute(mute, Guid.Empty); - } - catch (Exception ex) - { - Debug.WriteLine("Failed to set mute: " + ex.Message); - } - } - private static string ResolveProcessNameFromFriendlyName(string friendlyName) { string path = (string)s_friendlyNameToPath[friendlyName.ToLowerInvariant()]; From ec806b2963eb4c8e5fc5d9c476f1c0bd948fba0b Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 31 Mar 2026 22:05:27 -0700 Subject: [PATCH 04/27] Fix .agr files: PascalCase actionName values to match TS schemas The PascalCase rename missed the .agr (agent grammar) files, causing agc compilation failures in CI. Updated all actionName values in 5 .agr files to match their PascalCase counterparts in the TypeScript schemas. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agents/desktop/src/desktopSchema.agr | 104 +++++++++--------- .../desktop/src/windows/displaySchema.agr | 10 +- .../desktop/src/windows/inputSchema.agr | 16 +-- .../desktop/src/windows/powerSchema.agr | 14 +-- .../desktop/src/windows/systemSchema.agr | 22 ++-- 5 files changed, 83 insertions(+), 83 deletions(-) diff --git a/ts/packages/agents/desktop/src/desktopSchema.agr b/ts/packages/agents/desktop/src/desktopSchema.agr index 903b0fc78d..a3831147c7 100644 --- a/ts/packages/agents/desktop/src/desktopSchema.agr +++ b/ts/packages/agents/desktop/src/desktopSchema.agr @@ -45,14 +45,14 @@ import { DesktopActions } from "./actionsSchema.ts"; | $(name:wildcard) -> name; // ===== Program Launch/Close ===== - = launch $(program:) -> { actionName: "launchProgram", parameters: { name: program } } - | open $(program:) -> { actionName: "launchProgram", parameters: { name: program } } - | start $(program:) -> { actionName: "launchProgram", parameters: { name: program } } - | run $(program:) -> { actionName: "launchProgram", parameters: { name: program } }; + = launch $(program:) -> { actionName: "LaunchProgram", parameters: { name: program } } + | open $(program:) -> { actionName: "LaunchProgram", parameters: { name: program } } + | start $(program:) -> { actionName: "LaunchProgram", parameters: { name: program } } + | run $(program:) -> { actionName: "LaunchProgram", parameters: { name: program } }; - = close $(program:) -> { actionName: "closeProgram", parameters: { name: program } } - | quit $(program:) -> { actionName: "closeProgram", parameters: { name: program } } - | exit $(program:) -> { actionName: "closeProgram", parameters: { name: program } }; + = close $(program:) -> { actionName: "CloseProgram", parameters: { name: program } } + | quit $(program:) -> { actionName: "CloseProgram", parameters: { name: program } } + | exit $(program:) -> { actionName: "CloseProgram", parameters: { name: program } }; // ===== Window Management ===== = @@ -60,56 +60,56 @@ import { DesktopActions } from "./actionsSchema.ts"; | | ; - = maximize $(program:) -> { actionName: "maximize", parameters: { name: program } } - | make $(program:) full screen -> { actionName: "maximize", parameters: { name: program } }; + = maximize $(program:) -> { actionName: "Maximize", parameters: { name: program } } + | make $(program:) full screen -> { actionName: "Maximize", parameters: { name: program } }; - = minimize $(program:) -> { actionName: "minimize", parameters: { name: program } } - | hide $(program:) -> { actionName: "minimize", parameters: { name: program } }; + = minimize $(program:) -> { actionName: "Minimize", parameters: { name: program } } + | hide $(program:) -> { actionName: "Minimize", parameters: { name: program } }; - = switch to $(program:) -> { actionName: "switchTo", parameters: { name: program } } - | focus (on)? $(program:) -> { actionName: "switchTo", parameters: { name: program } } - | go to $(program:) -> { actionName: "switchTo", parameters: { name: program } }; + = switch to $(program:) -> { actionName: "SwitchTo", parameters: { name: program } } + | focus (on)? $(program:) -> { actionName: "SwitchTo", parameters: { name: program } } + | go to $(program:) -> { actionName: "SwitchTo", parameters: { name: program } }; - = tile $(left:) on (the)? left and $(right:) on (the)? right -> { actionName: "tile", parameters: { leftWindow: left, rightWindow: right } } - | put $(left:) on (the)? left and $(right:) on (the)? right -> { actionName: "tile", parameters: { leftWindow: left, rightWindow: right } } - | tile $(left:) and $(right:) -> { actionName: "tile", parameters: { leftWindow: left, rightWindow: right } }; + = tile $(left:) on (the)? left and $(right:) on (the)? right -> { actionName: "Tile", parameters: { leftWindow: left, rightWindow: right } } + | put $(left:) on (the)? left and $(right:) on (the)? right -> { actionName: "Tile", parameters: { leftWindow: left, rightWindow: right } } + | tile $(left:) and $(right:) -> { actionName: "Tile", parameters: { leftWindow: left, rightWindow: right } }; // ===== Volume Control ===== - = set volume to $(level:number) (percent)? -> { actionName: "volume", parameters: { targetVolume: level } } - | volume $(level:number) (percent)? -> { actionName: "volume", parameters: { targetVolume: level } } - | mute -> { actionName: "mute", parameters: { on: true } } - | unmute -> { actionName: "mute", parameters: { on: false } } - | restore volume -> { actionName: "restoreVolume" }; + = set volume to $(level:number) (percent)? -> { actionName: "Volume", parameters: { targetVolume: level } } + | volume $(level:number) (percent)? -> { actionName: "Volume", parameters: { targetVolume: level } } + | mute -> { actionName: "Mute", parameters: { on: true } } + | unmute -> { actionName: "Mute", parameters: { on: false } } + | restore volume -> { actionName: "RestoreVolume" }; // ===== Theme Mode ===== - = set theme to dark -> { actionName: "setThemeMode", parameters: { mode: "dark" } } - | set theme to light -> { actionName: "setThemeMode", parameters: { mode: "light" } } - | toggle (theme)? mode -> { actionName: "setThemeMode", parameters: { mode: "toggle" } } - | enable dark mode -> { actionName: "setThemeMode", parameters: { mode: "dark" } } - | enable light mode -> { actionName: "setThemeMode", parameters: { mode: "light" } } - | switch to dark (mode)? -> { actionName: "setThemeMode", parameters: { mode: "dark" } } - | switch to light (mode)? -> { actionName: "setThemeMode", parameters: { mode: "light" } }; + = set theme to dark -> { actionName: "SetThemeMode", parameters: { mode: "dark" } } + | set theme to light -> { actionName: "SetThemeMode", parameters: { mode: "light" } } + | toggle (theme)? mode -> { actionName: "SetThemeMode", parameters: { mode: "toggle" } } + | enable dark mode -> { actionName: "SetThemeMode", parameters: { mode: "dark" } } + | enable light mode -> { actionName: "SetThemeMode", parameters: { mode: "light" } } + | switch to dark (mode)? -> { actionName: "SetThemeMode", parameters: { mode: "dark" } } + | switch to light (mode)? -> { actionName: "SetThemeMode", parameters: { mode: "light" } }; // ===== WiFi Control ===== - = connect to $(ssid:wildcard) wifi -> { actionName: "connectWifi", parameters: { ssid } } - | connect to wifi $(ssid:wildcard) -> { actionName: "connectWifi", parameters: { ssid } } - | disconnect (from)? wifi -> { actionName: "disconnectWifi", parameters: {} } - | enable airplane mode -> { actionName: "toggleAirplaneMode", parameters: { enable: true } } - | disable airplane mode -> { actionName: "toggleAirplaneMode", parameters: { enable: false } } - | turn on airplane mode -> { actionName: "toggleAirplaneMode", parameters: { enable: true } } - | turn off airplane mode -> { actionName: "toggleAirplaneMode", parameters: { enable: false } }; + = connect to $(ssid:wildcard) wifi -> { actionName: "ConnectWifi", parameters: { ssid } } + | connect to wifi $(ssid:wildcard) -> { actionName: "ConnectWifi", parameters: { ssid } } + | disconnect (from)? wifi -> { actionName: "DisconnectWifi", parameters: {} } + | enable airplane mode -> { actionName: "ToggleAirplaneMode", parameters: { enable: true } } + | disable airplane mode -> { actionName: "ToggleAirplaneMode", parameters: { enable: false } } + | turn on airplane mode -> { actionName: "ToggleAirplaneMode", parameters: { enable: true } } + | turn off airplane mode -> { actionName: "ToggleAirplaneMode", parameters: { enable: false } }; // ===== Desktop Management ===== - = create (new)? desktop -> { actionName: "createDesktop", parameters: { names: [] } } - | create desktop $(name:wildcard) -> { actionName: "createDesktop", parameters: { names: [name] } } - | move $(program:) to desktop $(id:number) -> { actionName: "moveWindowToDesktop", parameters: { name: program, desktopId: id } } - | pin $(program:) (to all desktops)? -> { actionName: "pinWindow", parameters: { name: program } } - | switch to desktop $(id:number) -> { actionName: "switchDesktop", parameters: { desktopId: id } } - | next desktop -> { actionName: "nextDesktop", parameters: {} } - | previous desktop -> { actionName: "previousDesktop", parameters: {} } - | show notifications -> { actionName: "toggleNotifications", parameters: { enable: true } } - | hide notifications -> { actionName: "toggleNotifications", parameters: { enable: false } } - | toggle notifications -> { actionName: "toggleNotifications", parameters: { enable: true } }; + = create (new)? desktop -> { actionName: "CreateDesktop", parameters: { names: [] } } + | create desktop $(name:wildcard) -> { actionName: "CreateDesktop", parameters: { names: [name] } } + | move $(program:) to desktop $(id:number) -> { actionName: "MoveWindowToDesktop", parameters: { name: program, desktopId: id } } + | pin $(program:) (to all desktops)? -> { actionName: "PinWindow", parameters: { name: program } } + | switch to desktop $(id:number) -> { actionName: "SwitchDesktop", parameters: { desktopId: id } } + | next desktop -> { actionName: "NextDesktop", parameters: {} } + | previous desktop -> { actionName: "PreviousDesktop", parameters: {} } + | show notifications -> { actionName: "ToggleNotifications", parameters: { enable: true } } + | hide notifications -> { actionName: "ToggleNotifications", parameters: { enable: false } } + | toggle notifications -> { actionName: "ToggleNotifications", parameters: { enable: true } }; // ===== Network Settings (core) ===== = @@ -123,12 +123,12 @@ import { DesktopActions } from "./actionsSchema.ts"; | toggle bluetooth -> { actionName: "BluetoothToggle", parameters: {} }; = - turn on wifi -> { actionName: "enableWifi", parameters: { enable: true } } - | turn off wifi -> { actionName: "enableWifi", parameters: { enable: false } } - | enable wifi -> { actionName: "enableWifi", parameters: { enable: true } } - | disable wifi -> { actionName: "enableWifi", parameters: { enable: false } } - | turn on wi\-fi -> { actionName: "enableWifi", parameters: { enable: true } } - | turn off wi\-fi -> { actionName: "enableWifi", parameters: { enable: false } }; + turn on wifi -> { actionName: "EnableWifi", parameters: { enable: true } } + | turn off wifi -> { actionName: "EnableWifi", parameters: { enable: false } } + | enable wifi -> { actionName: "EnableWifi", parameters: { enable: true } } + | disable wifi -> { actionName: "EnableWifi", parameters: { enable: false } } + | turn on wi\-fi -> { actionName: "EnableWifi", parameters: { enable: true } } + | turn off wi\-fi -> { actionName: "EnableWifi", parameters: { enable: false } }; // ===== Brightness Control (core) ===== = diff --git a/ts/packages/agents/desktop/src/windows/displaySchema.agr b/ts/packages/agents/desktop/src/windows/displaySchema.agr index 5bbae4bff7..2089edb9e6 100644 --- a/ts/packages/agents/desktop/src/windows/displaySchema.agr +++ b/ts/packages/agents/desktop/src/windows/displaySchema.agr @@ -21,11 +21,11 @@ import { DesktopDisplayActions } from "./displayActionsSchema.ts"; | disable blue light filter -> { actionName: "EnableBlueLightFilterSchedule", parameters: { schedule: "", nightLightScheduleDisabled: true } }; = - adjust color temperature -> { actionName: "adjustColorTemperature", parameters: {} } - | reduce blue light -> { actionName: "adjustColorTemperature", parameters: { filterEffect: "reduce" } } - | increase blue light -> { actionName: "adjustColorTemperature", parameters: { filterEffect: "increase" } } - | warmer (screen)? colors -> { actionName: "adjustColorTemperature", parameters: { filterEffect: "reduce" } } - | cooler (screen)? colors -> { actionName: "adjustColorTemperature", parameters: { filterEffect: "increase" } }; + adjust color temperature -> { actionName: "AdjustColorTemperature", parameters: {} } + | reduce blue light -> { actionName: "AdjustColorTemperature", parameters: { filterEffect: "reduce" } } + | increase blue light -> { actionName: "AdjustColorTemperature", parameters: { filterEffect: "increase" } } + | warmer (screen)? colors -> { actionName: "AdjustColorTemperature", parameters: { filterEffect: "reduce" } } + | cooler (screen)? colors -> { actionName: "AdjustColorTemperature", parameters: { filterEffect: "increase" } }; = set (display)? scaling to $(size:string) (percent)? -> { actionName: "DisplayScaling", parameters: { sizeOverride: size } } diff --git a/ts/packages/agents/desktop/src/windows/inputSchema.agr b/ts/packages/agents/desktop/src/windows/inputSchema.agr index 4c5639a95a..fbf2a20e13 100644 --- a/ts/packages/agents/desktop/src/windows/inputSchema.agr +++ b/ts/packages/agents/desktop/src/windows/inputSchema.agr @@ -26,11 +26,11 @@ import { DesktopInputActions } from "./inputActionsSchema.ts"; | scroll $(lines:number) lines (per notch)? -> { actionName: "MouseWheelScrollLines", parameters: { scrollLines: lines } }; = - set primary mouse button to left -> { actionName: "setPrimaryMouseButton", parameters: { primaryButton: "left" } } - | set primary mouse button to right -> { actionName: "setPrimaryMouseButton", parameters: { primaryButton: "right" } } - | swap mouse buttons -> { actionName: "setPrimaryMouseButton", parameters: { primaryButton: "right" } } - | use left handed mouse -> { actionName: "setPrimaryMouseButton", parameters: { primaryButton: "right" } } - | use right handed mouse -> { actionName: "setPrimaryMouseButton", parameters: { primaryButton: "left" } }; + set primary mouse button to left -> { actionName: "SetPrimaryMouseButton", parameters: { primaryButton: "left" } } + | set primary mouse button to right -> { actionName: "SetPrimaryMouseButton", parameters: { primaryButton: "right" } } + | swap mouse buttons -> { actionName: "SetPrimaryMouseButton", parameters: { primaryButton: "right" } } + | use left handed mouse -> { actionName: "SetPrimaryMouseButton", parameters: { primaryButton: "right" } } + | use right handed mouse -> { actionName: "SetPrimaryMouseButton", parameters: { primaryButton: "left" } }; = enable enhanced pointer precision -> { actionName: "EnhancePointerPrecision", parameters: { enable: true } } @@ -49,9 +49,9 @@ import { DesktopInputActions } from "./inputActionsSchema.ts"; | smaller cursor -> { actionName: "AdjustMousePointerSize", parameters: { sizeAdjustment: "decrease" } }; = - customize mouse pointer to $(color:wildcard) -> { actionName: "mousePointerCustomization", parameters: { color } } - | change mouse pointer (style)? to $(color:wildcard) -> { actionName: "mousePointerCustomization", parameters: { color } } - | set mouse pointer color to $(color:wildcard) -> { actionName: "mousePointerCustomization", parameters: { color } }; + customize mouse pointer to $(color:wildcard) -> { actionName: "MousePointerCustomization", parameters: { color } } + | change mouse pointer (style)? to $(color:wildcard) -> { actionName: "MousePointerCustomization", parameters: { color } } + | set mouse pointer color to $(color:wildcard) -> { actionName: "MousePointerCustomization", parameters: { color } }; = enable touchpad -> { actionName: "EnableTouchPad", parameters: { enable: true } } diff --git a/ts/packages/agents/desktop/src/windows/powerSchema.agr b/ts/packages/agents/desktop/src/windows/powerSchema.agr index d11a658afd..11f6058091 100644 --- a/ts/packages/agents/desktop/src/windows/powerSchema.agr +++ b/ts/packages/agents/desktop/src/windows/powerSchema.agr @@ -17,13 +17,13 @@ import { DesktopPowerActions } from "./powerActionsSchema.ts"; | turn on battery saver at $(level:number) (percent)? -> { actionName: "BatterySaverActivationLevel", parameters: { thresholdValue: level } }; = - set power mode to best performance -> { actionName: "setPowerModePluggedIn", parameters: { powerMode: "bestPerformance" } } - | set power mode to balanced -> { actionName: "setPowerModePluggedIn", parameters: { powerMode: "balanced" } } - | set power mode to best power efficiency -> { actionName: "setPowerModePluggedIn", parameters: { powerMode: "bestPowerEfficiency" } } - | enable best performance mode -> { actionName: "setPowerModePluggedIn", parameters: { powerMode: "bestPerformance" } } - | enable balanced mode -> { actionName: "setPowerModePluggedIn", parameters: { powerMode: "balanced" } } - | enable power saver mode -> { actionName: "setPowerModePluggedIn", parameters: { powerMode: "bestPowerEfficiency" } } - | use best performance -> { actionName: "setPowerModePluggedIn", parameters: { powerMode: "bestPerformance" } }; + set power mode to best performance -> { actionName: "SetPowerModePluggedIn", parameters: { powerMode: "bestPerformance" } } + | set power mode to balanced -> { actionName: "SetPowerModePluggedIn", parameters: { powerMode: "balanced" } } + | set power mode to best power efficiency -> { actionName: "SetPowerModePluggedIn", parameters: { powerMode: "bestPowerEfficiency" } } + | enable best performance mode -> { actionName: "SetPowerModePluggedIn", parameters: { powerMode: "bestPerformance" } } + | enable balanced mode -> { actionName: "SetPowerModePluggedIn", parameters: { powerMode: "balanced" } } + | enable power saver mode -> { actionName: "SetPowerModePluggedIn", parameters: { powerMode: "bestPowerEfficiency" } } + | use best performance -> { actionName: "SetPowerModePluggedIn", parameters: { powerMode: "bestPerformance" } }; = set battery power mode to best performance -> { actionName: "SetPowerModeOnBattery", parameters: { mode: "bestPerformance" } } diff --git a/ts/packages/agents/desktop/src/windows/systemSchema.agr b/ts/packages/agents/desktop/src/windows/systemSchema.agr index 35261061b8..3a4e9dc61a 100644 --- a/ts/packages/agents/desktop/src/windows/systemSchema.agr +++ b/ts/packages/agents/desktop/src/windows/systemSchema.agr @@ -23,16 +23,16 @@ import { DesktopSystemActions } from "./systemActionsSchema.ts"; // ===== Network ===== = - enable metered connection -> { actionName: "enableMeteredConnections", parameters: { enable: true } } - | disable metered connection -> { actionName: "enableMeteredConnections", parameters: { enable: false } } - | turn on metered connection -> { actionName: "enableMeteredConnections", parameters: { enable: true } } - | turn off metered connection -> { actionName: "enableMeteredConnections", parameters: { enable: false } }; + enable metered connection -> { actionName: "EnableMeteredConnections", parameters: { enable: true } } + | disable metered connection -> { actionName: "EnableMeteredConnections", parameters: { enable: false } } + | turn on metered connection -> { actionName: "EnableMeteredConnections", parameters: { enable: true } } + | turn off metered connection -> { actionName: "EnableMeteredConnections", parameters: { enable: false } }; // ===== Gaming ===== = - enable game mode -> { actionName: "enableGameMode", parameters: {} } - | open game mode (settings)? -> { actionName: "enableGameMode", parameters: {} } - | turn on game mode -> { actionName: "enableGameMode", parameters: {} }; + enable game mode -> { actionName: "EnableGameMode", parameters: {} } + | open game mode (settings)? -> { actionName: "EnableGameMode", parameters: {} } + | turn on game mode -> { actionName: "EnableGameMode", parameters: {} }; // ===== Accessibility ===== = @@ -52,10 +52,10 @@ import { DesktopSystemActions } from "./systemActionsSchema.ts"; | turn off magnifier -> { actionName: "EnableMagnifier", parameters: { enable: false } }; = - enable sticky keys -> { actionName: "enableStickyKeys", parameters: { enable: true } } - | disable sticky keys -> { actionName: "enableStickyKeys", parameters: { enable: false } } - | turn on sticky keys -> { actionName: "enableStickyKeys", parameters: { enable: true } } - | turn off sticky keys -> { actionName: "enableStickyKeys", parameters: { enable: false } }; + enable sticky keys -> { actionName: "EnableStickyKeys", parameters: { enable: true } } + | disable sticky keys -> { actionName: "EnableStickyKeys", parameters: { enable: false } } + | turn on sticky keys -> { actionName: "EnableStickyKeys", parameters: { enable: true } } + | turn off sticky keys -> { actionName: "EnableStickyKeys", parameters: { enable: false } }; = enable filter keys -> { actionName: "EnableFilterKeysAction", parameters: { enable: true } } From 96a53983c9fc92a901080dafdb284108a2b476b7 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 1 Apr 2026 11:40:40 -0700 Subject: [PATCH 05/27] Add autoShell.Tests xUnit project with 52 tests Phase 2 unit test project with three test classes: - CommandDispatcherTests: quit detection, routing, error isolation, empty/unknown commands - AudioCommandHandlerTests: volume set/restore, mute on/off, invalid input handling - HandlerRegistrationTests: all 77 commands registered, no duplicates, PascalCase enforced Uses xUnit + Moq for mocking IAudioService and other internal interfaces. Added InternalsVisibleTo for test project and DynamicProxyGenAssembly2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AudioCommandHandlerTests.cs | 133 +++++++++++++++++ .../autoShell.Tests/CommandDispatcherTests.cs | 134 ++++++++++++++++++ .../HandlerRegistrationTests.cs | 89 ++++++++++++ dotnet/autoShell.Tests/autoShell.Tests.csproj | 27 ++++ dotnet/autoShell/autoShell.csproj | 4 + dotnet/autoShell/autoShell.sln | 26 ++++ 6 files changed, 413 insertions(+) create mode 100644 dotnet/autoShell.Tests/AudioCommandHandlerTests.cs create mode 100644 dotnet/autoShell.Tests/CommandDispatcherTests.cs create mode 100644 dotnet/autoShell.Tests/HandlerRegistrationTests.cs create mode 100644 dotnet/autoShell.Tests/autoShell.Tests.csproj diff --git a/dotnet/autoShell.Tests/AudioCommandHandlerTests.cs b/dotnet/autoShell.Tests/AudioCommandHandlerTests.cs new file mode 100644 index 0000000000..fae4b5f407 --- /dev/null +++ b/dotnet/autoShell.Tests/AudioCommandHandlerTests.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Handlers; +using autoShell.Services; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +public class AudioCommandHandlerTests +{ + private readonly Mock _audioMock = new(); + private readonly AudioCommandHandler _handler; + + public AudioCommandHandlerTests() + { + _handler = new AudioCommandHandler(_audioMock.Object); + } + + [Fact] + public void SupportedCommands_ContainsExpectedCommands() + { + var commands = _handler.SupportedCommands.ToList(); + Assert.Contains("Volume", commands); + Assert.Contains("Mute", commands); + Assert.Contains("RestoreVolume", commands); + Assert.Equal(3, commands.Count); + } + + // --- Volume --- + + [Theory] + [InlineData("0", 0)] + [InlineData("50", 50)] + [InlineData("100", 100)] + public void Volume_ValidPercent_SetsVolume(string input, int expected) + { + _audioMock.Setup(a => a.GetVolume()).Returns(75); + + Handle("Volume", input); + + _audioMock.Verify(a => a.SetVolume(expected), Times.Once); + } + + [Fact] + public void Volume_SavesCurrentVolumeBeforeSetting() + { + _audioMock.Setup(a => a.GetVolume()).Returns(42); + + Handle("Volume", "80"); + + // GetVolume should have been called to save the current level + _audioMock.Verify(a => a.GetVolume(), Times.Once); + } + + [Theory] + [InlineData("")] + [InlineData("abc")] + [InlineData("12.5")] + public void Volume_InvalidInput_DoesNotCallSetVolume(string input) + { + Handle("Volume", input); + + _audioMock.Verify(a => a.SetVolume(It.IsAny()), Times.Never); + } + + // --- RestoreVolume --- + + [Fact] + public void RestoreVolume_AfterVolumeChange_RestoresSavedLevel() + { + _audioMock.Setup(a => a.GetVolume()).Returns(65); + + // First set volume (saves 65) + Handle("Volume", "20"); + _audioMock.Invocations.Clear(); + + // Then restore + Handle("RestoreVolume", ""); + + _audioMock.Verify(a => a.SetVolume(65), Times.Once); + } + + [Fact] + public void RestoreVolume_WithoutPriorChange_RestoresZero() + { + Handle("RestoreVolume", ""); + + _audioMock.Verify(a => a.SetVolume(0), Times.Once); + } + + // --- Mute --- + + [Theory] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("false", false)] + [InlineData("False", false)] + public void Mute_ValidBool_SetsMute(string input, bool expected) + { + Handle("Mute", input); + + _audioMock.Verify(a => a.SetMute(expected), Times.Once); + } + + [Theory] + [InlineData("")] + [InlineData("yes")] + [InlineData("1")] + [InlineData("on")] + public void Mute_InvalidInput_DoesNotCallSetMute(string input) + { + Handle("Mute", input); + + _audioMock.Verify(a => a.SetMute(It.IsAny()), Times.Never); + } + + // --- Unknown key --- + + [Fact] + public void Handle_UnknownKey_DoesNothing() + { + Handle("UnknownAudioCmd", "value"); + + _audioMock.VerifyNoOtherCalls(); + } + + private void Handle(string key, string value) + { + _handler.Handle(key, value, JToken.FromObject(value)); + } +} diff --git a/dotnet/autoShell.Tests/CommandDispatcherTests.cs b/dotnet/autoShell.Tests/CommandDispatcherTests.cs new file mode 100644 index 0000000000..48276bfe7d --- /dev/null +++ b/dotnet/autoShell.Tests/CommandDispatcherTests.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Handlers; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +public class CommandDispatcherTests +{ + private readonly CommandDispatcher _dispatcher = new(); + + [Fact] + public void Dispatch_QuitKey_ReturnsTrue() + { + var json = JObject.Parse("""{"quit": true}"""); + bool result = _dispatcher.Dispatch(json); + Assert.True(result); + } + + [Fact] + public void Dispatch_NonQuitKey_ReturnsFalse() + { + _dispatcher.Register(new StubHandler("TestCmd")); + var json = JObject.Parse("""{"TestCmd": "value"}"""); + bool result = _dispatcher.Dispatch(json); + Assert.False(result); + } + + [Fact] + public void Dispatch_RoutesToCorrectHandler() + { + var handler = new StubHandler("Alpha", "Beta"); + _dispatcher.Register(handler); + + _dispatcher.Dispatch(JObject.Parse("""{"Alpha": "1"}""")); + Assert.Equal("Alpha", handler.LastKey); + Assert.Equal("1", handler.LastValue); + + _dispatcher.Dispatch(JObject.Parse("""{"Beta": "2"}""")); + Assert.Equal("Beta", handler.LastKey); + Assert.Equal("2", handler.LastValue); + } + + [Fact] + public void Dispatch_UnknownCommand_DoesNotThrow() + { + var json = JObject.Parse("""{"UnknownCmd": "value"}"""); + var ex = Record.Exception(() => _dispatcher.Dispatch(json)); + Assert.Null(ex); + } + + [Fact] + public void Dispatch_EmptyObject_ReturnsFalse() + { + bool result = _dispatcher.Dispatch(new JObject()); + Assert.False(result); + } + + [Fact] + public void Dispatch_QuitStopsProcessingSubsequentKeys() + { + var handler = new StubHandler("After"); + _dispatcher.Register(handler); + + // quit comes first — handler for "After" should not be called + var json = JObject.Parse("""{"quit": true, "After": "value"}"""); + bool result = _dispatcher.Dispatch(json); + + Assert.True(result); + Assert.Null(handler.LastKey); + } + + [Fact] + public void Dispatch_HandlerException_DoesNotBubbleUp() + { + var handler = new ThrowingHandler("Boom"); + _dispatcher.Register(handler); + + var json = JObject.Parse("""{"Boom": "value"}"""); + var ex = Record.Exception(() => _dispatcher.Dispatch(json)); + Assert.Null(ex); + } + + [Fact] + public void Dispatch_HandlerException_ContinuesToNextKey() + { + var throwing = new ThrowingHandler("Boom"); + var normal = new StubHandler("Ok"); + _dispatcher.Register(throwing, normal); + + _dispatcher.Dispatch(JObject.Parse("""{"Boom": "x", "Ok": "y"}""")); + Assert.Equal("Ok", normal.LastKey); + } + + /// + /// Stub handler that records the last key/value it received. + /// + private class StubHandler : ICommandHandler + { + public IEnumerable SupportedCommands { get; } + public string? LastKey { get; private set; } + public string? LastValue { get; private set; } + + public StubHandler(params string[] commands) + { + SupportedCommands = commands; + } + + public void Handle(string key, string value, JToken rawValue) + { + LastKey = key; + LastValue = value; + } + } + + /// + /// Handler that always throws, for testing exception isolation. + /// + private class ThrowingHandler : ICommandHandler + { + public IEnumerable SupportedCommands { get; } + + public ThrowingHandler(params string[] commands) + { + SupportedCommands = commands; + } + + public void Handle(string key, string value, JToken rawValue) + { + throw new InvalidOperationException("Test exception"); + } + } +} diff --git a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs new file mode 100644 index 0000000000..2ef957066a --- /dev/null +++ b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Handlers; + +namespace autoShell.Tests; + +/// +/// Verifies that each handler declares the expected supported commands. +/// Catches accidental renames or deletions. +/// +public class HandlerRegistrationTests +{ + private readonly List _handlers; + + public HandlerRegistrationTests() + { + var audioMock = new Moq.Mock(); + var registryMock = new Moq.Mock(); + var systemParamsMock = new Moq.Mock(); + var processMock = new Moq.Mock(); + + _handlers = + [ + new AudioCommandHandler(audioMock.Object), + new AppCommandHandler(), + new WindowCommandHandler(), + new ThemeCommandHandler(), + new VirtualDesktopCommandHandler(), + new NetworkCommandHandler(), + new DisplayCommandHandler(), + new SettingsCommandHandler(registryMock.Object, systemParamsMock.Object, processMock.Object), + new SystemCommandHandler(), + ]; + } + + [Fact] + public void AllHandlers_HaveNonEmptySupportedCommands() + { + foreach (var handler in _handlers) + { + Assert.True( + handler.SupportedCommands.Any(), + $"{handler.GetType().Name} has no supported commands"); + } + } + + [Fact] + public void AllHandlers_HaveNoDuplicateCommandsWithinHandler() + { + foreach (var handler in _handlers) + { + var commands = handler.SupportedCommands.ToList(); + var duplicates = commands.GroupBy(c => c).Where(g => g.Count() > 1).Select(g => g.Key).ToList(); + + Assert.True( + duplicates.Count == 0, + $"{handler.GetType().Name} has duplicate commands: {string.Join(", ", duplicates)}"); + } + } + + [Fact] + public void AllHandlers_HaveNoDuplicateCommandsAcrossHandlers() + { + var seen = new Dictionary(); + var duplicates = new List(); + + foreach (var handler in _handlers) + { + string handlerName = handler.GetType().Name; + foreach (string cmd in handler.SupportedCommands) + { + if (seen.TryGetValue(cmd, out string? existingHandler)) + { + duplicates.Add($"'{cmd}' in both {existingHandler} and {handlerName}"); + } + else + { + seen[cmd] = handlerName; + } + } + } + + Assert.True( + duplicates.Count == 0, + $"Duplicate commands across handlers: {string.Join("; ", duplicates)}"); + } + +} diff --git a/dotnet/autoShell.Tests/autoShell.Tests.csproj b/dotnet/autoShell.Tests/autoShell.Tests.csproj new file mode 100644 index 0000000000..6f2ae595dd --- /dev/null +++ b/dotnet/autoShell.Tests/autoShell.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0-windows + enable + enable + false + true + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/autoShell/autoShell.csproj b/dotnet/autoShell/autoShell.csproj index 0a9e9d1cb9..514231c6ef 100644 --- a/dotnet/autoShell/autoShell.csproj +++ b/dotnet/autoShell/autoShell.csproj @@ -9,6 +9,10 @@ bin\$(Configuration) false + + + + diff --git a/dotnet/autoShell/autoShell.sln b/dotnet/autoShell/autoShell.sln index 6ac946ee8c..aaea0bfece 100644 --- a/dotnet/autoShell/autoShell.sln +++ b/dotnet/autoShell/autoShell.sln @@ -5,16 +5,42 @@ VisualStudioVersion = 17.8.34408.163 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "autoShell", "autoShell.csproj", "{7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "autoShell.Tests", "..\autoShell.Tests\autoShell.Tests.csproj", "{EC831702-9307-4808-9615-FE20D78C14C6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Debug|x64.ActiveCfg = Debug|Any CPU + {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Debug|x64.Build.0 = Debug|Any CPU + {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Debug|x86.Build.0 = Debug|Any CPU {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Release|Any CPU.Build.0 = Release|Any CPU + {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Release|x64.ActiveCfg = Release|Any CPU + {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Release|x64.Build.0 = Release|Any CPU + {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Release|x86.ActiveCfg = Release|Any CPU + {7D095B6C-7EC1-4127-B1EA-8D7DC91A1C1C}.Release|x86.Build.0 = Release|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Debug|x64.ActiveCfg = Debug|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Debug|x64.Build.0 = Debug|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Debug|x86.ActiveCfg = Debug|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Debug|x86.Build.0 = Debug|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Release|Any CPU.Build.0 = Release|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Release|x64.ActiveCfg = Release|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Release|x64.Build.0 = Release|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Release|x86.ActiveCfg = Release|Any CPU + {EC831702-9307-4808-9615-FE20D78C14C6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From b450c9db5c5956b83031bc8ae4905046b6aea532 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 1 Apr 2026 13:11:14 -0700 Subject: [PATCH 06/27] Add autoShell test step to CI workflow Runs dotnet test for autoShell.Tests after the MSBuild build step, in both Debug and Release configurations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-dotnet.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-dotnet.yml b/.github/workflows/build-dotnet.yml index e9299f5650..b0d9b6c2fb 100644 --- a/.github/workflows/build-dotnet.yml +++ b/.github/workflows/build-dotnet.yml @@ -57,6 +57,10 @@ jobs: working-directory: dotnet run: | msbuild.exe autoShell/AutoShell.sln /p:platform="Any CPU" /p:configuration="${{ matrix.configuration }}" + - name: Test AutoShell + if: ${{ github.event_name != 'pull_request' || steps.filter.outputs.dotnet != 'false' }} + working-directory: dotnet/autoShell.Tests + run: dotnet test --configuration ${{ matrix.configuration }} - name: Restore Packages (TypeAgent) if: ${{ github.event_name != 'pull_request' || steps.filter.outputs.dotnet != 'false' }} working-directory: dotnet From b0f390216e7b2a1224f36678eec4641663d1fa01 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 1 Apr 2026 15:23:27 -0700 Subject: [PATCH 07/27] bugfix --- ts/packages/agents/desktop/src/connector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/packages/agents/desktop/src/connector.ts b/ts/packages/agents/desktop/src/connector.ts index 72e00df3e5..335d9e77b9 100644 --- a/ts/packages/agents/desktop/src/connector.ts +++ b/ts/packages/agents/desktop/src/connector.ts @@ -5,7 +5,7 @@ import child_process from "node:child_process"; import { fileURLToPath } from "node:url"; import { ProgramNameIndex, loadProgramNameIndex } from "./programNameIndex.js"; import { Storage } from "@typeagent/agent-sdk"; -import registerDebug from "Debug"; +import registerDebug from "debug"; import { AllDesktopActions } from "./allActionsSchema.js"; import fs from "node:fs"; import path from "node:path"; From cfba7831c9a9e8f7e40264bf893e407ebb6a1796 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 1 Apr 2026 15:47:17 -0700 Subject: [PATCH 08/27] wip --- .../HandlerRegistrationTests.cs | 14 +- dotnet/autoShell/AutoShell.cs | 247 +-- dotnet/autoShell/AutoShell_Settings.cs | 1615 ----------------- dotnet/autoShell/AutoShell_Themes.cs | 380 ---- dotnet/autoShell/AutoShell_Win32.cs | 69 +- .../autoShell/Handlers/AppCommandHandler.cs | 18 +- .../Handlers/DisplayCommandHandler.cs | 19 +- .../Handlers/NetworkCommandHandler.cs | 35 +- .../Handlers/SettingsCommandHandler.cs | 101 -- .../AccessibilitySettingsHandler.cs | 118 ++ .../DisplaySettingsHandler.cs | 174 ++ .../FileExplorerSettingsHandler.cs | 76 + .../SettingsHandlers/MouseSettingsHandler.cs | 119 ++ .../PersonalizationSettingsHandler.cs | 96 + .../SettingsHandlers/PowerSettingsHandler.cs | 68 + .../PrivacySettingsHandler.cs | 71 + .../SettingsHandlers/SystemSettingsHandler.cs | 84 + .../TaskbarSettingsHandler.cs | 129 ++ .../Handlers/SystemCommandHandler.cs | 22 +- .../autoShell/Handlers/ThemeCommandHandler.cs | 413 ++++- .../Handlers/VirtualDesktopCommandHandler.cs | 28 +- .../Handlers/WindowCommandHandler.cs | 23 +- .../Services/ISystemParametersService.cs | 9 + .../WindowsSystemParametersService.cs | 6 + 24 files changed, 1536 insertions(+), 2398 deletions(-) delete mode 100644 dotnet/autoShell/AutoShell_Settings.cs delete mode 100644 dotnet/autoShell/AutoShell_Themes.cs delete mode 100644 dotnet/autoShell/Handlers/SettingsCommandHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/AccessibilitySettingsHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/DisplaySettingsHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/FileExplorerSettingsHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/MouseSettingsHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/PersonalizationSettingsHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/PowerSettingsHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/PrivacySettingsHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/SystemSettingsHandler.cs create mode 100644 dotnet/autoShell/Handlers/SettingsHandlers/TaskbarSettingsHandler.cs diff --git a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs index 2ef957066a..3d42217ccd 100644 --- a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs +++ b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs @@ -25,12 +25,20 @@ public HandlerRegistrationTests() new AudioCommandHandler(audioMock.Object), new AppCommandHandler(), new WindowCommandHandler(), - new ThemeCommandHandler(), + new ThemeCommandHandler(registryMock.Object, processMock.Object, systemParamsMock.Object), new VirtualDesktopCommandHandler(), new NetworkCommandHandler(), new DisplayCommandHandler(), - new SettingsCommandHandler(registryMock.Object, systemParamsMock.Object, processMock.Object), - new SystemCommandHandler(), + new TaskbarSettingsHandler(registryMock.Object), + new DisplaySettingsHandler(registryMock.Object, processMock.Object), + new PersonalizationSettingsHandler(registryMock.Object, processMock.Object), + new MouseSettingsHandler(systemParamsMock.Object, processMock.Object), + new AccessibilitySettingsHandler(registryMock.Object, processMock.Object), + new PrivacySettingsHandler(registryMock.Object), + new PowerSettingsHandler(registryMock.Object, processMock.Object), + new FileExplorerSettingsHandler(registryMock.Object), + new SystemSettingsHandler(processMock.Object), + new SystemCommandHandler(processMock.Object), ]; } diff --git a/dotnet/autoShell/AutoShell.cs b/dotnet/autoShell/AutoShell.cs index 193c110364..18b6f32e88 100644 --- a/dotnet/autoShell/AutoShell.cs +++ b/dotnet/autoShell/AutoShell.cs @@ -86,9 +86,6 @@ static AutoShell() s_friendlyNameToId.Add(kvp.Key, kvp.Value); } - // Load the installed themes - LoadThemes(); - // Desktop management s_shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ImmersiveShell)); s_virtualDesktopManagerInternal = (IVirtualDesktopManagerInternal)s_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); @@ -108,12 +105,20 @@ static AutoShell() new AudioCommandHandler(audio), new AppCommandHandler(), new WindowCommandHandler(), - new ThemeCommandHandler(), + new ThemeCommandHandler(registry, process, systemParams), new VirtualDesktopCommandHandler(), new NetworkCommandHandler(), new DisplayCommandHandler(), - new SettingsCommandHandler(registry, systemParams, process), - new SystemCommandHandler() + new TaskbarSettingsHandler(registry), + new DisplaySettingsHandler(registry, process), + new PersonalizationSettingsHandler(registry, process), + new MouseSettingsHandler(systemParams, process), + new AccessibilitySettingsHandler(registry, process), + new PrivacySettingsHandler(registry), + new PowerSettingsHandler(registry, process), + new FileExplorerSettingsHandler(registry), + new SystemSettingsHandler(process), + new SystemCommandHandler(process) ); } @@ -208,7 +213,7 @@ internal static void LogWarning(string message) Console.ForegroundColor = previousColor; } - private static SortedList GetAllInstalledAppsIds() + internal static SortedList GetAllInstalledAppsIds() { // GUID taken from https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid var FOLDERID_AppsFolder = new Guid("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}"); @@ -256,7 +261,7 @@ private static IntPtr FindProcessWindowHandle(string processName) } // given part of a process name, raise the window of that process to the top level - private static void RaiseWindow(string processName) + internal static void RaiseWindow(string processName) { processName = ResolveProcessNameFromFriendlyName(processName); Process[] processes = Process.GetProcessesByName(processName); @@ -290,7 +295,7 @@ private static void RaiseWindow(string processName) } } - private static void MaximizeWindow(string processName) + internal static void MaximizeWindow(string processName) { processName = ResolveProcessNameFromFriendlyName(processName); Process[] processes = Process.GetProcessesByName(processName); @@ -320,7 +325,7 @@ private static void MaximizeWindow(string processName) } } - private static void MinimizeWindow(string processName) + internal static void MinimizeWindow(string processName) { processName = ResolveProcessNameFromFriendlyName(processName); Process[] processes = Process.GetProcessesByName(processName); @@ -348,7 +353,7 @@ private static void MinimizeWindow(string processName) } } - private static void TileWindowPair(string processName1, string processName2) + internal static void TileWindowPair(string processName1, string processName2) { // find both processes // TODO: Update this to account for UWP apps (e.g. calculator). UWPs are hosted by ApplicationFrameHost.exe @@ -487,7 +492,7 @@ private static (IntPtr hWnd, int pid) FindWindowByTitle(string titleSearch) } // given a friendly name, check if it's running and if not, start it; if it's running raise it to the top level - private static void OpenApplication(string friendlyName) + internal static void OpenApplication(string friendlyName) { // check to see if the application is running Process[] processes = Process.GetProcessesByName(friendlyName); @@ -550,7 +555,7 @@ private static void OpenApplication(string friendlyName) } // close application - private static void CloseApplication(string friendlyName) + internal static void CloseApplication(string friendlyName) { // check to see if the application is running string processName = ResolveProcessNameFromFriendlyName(friendlyName); @@ -569,16 +574,11 @@ private static void CloseApplication(string friendlyName) } } - private static void SetDesktopWallpaper(string imagePath) - { - SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, imagePath, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); - } - /// /// Creates virtual desktops from a JSON array of desktop names. /// /// JSON array containing desktop names, e.g., ["Work", "Personal", "Gaming"] - private static void CreateDesktop(string jsonValue) + internal static void CreateDesktop(string jsonValue) { try { @@ -639,7 +639,7 @@ private static void CreateDesktop(string jsonValue) } } - private static void SwitchDesktop(string desktopIdentifier) + internal static void SwitchDesktop(string desktopIdentifier) { if (!int.TryParse(desktopIdentifier, out int index)) { @@ -679,7 +679,7 @@ private static void SwitchDesktop(int index) Marshal.ReleaseComObject(desktops); } - private static void BumpDesktopIndex(int bump) + internal static void BumpDesktopIndex(int bump) { IVirtualDesktop desktop = s_virtualDesktopManagerInternal.GetCurrentDesktop(); int index = GetDesktopIndex(desktop); @@ -753,7 +753,7 @@ private static int GetDesktopIndex(IVirtualDesktop desktop) /// /// /// Currently not working correction, returns ACCESS_DENIED // TODO: investigate - private static void MoveWindowToDesktop(JToken value) + internal static void MoveWindowToDesktop(JToken value) { string process = value.SelectToken("process").ToString(); string desktop = value.SelectToken("desktop").ToString(); @@ -795,7 +795,7 @@ private static void MoveWindowToDesktop(JToken value) } } - private static void PinWindow(string processName) + internal static void PinWindow(string processName) { IntPtr hWnd = FindProcessWindowHandle(processName); @@ -841,7 +841,7 @@ private static bool execLine(JObject root) /// Sets the airplane mode state using the Radio Management API. /// /// True to enable airplane mode, false to disable. - private static void SetAirplaneMode(bool enable) + internal static void SetAirplaneMode(bool enable) { IRadioManager radioManager = null; try @@ -907,7 +907,7 @@ private static void SetAirplaneMode(bool enable) /// /// Lists all WiFi networks currently in range. /// - private static void ListWifiNetworks() + internal static void ListWifiNetworks() { IntPtr clientHandle = IntPtr.Zero; IntPtr wlanInterfaceList = IntPtr.Zero; @@ -1029,7 +1029,7 @@ private static void ListWifiNetworks() /// /// The SSID of the network to connect to. /// Optional password for secured networks. - private static void ConnectToWifi(string ssid, string password = null) + internal static void ConnectToWifi(string ssid, string password = null) { IntPtr clientHandle = IntPtr.Zero; IntPtr wlanInterfaceList = IntPtr.Zero; @@ -1158,7 +1158,7 @@ private static string GenerateWifiProfileXml(string ssid, string password) /// Sets the system text scaling factor (percentage). /// /// The text scaling percentage (100-225). - private static void SetTextSize(int percentage) + internal static void SetTextSize(int percentage) { try { @@ -1196,7 +1196,7 @@ private static void SetTextSize(int percentage) /// /// Lists all available display resolutions for the primary monitor. /// - private static void ListDisplayResolutions() + internal static void ListDisplayResolutions() { try { @@ -1238,7 +1238,7 @@ private static void ListDisplayResolutions() /// Sets the display resolution. /// /// JSON object with "width" and "height" properties, or a string like "1920x1080". - private static void SetDisplayResolution(JToken value) + internal static void SetDisplayResolution(JToken value) { try { @@ -1361,7 +1361,7 @@ private static void SetDisplayResolution(JToken value) } } - private static void DisconnectFromWifi() + internal static void DisconnectFromWifi() { IntPtr clientHandle = IntPtr.Zero; IntPtr wlanInterfaceList = IntPtr.Zero; @@ -1426,191 +1426,4 @@ private static void DisconnectFromWifi() } } } - - #region Bridge methods for command handlers - - /// - /// Bridge method for AppCommandHandler. - /// - internal static void HandleAppCommand(string key, string value) - { - switch (key) - { - case "LaunchProgram": - OpenApplication(value); - break; - case "CloseProgram": - CloseApplication(value); - break; - case "ListAppNames": - var installedApps = GetAllInstalledAppsIds(); - Console.WriteLine(JsonConvert.SerializeObject(installedApps.Keys)); - break; - } - } - - /// - /// Bridge method for WindowCommandHandler. - /// - internal static void HandleWindowCommand(string key, string value) - { - switch (key) - { - case "Maximize": - MaximizeWindow(value); - break; - case "Minimize": - MinimizeWindow(value); - break; - case "SwitchTo": - RaiseWindow(value); - break; - case "Tile": - string[] apps = value.Split(','); - if (apps.Length == 2) - { - TileWindowPair(apps[0], apps[1]); - } - break; - } - } - - /// - /// Bridge method for ThemeCommandHandler. - /// - internal static void HandleThemeCommand(string key, string value) - { - switch (key) - { - case "SetWallpaper": - SetDesktopWallpaper(value); - break; - case "ApplyTheme": - ApplyTheme(value); - break; - case "ListThemes": - var themes = GetInstalledThemes(); - Console.WriteLine(JsonConvert.SerializeObject(themes)); - break; - case "SetThemeMode": - if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase)) - { - ToggleLightDarkMode(); - } - else - { - if (bool.TryParse(value, out bool useLightMode)) - { - SetLightDarkMode(useLightMode); - } - else if (value.Equals("light", StringComparison.OrdinalIgnoreCase)) - { - SetLightDarkMode(true); - } - else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase)) - { - SetLightDarkMode(false); - } - } - break; - } - } - - /// - /// Bridge method for VirtualDesktopCommandHandler. - /// - internal static void HandleVirtualDesktopCommand(string key, string value, JToken rawValue) - { - switch (key) - { - case "CreateDesktop": - CreateDesktop(value); - break; - case "SwitchDesktop": - SwitchDesktop(value); - break; - case "NextDesktop": - BumpDesktopIndex(1); - break; - case "PreviousDesktop": - BumpDesktopIndex(-1); - break; - case "MoveWindowToDesktop": - MoveWindowToDesktop(rawValue); - break; - case "PinWindow": - PinWindow(value); - break; - } - } - - /// - /// Bridge method for NetworkCommandHandler. - /// - internal static void HandleNetworkCommand(string key, string value) - { - switch (key) - { - case "ToggleAirplaneMode": - SetAirplaneMode(bool.Parse(value)); - break; - case "ListWifiNetworks": - ListWifiNetworks(); - break; - case "ConnectWifi": - JObject netInfo = JObject.Parse(value); - string ssid = netInfo.Value("ssid"); - string password = netInfo["password"] is not null ? netInfo.Value("password") : ""; - ConnectToWifi(ssid, password); - break; - case "DisconnectWifi": - DisconnectFromWifi(); - break; - case "BluetoothToggle": - case "EnableWifi": - case "EnableMeteredConnections": - HandleSettingsCommand(key, value); - break; - } - } - - /// - /// Bridge method for DisplayCommandHandler. - /// - internal static void HandleDisplayCommand(string key, string value, JToken rawValue) - { - switch (key) - { - case "SetTextSize": - if (int.TryParse(value, out int textSizePct)) - { - SetTextSize(textSizePct); - } - break; - case "SetScreenResolution": - SetDisplayResolution(rawValue); - break; - case "ListResolutions": - ListDisplayResolutions(); - break; - } - } - - /// - /// Bridge method for SystemCommandHandler. - /// - internal static void HandleSystemCommand(string key, string value) - { - switch (key) - { - case "ToggleNotifications": - ShellExecute(IntPtr.Zero, "open", "ms-actioncenter:", null, null, 1); - break; - case "Debug": - Debugger.Launch(); - break; - } - } - - #endregion } diff --git a/dotnet/autoShell/AutoShell_Settings.cs b/dotnet/autoShell/AutoShell_Settings.cs deleted file mode 100644 index 83431edeb7..0000000000 --- a/dotnet/autoShell/AutoShell_Settings.cs +++ /dev/null @@ -1,1615 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using Microsoft.Win32; -using Newtonsoft.Json.Linq; - -namespace autoShell; - -/// -/// Partial class containing Windows Settings automation handlers -/// Implements 50+ common Windows settings actions for the TypeAgent desktop agent -/// -internal partial class AutoShell -{ - #region Network Settings - - /// - /// Toggles Bluetooth radio on or off - /// Command: {"BluetoothToggle": "{\"enableBluetooth\":true}"} - /// - private static void HandleBluetoothToggle(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enableBluetooth") ?? true; - - // Use the same radio management API as airplane mode - IRadioManager radioManager = null; - try - { - Type radioManagerType = Type.GetTypeFromCLSID(CLSID_RadioManagementAPI); - if (radioManagerType == null) - { - Debug.WriteLine("Failed to get Radio Management API type"); - return; - } - - radioManager = (IRadioManager)Activator.CreateInstance(radioManagerType); - if (radioManager == null) - { - Debug.WriteLine("Failed to create Radio Manager instance"); - return; - } - - // Note: This controls all radios. For Bluetooth-specific control, - // we'd need IRadioInstanceCollection, but registry is more reliable - SetRegistryValue(@"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters\Radio Support", - "SupportDLL", enable ? 1 : 0); - - Debug.WriteLine($"Bluetooth set to: {(enable ? "on" : "off")}"); - } - finally - { - if (radioManager != null) - Marshal.ReleaseComObject(radioManager); - } - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables WiFi - /// Command: {"EnableWifi": "{\"enable\":true}"} - /// - private static void HandleEnableWifi(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable"); - - // Use netsh to enable/disable WiFi - string command = enable ? "interface set interface \"Wi-Fi\" enabled" : - "interface set interface \"Wi-Fi\" disabled"; - - var psi = new ProcessStartInfo - { - FileName = "netsh", - Arguments = command, - CreateNoWindow = true, - UseShellExecute = false - }; - - Process.Start(psi)?.WaitForExit(); - Debug.WriteLine($"WiFi set to: {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables metered connection - /// Command: {"EnableMeteredConnections": "{\"enable\":true}"} - /// - private static void HandleEnableMeteredConnections(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable"); - - // Open network settings page - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:network-status", - UseShellExecute = true - }); - - Debug.WriteLine($"Metered connection setting - please configure manually"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Display Settings - - /// - /// Adjusts screen brightness (increase or decrease) - /// Command: {"AdjustScreenBrightness": "{\"brightnessLevel\":\"increase\"}"} - /// - private static void HandleAdjustScreenBrightness(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string level = param.Value("brightnessLevel"); - bool increase = level == "increase"; - - // Get current brightness - byte currentBrightness = GetCurrentBrightness(); - byte newBrightness = increase ? - (byte)Math.Min(100, currentBrightness + 10) : - (byte)Math.Max(0, currentBrightness - 10); - - SetBrightness(newBrightness); - Debug.WriteLine($"Brightness adjusted to: {newBrightness}%"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or configures Night Light (blue light filter) schedule - /// Command: {"EnableBlueLightFilterSchedule": "{\"schedule\":\"sunset to sunrise\",\"nightLightScheduleDisabled\":false}"} - /// - private static void HandleEnableBlueLightFilterSchedule(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool disabled = param.Value("nightLightScheduleDisabled"); - - // Night Light registry path - string regPath = @"Software\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount\Current\default$windows.data.bluelightreduction.settings\windows.data.bluelightreduction.settings"; - using (var key = Registry.CurrentUser.CreateSubKey(regPath)) - { - if (key != null) - { - // Enable/disable Night Light - key.SetValue("Data", disabled ? new byte[] { 0x02, 0x00, 0x00, 0x00 } : new byte[] { 0x02, 0x00, 0x00, 0x01 }); - } - } - - Debug.WriteLine($"Night Light schedule {(disabled ? "disabled" : "enabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Adjusts the color temperature for Night Light - /// Command: {"AdjustColorTemperature": "{\"filterEffect\":\"reduce\"}"} - /// - private static void HandleAdjustColorTemperature(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string effect = param.Value("filterEffect"); - - // Open display settings to Night Light page - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:nightlight", - UseShellExecute = true - }); - - Debug.WriteLine($"Night Light settings opened - adjust color temperature manually"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Sets display scaling percentage - /// Command: {"DisplayScaling": "{\"sizeOverride\":\"125\"}"} - /// - private static void HandleDisplayScaling(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string sizeStr = param.Value("sizeOverride"); - - if (int.TryParse(sizeStr, out int percentage)) - { - // Valid scaling values: 100, 125, 150, 175, 200 - percentage = percentage switch - { - < 113 => 100, - < 138 => 125, - < 163 => 150, - < 188 => 175, - _ => 200 - }; - - // Set DPI scaling - SetDpiScaling(percentage); - Debug.WriteLine($"Display scaling set to: {percentage}%"); - } - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Adjusts screen orientation - /// Command: {"AdjustScreenOrientation": "{\"orientation\":\"landscape\"}"} - /// - private static void HandleAdjustScreenOrientation(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string orientation = param.Value("orientation"); - - // Open display settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:display", - UseShellExecute = true - }); - - Debug.WriteLine($"Display settings opened for orientation change to: {orientation}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Adjusts display resolution - /// Command: {"DisplayResolutionAndAspectRatio": "{\"resolutionChange\":\"increase\"}"} - /// - private static void HandleDisplayResolutionAndAspectRatio(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string change = param.Value("resolutionChange"); - - // Open display settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:display", - UseShellExecute = true - }); - - Debug.WriteLine($"Display settings opened for resolution adjustment"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Locks or unlocks screen rotation - /// Command: {"RotationLock": "{\"enable\":true}"} - /// - private static void HandleRotationLock(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - // Registry key for rotation lock - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell")) - { - if (key != null) - { - key.SetValue("RotationLockPreference", enable ? 1 : 0, RegistryValueKind.DWord); - } - } - - Debug.WriteLine($"Rotation lock {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Personalization Settings - - /// - /// Sets system theme mode (dark or light) - /// Command: {"SystemThemeMode": "{\"mode\":\"dark\"}"} - /// - private static void HandleSystemThemeMode(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string mode = param.Value("mode"); - bool useLightMode = mode.Equals("light", StringComparison.OrdinalIgnoreCase); - - SetLightDarkMode(useLightMode); - Debug.WriteLine($"System theme set to: {mode}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables transparency effects - /// Command: {"EnableTransparency": "{\"enable\":true}"} - /// - private static void HandleEnableTransparency(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable"); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")) - { - if (key != null) - { - key.SetValue("EnableTransparency", enable ? 1 : 0, RegistryValueKind.DWord); - } - } - - Debug.WriteLine($"Transparency {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Applies accent color to title bars - /// Command: {"ApplyColorToTitleBar": "{\"enableColor\":true}"} - /// - private static void HandleApplyColorToTitleBar(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enableColor"); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\DWM")) - { - if (key != null) - { - key.SetValue("ColorPrevalence", enable ? 1 : 0, RegistryValueKind.DWord); - } - } - - Debug.WriteLine($"Title bar color {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables high contrast theme - /// Command: {"HighContrastTheme": "{}"} - /// - private static void HandleHighContrastTheme(string jsonParams) - { - try - { - // Open high contrast settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:easeofaccess-highcontrast", - UseShellExecute = true - }); - - Debug.WriteLine("High contrast settings opened"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Taskbar Settings - - /// - /// Auto-hides the taskbar - /// Command: {"AutoHideTaskbar": "{\"hideWhenNotUsing\":true,\"alwaysShow\":false}"} - /// - private static void HandleAutoHideTaskbar(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool hide = param.Value("hideWhenNotUsing"); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\StuckRects3")) - { - if (key != null) - { - byte[] settings = (byte[])key.GetValue("Settings"); - if (settings != null && settings.Length >= 9) - { - // Bit 0 of byte 8 controls auto-hide - if (hide) - settings[8] |= 0x01; - else - settings[8] &= 0xFE; - - key.SetValue("Settings", settings, RegistryValueKind.Binary); - - // Refresh taskbar - RefreshTaskbar(); - } - } - } - - Debug.WriteLine($"Taskbar auto-hide {(hide ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Sets taskbar alignment (left or center) - /// Command: {"TaskbarAlignment": "{\"alignment\":\"center\"}"} - /// - private static void HandleTaskbarAlignment(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string alignment = param.Value("alignment"); - bool useCenter = alignment.Equals("center", StringComparison.OrdinalIgnoreCase); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")) - { - if (key != null) - { - // 0 = left, 1 = center - key.SetValue("TaskbarAl", useCenter ? 1 : 0, RegistryValueKind.DWord); - RefreshTaskbar(); - } - } - - Debug.WriteLine($"Taskbar alignment set to: {alignment}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Shows or hides the Task View button - /// Command: {"TaskViewVisibility": "{\"visibility\":true}"} - /// - private static void HandleTaskViewVisibility(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool visible = param.Value("visibility"); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")) - { - if (key != null) - { - key.SetValue("ShowTaskViewButton", visible ? 1 : 0, RegistryValueKind.DWord); - RefreshTaskbar(); - } - } - - Debug.WriteLine($"Task View button {(visible ? "shown" : "hidden")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Shows or hides the Widgets button - /// Command: {"ToggleWidgetsButtonVisibility": "{\"visibility\":\"show\"}"} - /// - private static void HandleToggleWidgetsButtonVisibility(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string visibility = param.Value("visibility"); - bool show = visibility.Equals("show", StringComparison.OrdinalIgnoreCase); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")) - { - if (key != null) - { - key.SetValue("TaskbarDa", show ? 1 : 0, RegistryValueKind.DWord); - RefreshTaskbar(); - } - } - - Debug.WriteLine($"Widgets button {visibility}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Shows or hides badges on taskbar icons - /// Command: {"ShowBadgesOnTaskbar": "{\"enableBadging\":true}"} - /// - private static void HandleShowBadgesOnTaskbar(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enableBadging") ?? true; - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")) - { - if (key != null) - { - key.SetValue("TaskbarBadges", enable ? 1 : 0, RegistryValueKind.DWord); - RefreshTaskbar(); - } - } - - Debug.WriteLine($"Taskbar badges {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Shows taskbar on all monitors - /// Command: {"DisplayTaskbarOnAllMonitors": "{\"enable\":true}"} - /// - private static void HandleDisplayTaskbarOnAllMonitors(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")) - { - if (key != null) - { - key.SetValue("MMTaskbarEnabled", enable ? 1 : 0, RegistryValueKind.DWord); - RefreshTaskbar(); - } - } - - Debug.WriteLine($"Taskbar on all monitors {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Shows seconds in the system tray clock - /// Command: {"DisplaySecondsInSystrayClock": "{\"enable\":true}"} - /// - private static void HandleDisplaySecondsInSystrayClock(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")) - { - if (key != null) - { - key.SetValue("ShowSecondsInSystemClock", enable ? 1 : 0, RegistryValueKind.DWord); - RefreshTaskbar(); - } - } - - Debug.WriteLine($"Seconds in clock {(enable ? "shown" : "hidden")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Mouse Settings - - /// - /// Adjusts mouse cursor speed - /// Command: {"MouseCursorSpeed": "{\"speedLevel\":10}"} - /// - private static void HandleMouseCursorSpeed(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - int speed = param.Value("speedLevel"); - - // Speed range: 1-20 (default 10) - speed = Math.Max(1, Math.Min(20, speed)); - - SystemParametersInfo(SPI_SETMOUSESPEED, 0, (IntPtr)speed, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); - Debug.WriteLine($"Mouse speed set to: {speed}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Sets the number of lines to scroll per mouse wheel notch - /// Command: {"MouseWheelScrollLines": "{\"scrollLines\":3}"} - /// - private static void HandleMouseWheelScrollLines(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - int lines = param.Value("scrollLines"); - - lines = Math.Max(1, Math.Min(100, lines)); - - SystemParametersInfo(SPI_SETWHEELSCROLLLINES, lines, IntPtr.Zero, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); - Debug.WriteLine($"Mouse wheel scroll lines set to: {lines}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Sets the primary mouse button - /// Command: {"SetPrimaryMouseButton": "{\"primaryButton\":\"left\"}"} - /// - private static void HandleSetPrimaryMouseButton(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string button = param.Value("primaryButton"); - bool leftPrimary = button.Equals("left", StringComparison.OrdinalIgnoreCase); - - SwapMouseButton(leftPrimary ? 0 : 1); - Debug.WriteLine($"Primary mouse button set to: {button}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables enhanced pointer precision (mouse acceleration) - /// Command: {"EnhancePointerPrecision": "{\"enable\":true}"} - /// - private static void HandleEnhancePointerPrecision(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - int[] mouseParams = new int[3]; - SystemParametersInfo(SPI_GETMOUSE, 0, mouseParams, 0); - - // Set acceleration (third parameter) - mouseParams[2] = enable ? 1 : 0; - - SystemParametersInfo(SPI_SETMOUSE, 0, mouseParams, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); - Debug.WriteLine($"Enhanced pointer precision {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Adjusts mouse pointer size - /// Command: {"AdjustMousePointerSize": "{\"sizeAdjustment\":\"increase\"}"} - /// - private static void HandleAdjustMousePointerSize(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string adjustment = param.Value("sizeAdjustment"); - - // Open mouse pointer settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:easeofaccess-mouse", - UseShellExecute = true - }); - - Debug.WriteLine($"Mouse pointer settings opened for size adjustment"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Customizes mouse pointer color - /// Command: {"MousePointerCustomization": "{\"color\":\"#FF0000\"}"} - /// - private static void HandleMousePointerCustomization(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string color = param.Value("color"); - - // Open mouse pointer settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:easeofaccess-mouse", - UseShellExecute = true - }); - - Debug.WriteLine($"Mouse pointer settings opened for color customization"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Touchpad Settings - - /// - /// Enables or disables the touchpad - /// Command: {"EnableTouchPad": "{\"enable\":true}"} - /// - private static void HandleEnableTouchPad(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable"); - - // Open touchpad settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:devices-touchpad", - UseShellExecute = true - }); - - Debug.WriteLine($"Touchpad settings opened"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Adjusts touchpad cursor speed - /// Command: {"TouchpadCursorSpeed": "{\"speed\":5}"} - /// - private static void HandleTouchpadCursorSpeed(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - int speed = param.Value("speed") ?? 5; - - // Open touchpad settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:devices-touchpad", - UseShellExecute = true - }); - - Debug.WriteLine($"Touchpad settings opened for speed adjustment"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Privacy Settings - - /// - /// Manages microphone access for apps - /// Command: {"ManageMicrophoneAccess": "{\"accessSetting\":\"allow\"}"} - /// - private static void HandleManageMicrophoneAccess(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string access = param.Value("accessSetting"); - bool allow = access.Equals("allow", StringComparison.OrdinalIgnoreCase); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone")) - { - if (key != null) - { - key.SetValue("Value", allow ? "Allow" : "Deny", RegistryValueKind.String); - } - } - - Debug.WriteLine($"Microphone access {access}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Manages camera access for apps - /// Command: {"ManageCameraAccess": "{\"accessSetting\":\"allow\"}"} - /// - private static void HandleManageCameraAccess(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string access = param.Value("accessSetting") ?? "allow"; - bool allow = access.Equals("allow", StringComparison.OrdinalIgnoreCase); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam")) - { - if (key != null) - { - key.SetValue("Value", allow ? "Allow" : "Deny", RegistryValueKind.String); - } - } - - Debug.WriteLine($"Camera access {access}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Manages location access for apps - /// Command: {"ManageLocationAccess": "{\"accessSetting\":\"allow\"}"} - /// - private static void HandleManageLocationAccess(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string access = param.Value("accessSetting") ?? "allow"; - bool allow = access.Equals("allow", StringComparison.OrdinalIgnoreCase); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location")) - { - if (key != null) - { - key.SetValue("Value", allow ? "Allow" : "Deny", RegistryValueKind.String); - } - } - - Debug.WriteLine($"Location access {access}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Power Settings - - /// - /// Sets the battery saver activation threshold - /// Command: {"BatterySaverActivationLevel": "{\"thresholdValue\":20}"} - /// - private static void HandleBatterySaverActivationLevel(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - int threshold = param.Value("thresholdValue"); - - threshold = Math.Max(0, Math.Min(100, threshold)); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Power\BatterySaver")) - { - if (key != null) - { - key.SetValue("ActivationThreshold", threshold, RegistryValueKind.DWord); - } - } - - Debug.WriteLine($"Battery saver threshold set to: {threshold}%"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Sets power mode when plugged in - /// Command: {"SetPowerModePluggedIn": "{\"powerMode\":\"bestPerformance\"}"} - /// - private static void HandleSetPowerModePluggedIn(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string mode = param.Value("powerMode"); - - // Open power settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:powersleep", - UseShellExecute = true - }); - - Debug.WriteLine($"Power settings opened for mode adjustment"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Sets power mode when on battery - /// Command: {"SetPowerModeOnBattery": "{\"mode\":\"balanced\"}"} - /// - private static void HandleSetPowerModeOnBattery(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - string mode = param.Value("mode") ?? "balanced"; - - // Open power settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:powersleep", - UseShellExecute = true - }); - - Debug.WriteLine($"Power settings opened for battery mode adjustment"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Gaming Settings - - /// - /// Enables or disables Game Mode - /// Command: {"EnableGameMode": "{}"} - /// - private static void HandleEnableGameMode(string jsonParams) - { - try - { - // Open gaming settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:gaming-gamemode", - UseShellExecute = true - }); - - Debug.WriteLine($"Game Mode settings opened"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Accessibility Settings - - /// - /// Enables or disables Narrator - /// Command: {"EnableNarratorAction": "{\"enable\":true}"} - /// - private static void HandleEnableNarratorAction(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - if (enable) - { - Process.Start("narrator.exe"); - } - else - { - // Kill narrator process - var processes = Process.GetProcessesByName("Narrator"); - foreach (var p in processes) - { - p.Kill(); - } - } - - Debug.WriteLine($"Narrator {(enable ? "started" : "stopped")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables Magnifier - /// Command: {"EnableMagnifier": "{\"enable\":true}"} - /// - private static void HandleEnableMagnifier(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - if (enable) - { - Process.Start("magnify.exe"); - } - else - { - // Kill magnifier process - var processes = Process.GetProcessesByName("Magnify"); - foreach (var p in processes) - { - p.Kill(); - } - } - - Debug.WriteLine($"Magnifier {(enable ? "started" : "stopped")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables Sticky Keys - /// Command: {"EnableStickyKeys": "{\"enable\":true}"} - /// - private static void HandleEnableStickyKeysAction(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable"); - - using (var key = Registry.CurrentUser.CreateSubKey(@"Control Panel\Accessibility\StickyKeys")) - { - if (key != null) - { - key.SetValue("Flags", enable ? "510" : "506", RegistryValueKind.String); - } - } - - Debug.WriteLine($"Sticky Keys {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables Filter Keys - /// Command: {"EnableFilterKeysAction": "{\"enable\":true}"} - /// - private static void HandleEnableFilterKeysAction(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - using (var key = Registry.CurrentUser.CreateSubKey(@"Control Panel\Accessibility\Keyboard Response")) - { - if (key != null) - { - key.SetValue("Flags", enable ? "2" : "126", RegistryValueKind.String); - } - } - - Debug.WriteLine($"Filter Keys {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables mono audio - /// Command: {"MonoAudioToggle": "{\"enable\":true}"} - /// - private static void HandleMonoAudioToggle(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Multimedia\Audio")) - { - if (key != null) - { - key.SetValue("AccessibilityMonoMixState", enable ? 1 : 0, RegistryValueKind.DWord); - } - } - - Debug.WriteLine($"Mono audio {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region File Explorer Settings - - /// - /// Shows or hides file extensions in File Explorer - /// Command: {"ShowFileExtensions": "{\"enable\":true}"} - /// - private static void HandleShowFileExtensions(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")) - { - if (key != null) - { - // 0 = show extensions, 1 = hide extensions - key.SetValue("HideFileExt", enable ? 0 : 1, RegistryValueKind.DWord); - RefreshExplorer(); - } - } - - Debug.WriteLine($"File extensions {(enable ? "shown" : "hidden")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Shows or hides hidden and system files in File Explorer - /// Command: {"ShowHiddenAndSystemFiles": "{\"enable\":true}"} - /// - private static void HandleShowHiddenAndSystemFiles(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced")) - { - if (key != null) - { - // 1 = show hidden files, 2 = don't show hidden files - key.SetValue("Hidden", enable ? 1 : 2, RegistryValueKind.DWord); - // Show protected OS files - key.SetValue("ShowSuperHidden", enable ? 1 : 0, RegistryValueKind.DWord); - RefreshExplorer(); - } - } - - Debug.WriteLine($"Hidden files {(enable ? "shown" : "hidden")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Time & Region Settings - - /// - /// Enables or disables automatic time synchronization - /// Command: {"AutomaticTimeSettingAction": "{\"enableAutoTimeSync\":true}"} - /// - private static void HandleAutomaticTimeSettingAction(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enableAutoTimeSync"); - - // Open time settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:dateandtime", - UseShellExecute = true - }); - - Debug.WriteLine($"Time settings opened for auto-sync configuration"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Enables or disables automatic DST adjustment - /// Command: {"AutomaticDSTAdjustment": "{\"enable\":true}"} - /// - private static void HandleAutomaticDSTAdjustment(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - using (var key = Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Control\TimeZoneInformation")) - { - if (key != null) - { - key.SetValue("DynamicDaylightTimeDisabled", enable ? 0 : 1, RegistryValueKind.DWord); - } - } - - Debug.WriteLine($"Automatic DST adjustment {(enable ? "enabled" : "disabled")}"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Focus Assist Settings - - /// - /// Enables or disables Focus Assist (Quiet Hours) - /// Command: {"EnableQuietHours": "{\"startHour\":22,\"endHour\":7}"} - /// - private static void HandleEnableQuietHours(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - int startHour = param.Value("startHour") ?? 22; - int endHour = param.Value("endHour") ?? 7; - - // Open Focus Assist settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:quiethours", - UseShellExecute = true - }); - - Debug.WriteLine($"Focus Assist settings opened"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Multi-Monitor Settings - - /// - /// Remembers window locations based on monitor configuration - /// Command: {"RememberWindowLocations": "{\"enable\":true}"} - /// - private static void HandleRememberWindowLocationsAction(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable"); - - // This is handled by Windows automatically, but we can open display settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:display", - UseShellExecute = true - }); - - Debug.WriteLine($"Display settings opened for window location management"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Minimizes windows when a monitor is disconnected - /// Command: {"MinimizeWindowsOnMonitorDisconnectAction": "{\"enable\":true}"} - /// - private static void HandleMinimizeWindowsOnMonitorDisconnectAction(string jsonParams) - { - try - { - var param = JObject.Parse(jsonParams); - bool enable = param.Value("enable") ?? true; - - // Open display settings - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:display", - UseShellExecute = true - }); - - Debug.WriteLine($"Display settings opened for disconnect behavior"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Helper Methods - - /// - /// Gets the current brightness level - /// - private static byte GetCurrentBrightness() - { - try - { - using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\SettingSync\Settings\SystemSettings\Brightness")) - { - if (key != null) - { - object value = key.GetValue("Data"); - if (value is byte[] data && data.Length > 0) - { - return data[0]; - } - } - } - } - catch { } - - return 50; // Default to 50% if unable to read - } - - /// - /// Sets the brightness level - /// - private static void SetBrightness(byte brightness) - { - try - { - // Use WMI to set brightness - using (var searcher = new System.Management.ManagementObjectSearcher("root\\WMI", "SELECT * FROM WmiMonitorBrightnessMethods")) - { - using (var objectCollection = searcher.Get()) - { - foreach (System.Management.ManagementObject obj in objectCollection) - { - obj.InvokeMethod("WmiSetBrightness", new object[] { 1, brightness }); - } - } - } - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to set brightness: {ex.Message}"); - } - } - - /// - /// Sets DPI scaling percentage - /// - private static void SetDpiScaling(int percentage) - { - try - { - // Open display settings for DPI adjustment - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:display", - UseShellExecute = true - }); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to set DPI scaling: {ex.Message}"); - } - } - - /// - /// Refreshes the taskbar to apply changes - /// - private static void RefreshTaskbar() - { - try - { - // Send a broadcast message to refresh the explorer - SendNotifyMessage(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, IntPtr.Zero); - } - catch { } - } - - /// - /// Refreshes File Explorer to apply changes - /// - private static void RefreshExplorer() - { - try - { - SendNotifyMessage(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, IntPtr.Zero); - - // Alternative: restart explorer - // var processes = Process.GetProcessesByName("explorer"); - // foreach (var p in processes) p.Kill(); - // Process.Start("explorer.exe"); - } - catch { } - } - - /// - /// Sets a registry value - /// - private static void SetRegistryValue(string keyPath, string valueName, object value) - { - try - { - Registry.SetValue(keyPath, valueName, value); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to set registry value: {ex.Message}"); - } - } - - #endregion - - #region Bridge Method - - /// - /// Bridge method for SettingsCommandHandler — dispatches to individual settings handlers. - /// - internal static void HandleSettingsCommand(string key, string value) - { - switch (key) - { - case "BluetoothToggle": HandleBluetoothToggle(value); break; - case "EnableWifi": HandleEnableWifi(value); break; - case "EnableMeteredConnections": HandleEnableMeteredConnections(value); break; - case "AdjustScreenBrightness": HandleAdjustScreenBrightness(value); break; - case "EnableBlueLightFilterSchedule": HandleEnableBlueLightFilterSchedule(value); break; - case "AdjustColorTemperature": HandleAdjustColorTemperature(value); break; - case "DisplayScaling": HandleDisplayScaling(value); break; - case "AdjustScreenOrientation": HandleAdjustScreenOrientation(value); break; - case "DisplayResolutionAndAspectRatio": HandleDisplayResolutionAndAspectRatio(value); break; - case "RotationLock": HandleRotationLock(value); break; - case "SystemThemeMode": HandleSystemThemeMode(value); break; - case "EnableTransparency": HandleEnableTransparency(value); break; - case "ApplyColorToTitleBar": HandleApplyColorToTitleBar(value); break; - case "HighContrastTheme": HandleHighContrastTheme(value); break; - case "AutoHideTaskbar": HandleAutoHideTaskbar(value); break; - case "TaskbarAlignment": HandleTaskbarAlignment(value); break; - case "TaskViewVisibility": HandleTaskViewVisibility(value); break; - case "ToggleWidgetsButtonVisibility": HandleToggleWidgetsButtonVisibility(value); break; - case "ShowBadgesOnTaskbar": HandleShowBadgesOnTaskbar(value); break; - case "DisplayTaskbarOnAllMonitors": HandleDisplayTaskbarOnAllMonitors(value); break; - case "DisplaySecondsInSystrayClock": HandleDisplaySecondsInSystrayClock(value); break; - case "MouseCursorSpeed": HandleMouseCursorSpeed(value); break; - case "MouseWheelScrollLines": HandleMouseWheelScrollLines(value); break; - case "SetPrimaryMouseButton": HandleSetPrimaryMouseButton(value); break; - case "EnhancePointerPrecision": HandleEnhancePointerPrecision(value); break; - case "AdjustMousePointerSize": HandleAdjustMousePointerSize(value); break; - case "MousePointerCustomization": HandleMousePointerCustomization(value); break; - case "EnableTouchPad": HandleEnableTouchPad(value); break; - case "TouchpadCursorSpeed": HandleTouchpadCursorSpeed(value); break; - case "ManageMicrophoneAccess": HandleManageMicrophoneAccess(value); break; - case "ManageCameraAccess": HandleManageCameraAccess(value); break; - case "ManageLocationAccess": HandleManageLocationAccess(value); break; - case "BatterySaverActivationLevel": HandleBatterySaverActivationLevel(value); break; - case "SetPowerModePluggedIn": HandleSetPowerModePluggedIn(value); break; - case "SetPowerModeOnBattery": HandleSetPowerModeOnBattery(value); break; - case "EnableGameMode": HandleEnableGameMode(value); break; - case "EnableNarratorAction": HandleEnableNarratorAction(value); break; - case "EnableMagnifier": HandleEnableMagnifier(value); break; - case "EnableStickyKeys": HandleEnableStickyKeysAction(value); break; - case "EnableFilterKeysAction": HandleEnableFilterKeysAction(value); break; - case "MonoAudioToggle": HandleMonoAudioToggle(value); break; - case "ShowFileExtensions": HandleShowFileExtensions(value); break; - case "ShowHiddenAndSystemFiles": HandleShowHiddenAndSystemFiles(value); break; - case "AutomaticTimeSettingAction": HandleAutomaticTimeSettingAction(value); break; - case "AutomaticDSTAdjustment": HandleAutomaticDSTAdjustment(value); break; - case "EnableQuietHours": HandleEnableQuietHours(value); break; - case "RememberWindowLocations": HandleRememberWindowLocationsAction(value); break; - case "MinimizeWindowsOnMonitorDisconnectAction": HandleMinimizeWindowsOnMonitorDisconnectAction(value); break; - default: - Debug.WriteLine($"Unknown settings command: {key}"); - break; - } - } - - #endregion - - #region Win32 API Declarations for Settings - - // SystemParametersInfo constants (additional ones not in AutoShell_Win32.cs) - private const int SPI_SETMOUSESPEED = 0x0071; - private const int SPI_GETMOUSE = 0x0003; - private const int SPI_SETMOUSE = 0x0004; - private const int SPI_SETWHEELSCROLLLINES = 0x0069; - // Note: SPIF_UPDATEINIFILE, SPIF_SENDCHANGE, WM_SETTINGCHANGE, HWND_BROADCAST - // are already defined in AutoShell_Win32.cs - - [DllImport("user32.dll", SetLastError = true)] - private static extern bool SystemParametersInfo(int uiAction, int uiParam, IntPtr pvParam, int fWinIni); - - [DllImport("user32.dll", SetLastError = true)] - private static extern bool SystemParametersInfo(int uiAction, int uiParam, int[] pvParam, int fWinIni); - - [DllImport("user32.dll")] - private static extern bool SwapMouseButton(int fSwap); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern IntPtr SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - #endregion -} diff --git a/dotnet/autoShell/AutoShell_Themes.cs b/dotnet/autoShell/AutoShell_Themes.cs deleted file mode 100644 index e62bd611d7..0000000000 --- a/dotnet/autoShell/AutoShell_Themes.cs +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using Microsoft.Win32; - -namespace autoShell; - -/// -/// AutoShell class partial for managing Windows themes. -/// -/// This code was mostly generated by AI under the guidance of a human. -internal partial class AutoShell -{ - private static string s_previousTheme = null; - private static Dictionary s_themeDictionary = null; - private static Dictionary s_themeDisplayNameDictionary = null; - - private static void LoadThemes() - { - s_themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - s_themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - - string[] themePaths = - [ - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Themes"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Ease of Access Themes"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Themes") - ]; - - foreach (string themesFolder in themePaths) - { - if (Directory.Exists(themesFolder)) - { - foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme")) - { - string themeName = Path.GetFileNameWithoutExtension(themeFile); - if (!s_themeDictionary.ContainsKey(themeName)) - { - s_themeDictionary[themeName] = themeFile; - - // Parse display name from theme file - string displayName = GetThemeDisplayName(themeFile); - if (!string.IsNullOrEmpty(displayName) && !s_themeDisplayNameDictionary.ContainsKey(displayName)) - { - s_themeDisplayNameDictionary[displayName] = themeName; - } - } - } - } - } - - s_themeDictionary["previous"] = GetCurrentTheme(); - } - - /// - /// Parses the display name from a .theme file. - /// - /// The full path to the .theme file. - /// The display name, or null if not found. - private static string GetThemeDisplayName(string themeFilePath) - { - try - { - foreach (string line in File.ReadLines(themeFilePath)) - { - if (line.StartsWith("DisplayName=", StringComparison.OrdinalIgnoreCase)) - { - string displayName = line["DisplayName=".Length..].Trim(); - // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013) - if (displayName.StartsWith("@")) - { - displayName = ResolveLocalizedString(displayName); - } - return displayName; - } - } - } - catch - { - // Ignore errors reading theme file - } - return null; - } - - /// - /// Resolves a localized string resource reference. - /// - /// The localized string reference (e.g., @%SystemRoot%\System32\themeui.dll,-2013). - /// The resolved string, or the original string if resolution fails. - private static string ResolveLocalizedString(string localizedString) - { - try - { - // Remove the @ prefix - string resourcePath = localizedString[1..]; - // Expand environment variables - int commaIndex = resourcePath.LastIndexOf(','); - if (commaIndex > 0) - { - string dllPath = Environment.ExpandEnvironmentVariables(resourcePath[..commaIndex]); - string resourceIdStr = resourcePath[(commaIndex + 1)..]; - if (int.TryParse(resourceIdStr, out int resourceId)) - { - StringBuilder buffer = new StringBuilder(256); - IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); - if (hModule != IntPtr.Zero) - { - try - { - int result = LoadString(hModule, (uint)Math.Abs(resourceId), buffer, buffer.Capacity); - if (result > 0) - { - return buffer.ToString(); - } - } - finally - { - FreeLibrary(hModule); - } - } - } - } - } - catch - { - // Ignore errors resolving localized string - } - return localizedString; - } - - /// - /// Returns a list of all installed Windows themes. - /// - /// A list of theme names (without the .theme extension). - public static List GetInstalledThemes() - { - HashSet themes = []; - - themes.UnionWith(s_themeDictionary.Keys); - themes.UnionWith(s_themeDisplayNameDictionary.Keys); - - return [.. themes]; - } - - /// - /// Gets the current Windows theme name. - /// - /// The current theme name, or null if it cannot be determined. - public static string GetCurrentTheme() - { - try - { - using RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes"); - if (key != null) - { - string currentThemePath = key.GetValue("CurrentTheme") as string; - if (!string.IsNullOrEmpty(currentThemePath)) - { - return Path.GetFileNameWithoutExtension(currentThemePath); - } - } - } - catch - { - // Ignore errors reading registry - } - return null; - } - - /// - /// Applies a Windows theme by name. - /// - /// The name of the theme to apply (without .theme extension). - /// True if the theme was applied successfully, false otherwise. - public static bool ApplyTheme(string themeName) - { - string themePath = FindThemePath(themeName); - if (string.IsNullOrEmpty(themePath)) - { - return false; - } - - try - { - string previous = GetCurrentTheme(); - - if (!themeName.Equals("previous", StringComparison.InvariantCultureIgnoreCase)) - { - // Apply theme by opening the .theme file - Process p = Process.Start(themePath); - s_previousTheme = previous; - p.Exited += P_Exited; - - return true; - } - else - { - bool success = RevertToPreviousTheme(); - - if (success) - { - s_previousTheme = previous; - } - - return success; - } - } - catch - { - return false; - } - } - - private static void P_Exited(object sender, EventArgs e) - { - Debug.WriteLine(((Process)sender).ExitCode); - } - - /// - /// Reverts to the previous Windows theme. - /// - /// True if the previous theme was applied successfully, false otherwise. - public static bool RevertToPreviousTheme() - { - if (string.IsNullOrEmpty(s_previousTheme)) - { - return false; - } - - string themePath = FindThemePath(s_previousTheme); - if (string.IsNullOrEmpty(themePath)) - { - return false; - } - - try - { - Process.Start(themePath); - return true; - } - catch - { - return false; - } - } - - /// - /// Gets the name of the previous theme. - /// - /// The previous theme name, or null if no theme change has been made. - public static string GetPreviousTheme() - { - return s_previousTheme; - } - - /// - /// Finds the full path to a theme file by name or display name. - /// - /// The name of the theme (file name without extension or display name). - /// The full path to the theme file, or null if not found. - private static string FindThemePath(string themeName) - { - // First check by file name - if (s_themeDictionary.TryGetValue(themeName, out string themePath)) - { - return themePath; - } - - // Then check by display name - if (s_themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) - { - if (s_themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) - { - return themePathFromDisplay; - } - } - - return null; - } - - /// - /// Sets the Windows light or dark mode by modifying registry keys. - /// - /// True for light mode, false for dark mode. - /// True if the mode was set successfully, false otherwise. - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - public static bool SetLightDarkMode(bool useLightMode) - { - try - { - const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - int value = useLightMode ? 1 : 0; - - using (RegistryKey key = Registry.CurrentUser.OpenSubKey(personalizePath, true)) - { - if (key == null) - { - return false; - } - - // Set apps theme - key.SetValue("AppsUseLightTheme", value, RegistryValueKind.DWord); - - // Set system theme (taskbar, Start menu, etc.) - key.SetValue("SystemUsesLightTheme", value, RegistryValueKind.DWord); - } - - // Broadcast settings change notification to update UI - BroadcastSettingsChange(); - - return true; - } - catch - { - return false; - } - } - - /// - /// Toggles between light and dark mode. - /// - /// True if the toggle was successful, false otherwise. - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - public static bool ToggleLightDarkMode() - { - bool? currentMode = GetCurrentLightMode(); - return currentMode.HasValue ? SetLightDarkMode(!currentMode.Value) : false; - } - - /// - /// Broadcasts a WM_SETTINGCHANGE message to notify the system of theme changes. - /// - private static void BroadcastSettingsChange() - { - const int HWND_BROADCAST = 0xffff; - const int WM_SETTINGCHANGE = 0x001A; - const uint SMTO_ABORTIFHUNG = 0x0002; - SendMessageTimeout( - (IntPtr)HWND_BROADCAST, - WM_SETTINGCHANGE, - IntPtr.Zero, - "ImmersiveColorSet", - SMTO_ABORTIFHUNG, - 1000, - out nint result); - } - - /// - /// Gets the current light/dark mode setting from the registry. - /// - /// True if light mode, false if dark mode, null if unable to determine. - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - private static bool? GetCurrentLightMode() - { - try - { - const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - - using RegistryKey key = Registry.CurrentUser.OpenSubKey(personalizePath, false); - if (key == null) - { - return null; - } - - // Read AppsUseLightTheme value (0 = dark, 1 = light) - object value = key.GetValue("AppsUseLightTheme"); - return value is int intValue ? intValue == 1 : null; - } - catch - { - return null; - } - } - -} diff --git a/dotnet/autoShell/AutoShell_Win32.cs b/dotnet/autoShell/AutoShell_Win32.cs index 7e2656bcae..68a37f99c3 100644 --- a/dotnet/autoShell/AutoShell_Win32.cs +++ b/dotnet/autoShell/AutoShell_Win32.cs @@ -8,13 +8,8 @@ namespace autoShell { - internal unsafe partial class AutoShell + internal partial class AutoShell { - private const int SPI_SETDESKWALLPAPER = 20; - private const int SPIF_UPDATEINIFILE = 0x01; - private const int SPIF_SENDCHANGE = 0x02; - private const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002; - // Text scaling constants private const uint WM_SETTINGCHANGE = 0x001A; private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff); @@ -38,10 +33,6 @@ internal struct Size [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, ref RECT Rect); - // import GetShellWindow - [DllImport("user32.dll")] - private static extern IntPtr GetShellWindow(); - // import GetDesktopWindow [DllImport("user32.dll")] private static extern IntPtr GetDesktopWindow(); @@ -53,16 +44,6 @@ internal struct Size [DllImport("user32.dll", EntryPoint = "SendMessage", SetLastError = true)] private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, UInt32 wParam, IntPtr lParam); - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern IntPtr SendMessageTimeout( - IntPtr hWnd, - uint Msg, - IntPtr wParam, - string lParam, - uint fuFlags, - uint uTimeout, - out IntPtr lpdwResult); - // import SetWindowPos [DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); @@ -75,18 +56,6 @@ private static extern IntPtr SendMessageTimeout( [DllImport("user32.dll")] internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName); - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); - - [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] - private static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags); - - [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] - private static extern bool FreeLibrary(IntPtr hModule); - - [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)] - private static extern int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax); - #region Virtual Desktop APIs public enum APPLICATION_VIEW_CLOAK_TYPE : int @@ -343,33 +312,8 @@ internal interface IServiceProvider10 [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - // get handle of active window - [DllImport("user32.dll")] - private static extern IntPtr GetForegroundWindow(); - #endregion Window Functions - [DllImport("shell32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr ShellExecute( - IntPtr hwnd, - string lpOperation, - string lpFile, - string lpParameters, - string lpDirectory, - int nShowCmd); - - - [DllImport("combase.dll")] - internal static extern int WindowsCreateString(char* sourceString, int length, out IntPtr hstring); - - [DllImport("combase.dll")] - internal static extern int WindowsDeleteString(IntPtr hstring); - - [DllImport("combase.dll")] - internal static extern char* WindowsGetStringRawBuffer(IntPtr hstring, out uint length); - - // Add these COM interface definitions for Radio Management API - // GUIDs for Radio Management API internal static readonly Guid CLSID_RadioManagementAPI = new Guid(0x581333f6, 0x28db, 0x41be, 0xbc, 0x7a, 0xff, 0x20, 0x1f, 0x12, 0xf3, 0xf6); internal static readonly Guid IID_IRadioManager = new Guid(0xdb3afbfb, 0x08e6, 0x46c6, 0xaa, 0x70, 0xbf, 0x9a, 0x34, 0xc3, 0x0a, 0xb7); @@ -516,15 +460,8 @@ enum DOT11_BSS_TYPE #region Display Resolution private const int ENUM_CURRENT_SETTINGS = -1; - private const int ENUM_REGISTRY_SETTINGS = -2; private const int DISP_CHANGE_SUCCESSFUL = 0; private const int DISP_CHANGE_RESTART = 1; - private const int DISP_CHANGE_FAILED = -1; - private const int DISP_CHANGE_BADMODE = -2; - private const int DISP_CHANGE_NOTUPDATED = -3; - private const int DISP_CHANGE_BADFLAGS = -4; - private const int DISP_CHANGE_BADPARAM = -5; - private const int DISP_CHANGE_BADDUALVIEW = -6; private const int DM_PELSWIDTH = 0x80000; private const int DM_PELSHEIGHT = 0x100000; @@ -533,10 +470,6 @@ enum DOT11_BSS_TYPE private const int CDS_UPDATEREGISTRY = 0x01; private const int CDS_TEST = 0x02; - private const int CDS_FULLSCREEN = 0x04; - private const int CDS_GLOBAL = 0x08; - private const int CDS_SET_PRIMARY = 0x10; - private const int CDS_RESET = 0x40000000; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] internal struct DEVMODE diff --git a/dotnet/autoShell/Handlers/AppCommandHandler.cs b/dotnet/autoShell/Handlers/AppCommandHandler.cs index 8487105892..565ae6c789 100644 --- a/dotnet/autoShell/Handlers/AppCommandHandler.cs +++ b/dotnet/autoShell/Handlers/AppCommandHandler.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; @@ -22,6 +24,20 @@ internal class AppCommandHandler : ICommandHandler /// public void Handle(string key, string value, JToken rawValue) { - AutoShell.HandleAppCommand(key, value); + switch (key) + { + case "LaunchProgram": + AutoShell.OpenApplication(value); + break; + + case "CloseProgram": + AutoShell.CloseApplication(value); + break; + + case "ListAppNames": + var installedApps = AutoShell.GetAllInstalledAppsIds(); + Console.WriteLine(JsonConvert.SerializeObject(installedApps.Keys)); + break; + } } } diff --git a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs index 3347fe37ff..91326f7339 100644 --- a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs +++ b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs @@ -8,7 +8,6 @@ namespace autoShell.Handlers; /// /// Handles display commands: setScreenResolution, listResolutions, setTextSize. -/// Delegates to existing static methods in AutoShell. /// internal class DisplayCommandHandler : ICommandHandler { @@ -23,6 +22,22 @@ internal class DisplayCommandHandler : ICommandHandler /// public void Handle(string key, string value, JToken rawValue) { - AutoShell.HandleDisplayCommand(key, value, rawValue); + switch (key) + { + case "SetTextSize": + if (int.TryParse(value, out int textSizePct)) + { + AutoShell.SetTextSize(textSizePct); + } + break; + + case "SetScreenResolution": + AutoShell.SetDisplayResolution(rawValue); + break; + + case "ListResolutions": + AutoShell.ListDisplayResolutions(); + break; + } } } diff --git a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs index a0385e1d7c..aff0e6dc9b 100644 --- a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs +++ b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs @@ -1,14 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Diagnostics; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; /// -/// Handles network commands: toggleAirplaneMode, listWifiNetworks, connectWifi, disconnectWifi. -/// Delegates to existing static methods in AutoShell. +/// Handles network commands: toggleAirplaneMode, listWifiNetworks, connectWifi, disconnectWifi, +/// bluetoothToggle, enableWifi, enableMeteredConnections. /// internal class NetworkCommandHandler : ICommandHandler { @@ -27,6 +29,33 @@ internal class NetworkCommandHandler : ICommandHandler /// public void Handle(string key, string value, JToken rawValue) { - AutoShell.HandleNetworkCommand(key, value); + switch (key) + { + case "ToggleAirplaneMode": + AutoShell.SetAirplaneMode(bool.Parse(value)); + break; + + case "ListWifiNetworks": + AutoShell.ListWifiNetworks(); + break; + + case "ConnectWifi": + var netInfo = JObject.Parse(value); + string ssid = netInfo.Value("ssid"); + string password = netInfo["password"] is not null ? netInfo.Value("password") : ""; + AutoShell.ConnectToWifi(ssid, password); + break; + + case "DisconnectWifi": + AutoShell.DisconnectFromWifi(); + break; + + case "BluetoothToggle": + case "EnableWifi": + case "EnableMeteredConnections": + // Not yet implemented — requires additional infrastructure + Debug.WriteLine($"Command not yet implemented: {key}"); + break; + } } } diff --git a/dotnet/autoShell/Handlers/SettingsCommandHandler.cs b/dotnet/autoShell/Handlers/SettingsCommandHandler.cs deleted file mode 100644 index d9c7ff036b..0000000000 --- a/dotnet/autoShell/Handlers/SettingsCommandHandler.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using autoShell.Services; -using Newtonsoft.Json.Linq; - -namespace autoShell.Handlers; - -/// -/// Handles all Windows Settings actions (50+ registry/SystemParametersInfo-based toggles). -/// Groups: Network, Display, Personalization, Taskbar, Mouse, Touchpad, Privacy, Power, -/// Gaming, Accessibility, File Explorer, Time/Region, Focus Assist, Multi-Monitor. -/// -internal class SettingsCommandHandler : ICommandHandler -{ - /// - public IEnumerable SupportedCommands { get; } = - [ - // Accessibility - "EnableFilterKeysAction", - "EnableMagnifier", - "EnableNarratorAction", - "EnableStickyKeys", - "MonoAudioToggle", - // Display - "AdjustScreenBrightness", - "AdjustScreenOrientation", - "AdjustColorTemperature", - "DisplayResolutionAndAspectRatio", - "DisplayScaling", - "EnableBlueLightFilterSchedule", - "RotationLock", - // File Explorer - "ShowFileExtensions", - "ShowHiddenAndSystemFiles", - // Focus Assist - "EnableQuietHours", - // Gaming - "EnableGameMode", - // Mouse - "AdjustMousePointerSize", - "EnhancePointerPrecision", - "MouseCursorSpeed", - "MousePointerCustomization", - "MouseWheelScrollLines", - "SetPrimaryMouseButton", - // Multi-Monitor - "MinimizeWindowsOnMonitorDisconnectAction", - "RememberWindowLocations", - // Personalization - "ApplyColorToTitleBar", - "EnableTransparency", - "HighContrastTheme", - "SystemThemeMode", - // Power - "BatterySaverActivationLevel", - "SetPowerModeOnBattery", - "SetPowerModePluggedIn", - // Privacy - "ManageCameraAccess", - "ManageLocationAccess", - "ManageMicrophoneAccess", - // Taskbar - "AutoHideTaskbar", - "DisplaySecondsInSystrayClock", - "DisplayTaskbarOnAllMonitors", - "ShowBadgesOnTaskbar", - "TaskbarAlignment", - "TaskViewVisibility", - "ToggleWidgetsButtonVisibility", - // Time & Region - "AutomaticDSTAdjustment", - "AutomaticTimeSettingAction", - // Touchpad - "EnableTouchPad", - "TouchpadCursorSpeed", - ]; - - private readonly IRegistryService _registry; - private readonly ISystemParametersService _systemParams; - private readonly IProcessService _process; - - public SettingsCommandHandler( - IRegistryService registry, - ISystemParametersService systemParams, - IProcessService process) - { - _registry = registry; - _systemParams = systemParams; - _process = process; - } - - /// - public void Handle(string key, string value, JToken rawValue) - { - // Delegate to the existing static handlers in AutoShell_Settings.cs - // This preserves all existing behavior during Phase 1 - AutoShell.HandleSettingsCommand(key, value); - } -} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/AccessibilitySettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/AccessibilitySettingsHandler.cs new file mode 100644 index 0000000000..f16e5f7d5f --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/AccessibilitySettingsHandler.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles accessibility settings: filter keys, magnifier, narrator, sticky keys, and mono audio. +/// +internal class AccessibilitySettingsHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "EnableFilterKeysAction", + "EnableMagnifier", + "EnableNarratorAction", + "EnableStickyKeys", + "MonoAudioToggle", + ]; + + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public AccessibilitySettingsHandler(IRegistryService registry, IProcessService process) + { + _registry = registry; + _process = process; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + var param = JObject.Parse(value); + + switch (key) + { + case "EnableFilterKeysAction": + HandleFilterKeys(param); + break; + + case "EnableMagnifier": + HandleToggleProcess(param, "magnify.exe", "Magnify"); + break; + + case "EnableNarratorAction": + HandleToggleProcess(param, "narrator.exe", "Narrator"); + break; + + case "EnableStickyKeys": + HandleStickyKeys(param); + break; + + case "MonoAudioToggle": + HandleMonoAudio(param); + break; + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + private void HandleFilterKeys(JObject param) + { + bool enable = param.Value("enable") ?? true; + _registry.SetValue( + @"Control Panel\Accessibility\Keyboard Response", + "Flags", + enable ? "2" : "126", + RegistryValueKind.String); + } + + private void HandleStickyKeys(JObject param) + { + bool enable = param.Value("enable") ?? true; + _registry.SetValue( + @"Control Panel\Accessibility\StickyKeys", + "Flags", + enable ? "510" : "506", + RegistryValueKind.String); + } + + private void HandleMonoAudio(JObject param) + { + bool enable = param.Value("enable") ?? true; + _registry.SetValue( + @"Software\Microsoft\Multimedia\Audio", + "AccessibilityMonoMixState", + enable ? 1 : 0, + RegistryValueKind.DWord); + } + + private void HandleToggleProcess(JObject param, string exeName, string processName) + { + bool enable = param.Value("enable") ?? true; + + if (enable) + { + _process.Start(new System.Diagnostics.ProcessStartInfo { FileName = exeName }); + } + else + { + foreach (var p in _process.GetProcessesByName(processName)) + { + p.Kill(); + } + } + } +} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/DisplaySettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/DisplaySettingsHandler.cs new file mode 100644 index 0000000000..4ca91fde74 --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/DisplaySettingsHandler.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles display settings: brightness, color temperature, orientation, resolution, scaling, +/// blue light filter, and rotation lock. +/// +internal class DisplaySettingsHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "AdjustColorTemperature", + "AdjustScreenBrightness", + "AdjustScreenOrientation", + "DisplayResolutionAndAspectRatio", + "DisplayScaling", + "EnableBlueLightFilterSchedule", + "RotationLock", + ]; + + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public DisplaySettingsHandler(IRegistryService registry, IProcessService process) + { + _registry = registry; + _process = process; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + var param = JObject.Parse(value); + + switch (key) + { + case "AdjustScreenBrightness": + HandleAdjustScreenBrightness(param); + break; + + case "DisplayScaling": + HandleDisplayScaling(param); + break; + + case "AdjustColorTemperature": + _process.StartShellExecute("ms-settings:nightlight"); + break; + + case "AdjustScreenOrientation": + case "DisplayResolutionAndAspectRatio": + _process.StartShellExecute("ms-settings:display"); + break; + + case "EnableBlueLightFilterSchedule": + HandleBlueLightFilter(param); + break; + + case "RotationLock": + HandleRotationLock(param); + break; + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + private void HandleAdjustScreenBrightness(JObject param) + { + string level = param.Value("brightnessLevel"); + bool increase = level == "increase"; + + byte currentBrightness = GetCurrentBrightness(); + byte newBrightness = increase + ? (byte)Math.Min(100, currentBrightness + 10) + : (byte)Math.Max(0, currentBrightness - 10); + + SetBrightness(newBrightness); + Debug.WriteLine($"Brightness adjusted to: {newBrightness}%"); + } + + private void HandleDisplayScaling(JObject param) + { + string sizeStr = param.Value("sizeOverride"); + + if (int.TryParse(sizeStr, out int percentage)) + { + percentage = percentage switch + { + < 113 => 100, + < 138 => 125, + < 163 => 150, + < 188 => 175, + _ => 200 + }; + + // DPI scaling requires opening settings + _process.StartShellExecute("ms-settings:display"); + Debug.WriteLine($"Display scaling target: {percentage}%"); + } + } + + private static byte GetCurrentBrightness() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey( + @"Software\Microsoft\Windows\CurrentVersion\SettingSync\Settings\SystemSettings\Brightness"); + if (key != null) + { + object value = key.GetValue("Data"); + if (value is byte[] data && data.Length > 0) + return data[0]; + } + } + catch { } + return 50; + } + + private static void SetBrightness(byte brightness) + { + try + { + using var searcher = new System.Management.ManagementObjectSearcher( + "root\\WMI", "SELECT * FROM WmiMonitorBrightnessMethods"); + using var objectCollection = searcher.Get(); + foreach (System.Management.ManagementObject obj in objectCollection) + { + obj.InvokeMethod("WmiSetBrightness", new object[] { 1, brightness }); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to set brightness: {ex.Message}"); + } + } + + private void HandleBlueLightFilter(JObject param) + { + bool disabled = param.Value("nightLightScheduleDisabled") ?? false; + byte[] data = disabled + ? new byte[] { 0x02, 0x00, 0x00, 0x00 } + : new byte[] { 0x02, 0x00, 0x00, 0x01 }; + + _registry.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount\Current\default$windows.data.bluelightreduction.settings\windows.data.bluelightreduction.settings", + "Data", + data, + RegistryValueKind.Binary); + } + + private void HandleRotationLock(JObject param) + { + bool enable = param.Value("enable") ?? true; + _registry.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell", + "RotationLockPreference", + enable ? 1 : 0, + RegistryValueKind.DWord); + } +} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/FileExplorerSettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/FileExplorerSettingsHandler.cs new file mode 100644 index 0000000000..26654264b6 --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/FileExplorerSettingsHandler.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles File Explorer settings: file extensions and hidden/system files visibility. +/// +internal partial class FileExplorerSettingsHandler : ICommandHandler +{ + private const string ExplorerAdvanced = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; + + [LibraryImport("user32.dll")] + private static partial IntPtr SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + /// + public IEnumerable SupportedCommands { get; } = + [ + "ShowFileExtensions", + "ShowHiddenAndSystemFiles", + ]; + + private readonly IRegistryService _registry; + + public FileExplorerSettingsHandler(IRegistryService registry) + { + _registry = registry; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + var param = JObject.Parse(value); + + switch (key) + { + case "ShowFileExtensions": + HandleShowFileExtensions(param); + break; + + case "ShowHiddenAndSystemFiles": + HandleShowHiddenAndSystemFiles(param); + break; + } + + SendNotifyMessage((IntPtr)0xffff, 0x001A, IntPtr.Zero, IntPtr.Zero); + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + private void HandleShowFileExtensions(JObject param) + { + bool enable = param.Value("enable") ?? true; + // Inverted: enable showing extensions = HideFileExt 0 + _registry.SetValue(ExplorerAdvanced, "HideFileExt", enable ? 0 : 1, RegistryValueKind.DWord); + } + + private void HandleShowHiddenAndSystemFiles(JObject param) + { + bool enable = param.Value("enable") ?? true; + _registry.SetValue(ExplorerAdvanced, "Hidden", enable ? 1 : 2, RegistryValueKind.DWord); + _registry.SetValue(ExplorerAdvanced, "ShowSuperHidden", enable ? 1 : 0, RegistryValueKind.DWord); + } +} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/MouseSettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/MouseSettingsHandler.cs new file mode 100644 index 0000000000..91d94bfa8c --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/MouseSettingsHandler.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles mouse and touchpad settings: pointer size, precision, cursor speed, scroll lines, +/// primary button, customization, and touchpad. +/// +internal partial class MouseSettingsHandler : ICommandHandler +{ + private const int SPI_GETMOUSE = 3; + private const int SPI_SETMOUSE = 4; + private const int SPI_SETMOUSESPEED = 0x0071; + private const int SPI_SETWHEELSCROLLLINES = 0x0069; + private const int SPIF_UPDATEINIFILE_SENDCHANGE = 3; + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SwapMouseButton(int fSwap); + + /// + public IEnumerable SupportedCommands { get; } = + [ + "AdjustMousePointerSize", + "EnableTouchPad", + "EnhancePointerPrecision", + "MouseCursorSpeed", + "MousePointerCustomization", + "MouseWheelScrollLines", + "SetPrimaryMouseButton", + "TouchpadCursorSpeed", + ]; + + private readonly ISystemParametersService _systemParams; + private readonly IProcessService _process; + + public MouseSettingsHandler(ISystemParametersService systemParams, IProcessService process) + { + _systemParams = systemParams; + _process = process; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + var param = JObject.Parse(value); + + switch (key) + { + case "AdjustMousePointerSize": + case "MousePointerCustomization": + _process.StartShellExecute("ms-settings:easeofaccess-mouse"); + break; + + case "EnableTouchPad": + case "TouchpadCursorSpeed": + _process.StartShellExecute("ms-settings:devices-touchpad"); + break; + + case "EnhancePointerPrecision": + HandleEnhancePointerPrecision(param); + break; + + case "SetPrimaryMouseButton": + HandleSetPrimaryMouseButton(param); + break; + + case "MouseCursorSpeed": + HandleMouseCursorSpeed(param); + break; + + case "MouseWheelScrollLines": + HandleMouseWheelScrollLines(param); + break; + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + private void HandleMouseCursorSpeed(JObject param) + { + int speed = param.Value("speedLevel") ?? 10; + _systemParams.SetParameter(SPI_SETMOUSESPEED, 0, (IntPtr)speed, SPIF_UPDATEINIFILE_SENDCHANGE); + } + + private void HandleMouseWheelScrollLines(JObject param) + { + int lines = param.Value("scrollLines") ?? 3; + _systemParams.SetParameter(SPI_SETWHEELSCROLLLINES, lines, IntPtr.Zero, SPIF_UPDATEINIFILE_SENDCHANGE); + } + + private void HandleEnhancePointerPrecision(JObject param) + { + bool enable = param.Value("enable") ?? true; + int[] mouseParams = new int[3]; + _systemParams.GetParameter(SPI_GETMOUSE, 0, mouseParams, 0); + mouseParams[2] = enable ? 1 : 0; + _systemParams.SetParameter(SPI_SETMOUSE, 0, mouseParams, SPIF_UPDATEINIFILE_SENDCHANGE); + } + + private static void HandleSetPrimaryMouseButton(JObject param) + { + string button = param.Value("primaryButton") ?? "left"; + bool leftPrimary = button.Equals("left", StringComparison.OrdinalIgnoreCase); + SwapMouseButton(leftPrimary ? 0 : 1); + } +} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/PersonalizationSettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/PersonalizationSettingsHandler.cs new file mode 100644 index 0000000000..f040a1c0f0 --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/PersonalizationSettingsHandler.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles personalization settings: title bar color, transparency, high contrast, and theme mode. +/// +internal class PersonalizationSettingsHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "ApplyColorToTitleBar", + "EnableTransparency", + "HighContrastTheme", + "SystemThemeMode", + ]; + + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public PersonalizationSettingsHandler(IRegistryService registry, IProcessService process) + { + _registry = registry; + _process = process; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + var param = JObject.Parse(value); + + switch (key) + { + case "ApplyColorToTitleBar": + HandleApplyColorToTitleBar(param); + break; + + case "EnableTransparency": + HandleEnableTransparency(param); + break; + + case "HighContrastTheme": + _process.StartShellExecute("ms-settings:easeofaccess-highcontrast"); + break; + + case "SystemThemeMode": + HandleSystemThemeMode(param); + break; + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + private void HandleApplyColorToTitleBar(JObject param) + { + bool enable = param.Value("enableColor") ?? true; + _registry.SetValue( + @"Software\Microsoft\Windows\DWM", + "ColorPrevalence", + enable ? 1 : 0, + RegistryValueKind.DWord); + } + + private void HandleEnableTransparency(JObject param) + { + bool enable = param.Value("enable") ?? true; + _registry.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + "EnableTransparency", + enable ? 1 : 0, + RegistryValueKind.DWord); + } + + private void HandleSystemThemeMode(JObject param) + { + string mode = param.Value("mode") ?? "dark"; + int value = mode.Equals("light", StringComparison.OrdinalIgnoreCase) ? 1 : 0; + + const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registry.SetValue(personalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); + _registry.SetValue(personalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); + } +} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/PowerSettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/PowerSettingsHandler.cs new file mode 100644 index 0000000000..79d843efd7 --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/PowerSettingsHandler.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles power settings: battery saver threshold and power mode (on battery / plugged in). +/// +internal class PowerSettingsHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "BatterySaverActivationLevel", + "SetPowerModeOnBattery", + "SetPowerModePluggedIn", + ]; + + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public PowerSettingsHandler(IRegistryService registry, IProcessService process) + { + _registry = registry; + _process = process; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + var param = JObject.Parse(value); + + switch (key) + { + case "BatterySaverActivationLevel": + HandleBatterySaverThreshold(param); + break; + + case "SetPowerModeOnBattery": + case "SetPowerModePluggedIn": + _process.StartShellExecute("ms-settings:powersleep"); + break; + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + private void HandleBatterySaverThreshold(JObject param) + { + int threshold = param.Value("thresholdValue") ?? 20; + _registry.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\Power\BatterySaver", + "ActivationThreshold", + threshold, + RegistryValueKind.DWord); + } +} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/PrivacySettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/PrivacySettingsHandler.cs new file mode 100644 index 0000000000..25d5c77ac0 --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/PrivacySettingsHandler.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles privacy settings: camera, location, and microphone access. +/// +internal class PrivacySettingsHandler : ICommandHandler +{ + private const string ConsentStoreBase = @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore"; + + /// + public IEnumerable SupportedCommands { get; } = + [ + "ManageCameraAccess", + "ManageLocationAccess", + "ManageMicrophoneAccess", + ]; + + private readonly IRegistryService _registry; + + public PrivacySettingsHandler(IRegistryService registry) + { + _registry = registry; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + var param = JObject.Parse(value); + + string subKey = key switch + { + "ManageCameraAccess" => "webcam", + "ManageLocationAccess" => "location", + "ManageMicrophoneAccess" => "microphone", + _ => null, + }; + + if (subKey != null) + { + SetAccessSetting(param, subKey); + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + private void SetAccessSetting(JObject param, string capability) + { + string setting = param.Value("accessSetting") ?? "Allow"; + string regValue = setting.Equals("deny", StringComparison.OrdinalIgnoreCase) ? "Deny" : "Allow"; + + _registry.SetValue( + ConsentStoreBase + @"\" + capability, + "Value", + regValue, + RegistryValueKind.String); + } +} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/SystemSettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/SystemSettingsHandler.cs new file mode 100644 index 0000000000..8da90db9c1 --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/SystemSettingsHandler.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles miscellaneous system settings: time/region, focus assist, gaming, and multi-monitor. +/// +internal class SystemSettingsHandler : ICommandHandler +{ + /// + public IEnumerable SupportedCommands { get; } = + [ + "AutomaticDSTAdjustment", + "AutomaticTimeSettingAction", + "EnableGameMode", + "EnableQuietHours", + "MinimizeWindowsOnMonitorDisconnectAction", + "RememberWindowLocations", + ]; + + private readonly IProcessService _process; + + public SystemSettingsHandler(IProcessService process) + { + _process = process; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + switch (key) + { + case "AutomaticDSTAdjustment": + HandleAutomaticDSTAdjustment(value); + break; + + case "AutomaticTimeSettingAction": + _process.StartShellExecute("ms-settings:dateandtime"); + break; + + case "EnableGameMode": + _process.StartShellExecute("ms-settings:gaming-gamemode"); + break; + + case "EnableQuietHours": + _process.StartShellExecute("ms-settings:quiethours"); + break; + + case "MinimizeWindowsOnMonitorDisconnectAction": + case "RememberWindowLocations": + _process.StartShellExecute("ms-settings:display"); + break; + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + private static void HandleAutomaticDSTAdjustment(string jsonParams) + { + var param = JObject.Parse(jsonParams); + bool enable = param.Value("enable") ?? true; + + using var key = Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"); + if (key != null) + { + key.SetValue("DynamicDaylightTimeDisabled", enable ? 0 : 1, RegistryValueKind.DWord); + } + + Debug.WriteLine($"Automatic DST adjustment {(enable ? "enabled" : "disabled")}"); + } +} diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/TaskbarSettingsHandler.cs b/dotnet/autoShell/Handlers/SettingsHandlers/TaskbarSettingsHandler.cs new file mode 100644 index 0000000000..1292b609b4 --- /dev/null +++ b/dotnet/autoShell/Handlers/SettingsHandlers/TaskbarSettingsHandler.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles taskbar settings: auto-hide, alignment, task view, widgets, badges, multi-monitor, clock. +/// +internal partial class TaskbarSettingsHandler : ICommandHandler +{ + private const string ExplorerAdvanced = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; + private const string StuckRects3 = @"Software\Microsoft\Windows\CurrentVersion\Explorer\StuckRects3"; + + [LibraryImport("user32.dll")] + private static partial IntPtr SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + /// + public IEnumerable SupportedCommands { get; } = + [ + "AutoHideTaskbar", + "DisplaySecondsInSystrayClock", + "DisplayTaskbarOnAllMonitors", + "ShowBadgesOnTaskbar", + "TaskbarAlignment", + "TaskViewVisibility", + "ToggleWidgetsButtonVisibility", + ]; + + private readonly IRegistryService _registry; + + public TaskbarSettingsHandler(IRegistryService registry) + { + _registry = registry; + } + + /// + public void Handle(string key, string value, JToken rawValue) + { + try + { + var param = JObject.Parse(value); + + switch (key) + { + case "AutoHideTaskbar": + HandleAutoHideTaskbar(param); + break; + case "TaskbarAlignment": + HandleTaskbarAlignment(param); + break; + case "TaskViewVisibility": + SetToggle(param, "visibility", "ShowTaskViewButton"); + break; + case "ToggleWidgetsButtonVisibility": + SetToggle(param, "visibility", "TaskbarDa", trueValue: "show"); + break; + case "ShowBadgesOnTaskbar": + SetToggle(param, "enableBadging", "TaskbarBadges"); + break; + case "DisplayTaskbarOnAllMonitors": + SetToggle(param, "enable", "MMTaskbarEnabled"); + break; + case "DisplaySecondsInSystrayClock": + SetToggle(param, "enable", "ShowSecondsInSystemClock"); + break; + } + + SendNotifyMessage((IntPtr)0xffff, 0x001A, IntPtr.Zero, IntPtr.Zero); + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + /// + /// Sets a DWord toggle in Explorer\Advanced. + /// For bool JSON values, true=1 false=0. + /// For string JSON values, compares against . + /// + private void SetToggle(JObject param, string jsonProperty, string registryValue, string trueValue = null) + { + int regValue; + if (trueValue != null) + { + string val = param.Value(jsonProperty) ?? ""; + regValue = val.Equals(trueValue, StringComparison.OrdinalIgnoreCase) ? 1 : 0; + } + else + { + regValue = (param.Value(jsonProperty) ?? true) ? 1 : 0; + } + + _registry.SetValue(ExplorerAdvanced, registryValue, regValue, RegistryValueKind.DWord); + } + + private void HandleAutoHideTaskbar(JObject param) + { + bool hide = param.Value("hideWhenNotUsing"); + + // Auto-hide uses a binary blob in a different registry path + var settings = _registry.GetValue(StuckRects3, "Settings", null) as byte[]; + if (settings != null && settings.Length >= 9) + { + // Bit 0 of byte 8 controls auto-hide + if (hide) + settings[8] |= 0x01; + else + settings[8] &= 0xFE; + + _registry.SetValue(StuckRects3, "Settings", settings, RegistryValueKind.Binary); + } + } + + private void HandleTaskbarAlignment(JObject param) + { + string alignment = param.Value("alignment") ?? "center"; + bool useCenter = alignment.Equals("center", StringComparison.OrdinalIgnoreCase); + _registry.SetValue(ExplorerAdvanced, "TaskbarAl", useCenter ? 1 : 0, RegistryValueKind.DWord); + } +} diff --git a/dotnet/autoShell/Handlers/SystemCommandHandler.cs b/dotnet/autoShell/Handlers/SystemCommandHandler.cs index 9bf7fce55b..e604040a6c 100644 --- a/dotnet/autoShell/Handlers/SystemCommandHandler.cs +++ b/dotnet/autoShell/Handlers/SystemCommandHandler.cs @@ -2,12 +2,14 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.Diagnostics; +using autoShell.Services; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; /// -/// Handles system/utility commands: quit, debug, toggleNotifications. +/// Handles system/utility commands: debug, toggleNotifications. /// internal class SystemCommandHandler : ICommandHandler { @@ -18,9 +20,25 @@ internal class SystemCommandHandler : ICommandHandler "ToggleNotifications", ]; + private readonly IProcessService _process; + + public SystemCommandHandler(IProcessService process) + { + _process = process; + } + /// public void Handle(string key, string value, JToken rawValue) { - AutoShell.HandleSystemCommand(key, value); + switch (key) + { + case "ToggleNotifications": + _process.StartShellExecute("ms-actioncenter:"); + break; + + case "Debug": + Debugger.Launch(); + break; + } } } diff --git a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs index bb7bbc3873..6560ae1240 100644 --- a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs +++ b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs @@ -1,17 +1,50 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; /// /// Handles theme-related commands: applyTheme, listThemes, setThemeMode, setWallpaper. -/// Delegates to existing static methods in AutoShell. +/// Contains all Windows theme management logic including discovery, application, +/// and light/dark mode toggling. /// -internal class ThemeCommandHandler : ICommandHandler +internal partial class ThemeCommandHandler : ICommandHandler { + private const int SPI_SETDESKWALLPAPER = 0x0014; + private const int SPIF_UPDATEINIFILE_SENDCHANGE = 3; + private const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002; + + [LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + private static partial IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool FreeLibrary(IntPtr hModule); + + [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)] + private static partial int LoadString(IntPtr hInstance, uint uID, [Out] char[] lpBuffer, int nBufferMax); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern IntPtr SendMessageTimeout( + IntPtr hWnd, + uint Msg, + IntPtr wParam, + string lParam, + uint fuFlags, + uint uTimeout, + out IntPtr lpdwResult); + /// public IEnumerable SupportedCommands { get; } = [ @@ -21,9 +54,383 @@ internal class ThemeCommandHandler : ICommandHandler "SetWallpaper", ]; + private readonly IRegistryService _registry; + private readonly IProcessService _process; + private readonly ISystemParametersService _systemParams; + + private string _previousTheme; + private Dictionary _themeDictionary; + private Dictionary _themeDisplayNameDictionary; + + public ThemeCommandHandler(IRegistryService registry, IProcessService process, ISystemParametersService systemParams) + { + _registry = registry; + _process = process; + _systemParams = systemParams; + + LoadThemes(); + } + /// public void Handle(string key, string value, JToken rawValue) { - AutoShell.HandleThemeCommand(key, value); + switch (key) + { + case "SetWallpaper": + _systemParams.SetParameter(SPI_SETDESKWALLPAPER, 0, value, SPIF_UPDATEINIFILE_SENDCHANGE); + break; + + case "ApplyTheme": + ApplyTheme(value); + break; + + case "ListThemes": + var themes = GetInstalledThemes(); + Console.WriteLine(JsonConvert.SerializeObject(themes)); + break; + + case "SetThemeMode": + HandleSetThemeMode(value); + break; + } + } + + private void HandleSetThemeMode(string value) + { + if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase)) + { + ToggleLightDarkMode(); + } + else if (value.Equals("light", StringComparison.OrdinalIgnoreCase)) + { + SetLightDarkMode(true); + } + else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase)) + { + SetLightDarkMode(false); + } + else if (bool.TryParse(value, out bool useLightMode)) + { + SetLightDarkMode(useLightMode); + } + } + + #region Theme Management + + private void LoadThemes() + { + _themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + _themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string[] themePaths = + [ + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Themes"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Ease of Access Themes"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Themes") + ]; + + foreach (string themesFolder in themePaths) + { + if (Directory.Exists(themesFolder)) + { + foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme")) + { + string themeName = Path.GetFileNameWithoutExtension(themeFile); + if (!_themeDictionary.ContainsKey(themeName)) + { + _themeDictionary[themeName] = themeFile; + + // Parse display name from theme file + string displayName = GetThemeDisplayName(themeFile); + if (!string.IsNullOrEmpty(displayName) && !_themeDisplayNameDictionary.ContainsKey(displayName)) + { + _themeDisplayNameDictionary[displayName] = themeName; + } + } + } + } + } + + _themeDictionary["previous"] = GetCurrentTheme(); + } + + /// + /// Parses the display name from a .theme file. + /// + private static string GetThemeDisplayName(string themeFilePath) + { + try + { + foreach (string line in File.ReadLines(themeFilePath)) + { + if (line.StartsWith("DisplayName=", StringComparison.OrdinalIgnoreCase)) + { + string displayName = line["DisplayName=".Length..].Trim(); + // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013) + if (displayName.StartsWith("@")) + { + displayName = ResolveLocalizedString(displayName); + } + return displayName; + } + } + } + catch + { + // Ignore errors reading theme file + } + return null; + } + + /// + /// Resolves a localized string resource reference. + /// + private static string ResolveLocalizedString(string localizedString) + { + try + { + // Remove the @ prefix + string resourcePath = localizedString[1..]; + // Expand environment variables + int commaIndex = resourcePath.LastIndexOf(','); + if (commaIndex > 0) + { + string dllPath = Environment.ExpandEnvironmentVariables(resourcePath[..commaIndex]); + string resourceIdStr = resourcePath[(commaIndex + 1)..]; + if (int.TryParse(resourceIdStr, out int resourceId)) + { + char[] buffer = new char[256]; + IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); + if (hModule != IntPtr.Zero) + { + try + { + int result = LoadString(hModule, (uint)Math.Abs(resourceId), buffer, buffer.Length); + if (result > 0) + { + return new string(buffer, 0, result); + } + } + finally + { + FreeLibrary(hModule); + } + } + } + } + } + catch + { + // Ignore errors resolving localized string + } + return localizedString; + } + + /// + /// Returns a list of all installed Windows themes. + /// + public List GetInstalledThemes() + { + HashSet themes = []; + + themes.UnionWith(_themeDictionary.Keys); + themes.UnionWith(_themeDisplayNameDictionary.Keys); + + return [.. themes]; + } + + /// + /// Gets the current Windows theme name. + /// + public string GetCurrentTheme() + { + try + { + const string themesPath = @"Software\Microsoft\Windows\CurrentVersion\Themes"; + string currentThemePath = _registry.GetValue(themesPath, "CurrentTheme") as string; + if (!string.IsNullOrEmpty(currentThemePath)) + { + return Path.GetFileNameWithoutExtension(currentThemePath); + } + } + catch + { + // Ignore errors reading registry + } + return null; } + + /// + /// Applies a Windows theme by name. + /// + public bool ApplyTheme(string themeName) + { + string themePath = FindThemePath(themeName); + if (string.IsNullOrEmpty(themePath)) + { + return false; + } + + try + { + string previous = GetCurrentTheme(); + + if (!themeName.Equals("previous", StringComparison.InvariantCultureIgnoreCase)) + { + _process.StartShellExecute(themePath); + _previousTheme = previous; + return true; + } + else + { + bool success = RevertToPreviousTheme(); + + if (success) + { + _previousTheme = previous; + } + + return success; + } + } + catch + { + return false; + } + } + + /// + /// Reverts to the previous Windows theme. + /// + public bool RevertToPreviousTheme() + { + if (string.IsNullOrEmpty(_previousTheme)) + { + return false; + } + + string themePath = FindThemePath(_previousTheme); + if (string.IsNullOrEmpty(themePath)) + { + return false; + } + + try + { + _process.StartShellExecute(themePath); + return true; + } + catch + { + return false; + } + } + + /// + /// Gets the name of the previous theme. + /// + public string GetPreviousTheme() + { + return _previousTheme; + } + + /// + /// Finds the full path to a theme file by name or display name. + /// + private string FindThemePath(string themeName) + { + // First check by file name + if (_themeDictionary.TryGetValue(themeName, out string themePath)) + { + return themePath; + } + + // Then check by display name + if (_themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) + { + if (_themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) + { + return themePathFromDisplay; + } + } + + return null; + } + + #endregion + + #region Light/Dark Mode + + /// + /// Sets the Windows light or dark mode by modifying registry keys. + /// + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + public bool SetLightDarkMode(bool useLightMode) + { + try + { + const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + int value = useLightMode ? 1 : 0; + + _registry.SetValue(personalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); + _registry.SetValue(personalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); + + // Broadcast settings change notification to update UI + BroadcastSettingsChange(); + + return true; + } + catch + { + return false; + } + } + + /// + /// Toggles between light and dark mode. + /// + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + public bool ToggleLightDarkMode() + { + bool? currentMode = GetCurrentLightMode(); + return currentMode.HasValue ? SetLightDarkMode(!currentMode.Value) : false; + } + + /// + /// Broadcasts a WM_SETTINGCHANGE message to notify the system of theme changes. + /// + private static void BroadcastSettingsChange() + { + const int HWND_BROADCAST = 0xffff; + const int WM_SETTINGCHANGE = 0x001A; + const uint SMTO_ABORTIFHUNG = 0x0002; + SendMessageTimeout( + (IntPtr)HWND_BROADCAST, + WM_SETTINGCHANGE, + IntPtr.Zero, + "ImmersiveColorSet", + SMTO_ABORTIFHUNG, + 1000, + out nint result); + } + + /// + /// Gets the current light/dark mode setting from the registry. + /// + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + private bool? GetCurrentLightMode() + { + try + { + const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + object value = _registry.GetValue(personalizePath, "AppsUseLightTheme"); + return value is int intValue ? intValue == 1 : null; + } + catch + { + return null; + } + } + + #endregion } diff --git a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs index d6caca3d10..31ea24e807 100644 --- a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs +++ b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs @@ -9,7 +9,6 @@ namespace autoShell.Handlers; /// /// Handles virtual desktop commands: createDesktop, switchDesktop, nextDesktop, previousDesktop, /// moveWindowToDesktop, pinWindow. -/// Delegates to existing static methods in AutoShell. /// internal class VirtualDesktopCommandHandler : ICommandHandler { @@ -27,6 +26,31 @@ internal class VirtualDesktopCommandHandler : ICommandHandler /// public void Handle(string key, string value, JToken rawValue) { - AutoShell.HandleVirtualDesktopCommand(key, value, rawValue); + switch (key) + { + case "CreateDesktop": + AutoShell.CreateDesktop(value); + break; + + case "SwitchDesktop": + AutoShell.SwitchDesktop(value); + break; + + case "NextDesktop": + AutoShell.BumpDesktopIndex(1); + break; + + case "PreviousDesktop": + AutoShell.BumpDesktopIndex(-1); + break; + + case "MoveWindowToDesktop": + AutoShell.MoveWindowToDesktop(rawValue); + break; + + case "PinWindow": + AutoShell.PinWindow(value); + break; + } } } diff --git a/dotnet/autoShell/Handlers/WindowCommandHandler.cs b/dotnet/autoShell/Handlers/WindowCommandHandler.cs index 3516e1491b..a16aa69205 100644 --- a/dotnet/autoShell/Handlers/WindowCommandHandler.cs +++ b/dotnet/autoShell/Handlers/WindowCommandHandler.cs @@ -23,6 +23,27 @@ internal class WindowCommandHandler : ICommandHandler /// public void Handle(string key, string value, JToken rawValue) { - AutoShell.HandleWindowCommand(key, value); + switch (key) + { + case "Maximize": + AutoShell.MaximizeWindow(value); + break; + + case "Minimize": + AutoShell.MinimizeWindow(value); + break; + + case "SwitchTo": + AutoShell.RaiseWindow(value); + break; + + case "Tile": + string[] apps = value.Split(','); + if (apps.Length == 2) + { + AutoShell.TileWindowPair(apps[0], apps[1]); + } + break; + } } } diff --git a/dotnet/autoShell/Services/ISystemParametersService.cs b/dotnet/autoShell/Services/ISystemParametersService.cs index 261d8f8c05..f414204792 100644 --- a/dotnet/autoShell/Services/ISystemParametersService.cs +++ b/dotnet/autoShell/Services/ISystemParametersService.cs @@ -28,6 +28,15 @@ internal interface ISystemParametersService /// Flags controlling persistence and notification (SPIF_*). bool SetParameter(int action, int param, string vparam, int flags); + /// + /// Sets a system parameter via SystemParametersInfo with an int array value. + /// + /// The system parameter action constant (SPI_SET*). + /// Additional parameter whose meaning depends on the action. + /// Array containing the value to set. + /// Flags controlling persistence and notification (SPIF_*). + bool SetParameter(int action, int param, int[] vparam, int flags); + /// /// Gets a system parameter via SystemParametersInfo into an int array. /// diff --git a/dotnet/autoShell/Services/WindowsSystemParametersService.cs b/dotnet/autoShell/Services/WindowsSystemParametersService.cs index 08ea001840..bf039d4a13 100644 --- a/dotnet/autoShell/Services/WindowsSystemParametersService.cs +++ b/dotnet/autoShell/Services/WindowsSystemParametersService.cs @@ -37,6 +37,12 @@ public bool SetParameter(int action, int param, string vparam, int flags) return SystemParametersInfo(action, param, vparam, flags) != 0; } + /// + public bool SetParameter(int action, int param, int[] vparam, int flags) + { + return SystemParametersInfo(action, param, vparam, flags); + } + /// public bool GetParameter(int action, int param, int[] vparam, int flags) { From de17572764c5a185b01ac4237295beacb203caac Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 1 Apr 2026 20:17:51 -0700 Subject: [PATCH 09/27] All logic moved to handlers --- .../HandlerRegistrationTests.cs | 7 +- dotnet/autoShell/AutoShell.cs | 1290 +---------------- dotnet/autoShell/AutoShell_Win32.cs | 525 ------- .../autoShell/Handlers/AppCommandHandler.cs | 96 +- .../Handlers/DisplayCommandHandler.cs | 268 +++- .../Handlers/NetworkCommandHandler.cs | 511 ++++++- .../Handlers/VirtualDesktopCommandHandler.cs | 538 ++++++- .../Handlers/WindowCommandHandler.cs | 301 +++- dotnet/autoShell/Services/IAppRegistry.cs | 47 + .../autoShell/Services/WindowsAppRegistry.cs | 138 ++ dotnet/autoShell/UIAutomation.cs | 4 +- 11 files changed, 1890 insertions(+), 1835 deletions(-) delete mode 100644 dotnet/autoShell/AutoShell_Win32.cs create mode 100644 dotnet/autoShell/Services/IAppRegistry.cs create mode 100644 dotnet/autoShell/Services/WindowsAppRegistry.cs diff --git a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs index 3d42217ccd..125869c024 100644 --- a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs +++ b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs @@ -19,14 +19,15 @@ public HandlerRegistrationTests() var registryMock = new Moq.Mock(); var systemParamsMock = new Moq.Mock(); var processMock = new Moq.Mock(); + var appRegistryMock = new Moq.Mock(); _handlers = [ new AudioCommandHandler(audioMock.Object), - new AppCommandHandler(), - new WindowCommandHandler(), + new AppCommandHandler(appRegistryMock.Object, processMock.Object), + new WindowCommandHandler(appRegistryMock.Object), new ThemeCommandHandler(registryMock.Object, processMock.Object, systemParamsMock.Object), - new VirtualDesktopCommandHandler(), + new VirtualDesktopCommandHandler(appRegistryMock.Object), new NetworkCommandHandler(), new DisplayCommandHandler(), new TaskbarSettingsHandler(registryMock.Object), diff --git a/dotnet/autoShell/AutoShell.cs b/dotnet/autoShell/AutoShell.cs index 18b6f32e88..af333d5e42 100644 --- a/dotnet/autoShell/AutoShell.cs +++ b/dotnet/autoShell/AutoShell.cs @@ -2,36 +2,21 @@ // Licensed under the MIT License. using System; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; using autoShell.Handlers; using autoShell.Services; -using Microsoft.VisualBasic; -using Microsoft.WindowsAPICodePack.Shell; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace autoShell; -internal partial class AutoShell +internal class AutoShell { - // create a map of friendly names to executable paths - private static Hashtable s_friendlyNameToPath = []; - private static Hashtable s_friendlyNameToId = []; - private static SortedList s_sortedList; - - private static IServiceProvider10 s_shell; - private static IVirtualDesktopManager s_virtualDesktopManager; - private static IVirtualDesktopManagerInternal s_virtualDesktopManagerInternal; - private static IVirtualDesktopManagerInternal_BUGBUG s_virtualDesktopManagerInternal_BUGBUG; - private static IApplicationViewCollection s_applicationViewCollection; - private static IVirtualDesktopPinnedApps s_virtualDesktopPinnedApps; + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern IntPtr GetCommandLineW(); private static CommandDispatcher s_dispatcher; @@ -41,72 +26,20 @@ internal partial class AutoShell /// static AutoShell() { - // get current user name - string userName = Environment.UserName; - // known appication, path to executable, any env var for working directory - s_sortedList = new SortedList - { - { "chrome", new[] { "chrome.exe" } }, - { "power point", new[] { "C:\\Program Files\\Microsoft Office\\root\\Office16\\POWERPNT.EXE" } }, - { "powerpoint", new[] { "C:\\Program Files\\Microsoft Office\\root\\Office16\\POWERPNT.EXE" } }, - { "word", new[] { "C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE" } }, - { "winword", new[] { "C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE" } }, - { "excel", new[] { "C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE" } }, - { "outlook", new[] { "C:\\Program Files\\Microsoft Office\\root\\Office16\\OUTLOOK.EXE" } }, - { "visual studio", new[] { "devenv.exe" } }, - { "visual studio code", new[] { "C:\\Users\\" + userName + "\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe" } }, - { "edge", new[] { "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" } }, - { "microsoft edge", new[] { "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" } }, - { "notepad", new[] { "C:\\Windows\\System32\\notepad.exe" } }, - { "paint", new[] { "mspaint.exe" } }, - { "calculator", new[] { "calc.exe" } }, - { "file explorer", new[] { "C:\\Windows\\explorer.exe" } }, - { "control panel", new[] { "C:\\Windows\\System32\\control.exe" } }, - { "task manager", new[] { "C:\\Windows\\System32\\Taskmgr.exe" } }, - { "cmd", new[] { "C:\\Windows\\System32\\cmd.exe" } }, - { "powershell", new[] { "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" } }, - { "snipping tool", new[] { "C:\\Windows\\System32\\SnippingTool.exe" } }, - { "magnifier", new[] { "C:\\Windows\\System32\\Magnify.exe" } }, - { "paint 3d", new[] { "C:\\Program Files\\WindowsApps\\Microsoft.MSPaint_10.1807.18022.0_x64__8wekyb3d8bbwe\\" } }, - { "m365 copilot", new[] { "C:\\Program Files\\WindowsApps\\Microsoft.MicrosoftOfficeHub_19.2512.45041.0_x64__8wekyb3d8bbwe\\M365Copilot.exe" } }, - { "copilot", new[] { "C:\\Program Files\\WindowsApps\\Microsoft.MicrosoftOfficeHub_19.2512.45041.0_x64__8wekyb3d8bbwe\\M365Copilot.exe" } }, - { "spotify", new[] { "C:\\Program Files\\WindowsApps\\SpotifyAB.SpotifyMusic_1.278.418.0_x64__zpdnekdrzrea0\\spotify.exe" } }, - { "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" } }, - }; - - // add the entries to the hashtable - foreach (var kvp in s_sortedList) - { - s_friendlyNameToPath.Add(kvp.Key, kvp.Value[0]); - } - - var installedApps = GetAllInstalledAppsIds(); - foreach (var kvp in installedApps) - { - s_friendlyNameToId.Add(kvp.Key, kvp.Value); - } - - // Desktop management - s_shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ImmersiveShell)); - s_virtualDesktopManagerInternal = (IVirtualDesktopManagerInternal)s_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); - s_virtualDesktopManagerInternal_BUGBUG = (IVirtualDesktopManagerInternal_BUGBUG)s_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); - s_virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_VirtualDesktopManager)); - s_applicationViewCollection = (IApplicationViewCollection)s_shell.QueryService(typeof(IApplicationViewCollection).GUID, typeof(IApplicationViewCollection).GUID); - s_virtualDesktopPinnedApps = (IVirtualDesktopPinnedApps)s_shell.QueryService(CLSID_VirtualDesktopPinnedApps, typeof(IVirtualDesktopPinnedApps).GUID); - // Initialize command dispatcher with all handlers var registry = new WindowsRegistryService(); var systemParams = new WindowsSystemParametersService(); var process = new WindowsProcessService(); var audio = new WindowsAudioService(); + var appRegistry = new WindowsAppRegistry(); s_dispatcher = new CommandDispatcher(); s_dispatcher.Register( new AudioCommandHandler(audio), - new AppCommandHandler(), - new WindowCommandHandler(), + new AppCommandHandler(appRegistry, process), + new WindowCommandHandler(appRegistry), new ThemeCommandHandler(registry, process, systemParams), - new VirtualDesktopCommandHandler(), + new VirtualDesktopCommandHandler(appRegistry), new NetworkCommandHandler(), new DisplayCommandHandler(), new TaskbarSettingsHandler(registry), @@ -213,1217 +146,8 @@ internal static void LogWarning(string message) Console.ForegroundColor = previousColor; } - internal static SortedList GetAllInstalledAppsIds() - { - // GUID taken from https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid - var FOLDERID_AppsFolder = new Guid("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}"); - ShellObject appsFolder = (ShellObject)KnownFolderHelper.FromKnownFolderId(FOLDERID_AppsFolder); - var appIds = new SortedList(); - - foreach (var app in (IKnownFolder)appsFolder) - { - string appName = app.Name.ToLowerInvariant(); - if (appIds.ContainsKey(appName)) - { - Debug.WriteLine("Key has multiple values: " + appName); - } - else - { - // The ParsingName property is the AppUserModelID - appIds.Add(appName, app.ParsingName); - } - } - - return appIds; - } - - private static string ResolveProcessNameFromFriendlyName(string friendlyName) - { - string path = (string)s_friendlyNameToPath[friendlyName.ToLowerInvariant()]; - return path != null ? Path.GetFileNameWithoutExtension(path) : friendlyName; - } - - private static IntPtr FindProcessWindowHandle(string processName) - { - processName = ResolveProcessNameFromFriendlyName(processName); - Process[] processes = Process.GetProcessesByName(processName); - // loop through the processes that match the name; raise the first one that has a main window - foreach (Process p in processes) - { - if (p.MainWindowHandle != IntPtr.Zero) - { - return p.MainWindowHandle; - } - } - - // Try to find by window title if we haven't found it and bring it forward - return FindWindowByTitle(processName).hWnd; - } - - // given part of a process name, raise the window of that process to the top level - internal static void RaiseWindow(string processName) - { - processName = ResolveProcessNameFromFriendlyName(processName); - Process[] processes = Process.GetProcessesByName(processName); - // loop through the processes that match the name; raise the first one that has a main window - foreach (Process p in processes) - { - if (p.MainWindowHandle != IntPtr.Zero) - { - SetForegroundWindow(p.MainWindowHandle); - Interaction.AppActivate(p.Id); - return; - } - } - - // this means all the applications processes are running in the background. This happens for edge and chrome browsers. - string path = (string)s_friendlyNameToPath[processName]; - if (path != null) - { - Process.Start(path); - } - else - { - // Try to find by window title if we haven't found it and bring it forward - (nint hWnd1, int pid) = FindWindowByTitle(processName); - - if (hWnd1 != nint.Zero) - { - SetForegroundWindow(hWnd1); - Interaction.AppActivate(pid); - } - } - } - - internal static void MaximizeWindow(string processName) - { - processName = ResolveProcessNameFromFriendlyName(processName); - Process[] processes = Process.GetProcessesByName(processName); - // loop through the processes that match the name; raise the first one that has a main window - foreach (Process p in processes) - { - if (p.MainWindowHandle != IntPtr.Zero) - { - uint WM_SYSCOMMAND = 0x112; - uint SC_MAXIMIZE = 0xf030; - SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero); - SetForegroundWindow(p.MainWindowHandle); - Interaction.AppActivate(p.Id); - return; - } - } - - // if we haven't found what we are looking for let's enumerate the top level windows and try that way - (nint hWnd, int pid) = FindWindowByTitle(processName); - if (hWnd != nint.Zero) - { - uint WM_SYSCOMMAND = 0x112; - uint SC_MAXIMIZE = 0xf030; - SendMessage(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero); - SetForegroundWindow(hWnd); - Interaction.AppActivate(pid); - } - } - - internal static void MinimizeWindow(string processName) - { - processName = ResolveProcessNameFromFriendlyName(processName); - Process[] processes = Process.GetProcessesByName(processName); - // loop through the processes that match the name; raise the first one that has a main window - foreach (Process p in processes) - { - if (p.MainWindowHandle != IntPtr.Zero) - { - uint WM_SYSCOMMAND = 0x112; - uint SC_MINIMIZE = 0xF020; - SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero); - break; - } - } - - // if we haven't found what we are looking for let's enumerate the top level windows and try that way - (nint hWnd, int pid) = FindWindowByTitle(processName); - if (hWnd != nint.Zero) - { - uint WM_SYSCOMMAND = 0x112; - uint SC_MINIMIZE = 0xF020; - SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero); - SetForegroundWindow(hWnd); - Interaction.AppActivate(pid); - } - } - - internal static void TileWindowPair(string processName1, string processName2) - { - // find both processes - // TODO: Update this to account for UWP apps (e.g. calculator). UWPs are hosted by ApplicationFrameHost.exe - processName1 = ResolveProcessNameFromFriendlyName(processName1); - Process[] processes1 = Process.GetProcessesByName(processName1); - IntPtr hWnd1 = IntPtr.Zero; - IntPtr hWnd2 = IntPtr.Zero; - int pid1 = -1; - int pid2 = -1; - - foreach (Process p in processes1) - { - if (p.MainWindowHandle != IntPtr.Zero) - { - hWnd1 = p.MainWindowHandle; - pid1 = p.Id; - break; - } - } - - // If no process found by name, search by window title - if (hWnd1 == IntPtr.Zero) - { - (hWnd1, pid1) = FindWindowByTitle(processName1); - } - - processName2 = ResolveProcessNameFromFriendlyName(processName2); - Process[] processes2 = Process.GetProcessesByName(processName2); - foreach (Process p in processes2) - { - if (p.MainWindowHandle != IntPtr.Zero) - { - hWnd2 = p.MainWindowHandle; - pid2 = p.Id; - break; - } - } - - // If no process found by name, search by window title - if (hWnd2 == IntPtr.Zero) - { - (hWnd2, pid2) = FindWindowByTitle(processName2); - } - - if (hWnd1 != IntPtr.Zero && hWnd2 != IntPtr.Zero) - { - // TODO: handle multiple monitors - // get the screen size - IntPtr desktopHandle = GetDesktopWindow(); - RECT desktopRect = new RECT(); - GetWindowRect(desktopHandle, ref desktopRect); - // get the dimensions of the taskbar - // find the taskbar window - IntPtr taskbarHandle = IntPtr.Zero; - IntPtr hWnd = IntPtr.Zero; - while ((hWnd = FindWindowEx(IntPtr.Zero, hWnd, "Shell_TrayWnd", null)) != IntPtr.Zero) - { - // find the taskbar window's child - taskbarHandle = FindWindowEx(hWnd, IntPtr.Zero, "ReBarWindow32", null); - if (taskbarHandle != IntPtr.Zero) - { - break; - } - } - if (hWnd == IntPtr.Zero) - { - Debug.WriteLine("Taskbar not found"); - return; - } - else - { - RECT taskbarRect = new RECT(); - GetWindowRect(hWnd, ref taskbarRect); - Debug.WriteLine("Taskbar Rect: " + taskbarRect.Left + ", " + taskbarRect.Top + ", " + taskbarRect.Right + ", " + taskbarRect.Bottom); - // TODO: handle left, top, right and nonexistant taskbars - // subtract the taskbar height from the screen height - desktopRect.Bottom -= (int)((taskbarRect.Bottom - taskbarRect.Top) / 2); - } - // set the window positions using the shellRect and making sure the windows are visible - int halfwidth = (desktopRect.Right - desktopRect.Left) / 2; - int height = desktopRect.Bottom - desktopRect.Top; - IntPtr HWND_TOP = IntPtr.Zero; - uint showWindow = 0x40; - - // Restore windows first (in case they're maximized - SetWindowPos won't work on maximized windows) - uint SW_RESTORE = 9; - ShowWindow(hWnd1, SW_RESTORE); - ShowWindow(hWnd2, SW_RESTORE); - - SetWindowPos(hWnd1, HWND_TOP, desktopRect.Left, desktopRect.Top, halfwidth, height, showWindow); - SetForegroundWindow(hWnd1); - Interaction.AppActivate(pid1); - SetWindowPos(hWnd2, HWND_TOP, desktopRect.Left + halfwidth, desktopRect.Top, halfwidth, height, showWindow); - SetForegroundWindow(hWnd2); - Interaction.AppActivate(pid2); - } - } - - /// - /// Finds a top-level window by searching for a partial match in the window title. - /// - /// The text to search for in window titles (case-insensitive). - /// A tuple containing the window handle and process ID, or (IntPtr.Zero, -1) if not found. - private static (IntPtr hWnd, int pid) FindWindowByTitle(string titleSearch) - { - IntPtr foundHandle = IntPtr.Zero; - int foundPid = -1; - StringBuilder windowTitle = new StringBuilder(256); - - EnumWindows((hWnd, lParam) => - { - // Only consider visible windows - if (!IsWindowVisible(hWnd)) - { - return true; // Continue enumeration - } - - // Get window title - int length = GetWindowText(hWnd, windowTitle, windowTitle.Capacity); - if (length > 0) - { - string title = windowTitle.ToString(); - // Case-insensitive partial match - if (title.Contains(titleSearch, StringComparison.OrdinalIgnoreCase)) - { - foundHandle = hWnd; - GetWindowThreadProcessId(hWnd, out uint pid); - foundPid = (int)pid; - return false; // Stop enumeration - } - } - return true; // Continue enumeration - }, IntPtr.Zero); - - return (foundHandle, foundPid); - } - - // given a friendly name, check if it's running and if not, start it; if it's running raise it to the top level - internal static void OpenApplication(string friendlyName) - { - // check to see if the application is running - Process[] processes = Process.GetProcessesByName(friendlyName); - if (processes.Length == 0) - { - // if not, start it - Debug.WriteLine("Starting " + friendlyName); - string path = (string)s_friendlyNameToPath[friendlyName.ToLowerInvariant()]; - if (path != null) - { - ProcessStartInfo psi = new ProcessStartInfo() - { - FileName = path, - UseShellExecute = true - }; - - // do we have a specific startup directory for this application? - if (s_sortedList.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 1) - { - psi.WorkingDirectory = Environment.ExpandEnvironmentVariables("%" + value[1] + "%") ?? string.Empty; - } - - // do we have any specific command line arguments for this application? - if (s_sortedList.TryGetValue(friendlyName.ToLowerInvariant(), out string[] args) && value.Length > 2) - { - psi.Arguments = string.Join(" ", args.Skip(2)); - } - - try - { - Process.Start(psi); - } - catch (System.ComponentModel.Win32Exception) - { - psi.FileName = friendlyName; - - // alternate start method - Process.Start(psi); - } - } - else - { - string appModelUserID = (string)s_friendlyNameToId[friendlyName.ToLowerInvariant()]; - if (appModelUserID != null) - { - try - { - Process.Start("explorer.exe", @" shell:appsFolder\" + appModelUserID); - } - catch { } - } - } - } - else - { - // if so, raise it to the top level - Debug.WriteLine("Raising " + friendlyName); - RaiseWindow(friendlyName); - } - } - - // close application - internal static void CloseApplication(string friendlyName) - { - // check to see if the application is running - string processName = ResolveProcessNameFromFriendlyName(friendlyName); - Process[] processes = Process.GetProcessesByName(processName); - if (processes.Length != 0) - { - // if so, close it - Debug.WriteLine("Closing " + friendlyName); - foreach (Process p in processes) - { - if (p.MainWindowHandle != IntPtr.Zero) - { - p.CloseMainWindow(); - } - } - } - } - - /// - /// Creates virtual desktops from a JSON array of desktop names. - /// - /// JSON array containing desktop names, e.g., ["Work", "Personal", "Gaming"] - internal static void CreateDesktop(string jsonValue) - { - try - { - // Parse the JSON array of desktop names - JArray desktopNames = JArray.Parse(jsonValue); - - if (desktopNames == null || desktopNames.Count == 0) - { - desktopNames = ["desktop X"]; - } - - if (s_virtualDesktopManagerInternal == null) - { - Debug.WriteLine($"Failed to get Virtual Desktop Manager Internal"); - return; - } - - foreach (JToken desktopNameToken in desktopNames) - { - string desktopName = desktopNameToken.ToString(); - - - try - { - // Create a new virtual desktop - IVirtualDesktop newDesktop = s_virtualDesktopManagerInternal.CreateDesktop(); - - if (newDesktop != null) - { - // Set the desktop name (Windows 10 build 20231+ / Windows 11) - try - { - // TODO: debug & get working - // Works in .NET framework but not .NET - //s_virtualDesktopManagerInternal_BUGBUG.SetDesktopName(newDesktop, desktopName); - //Debug.WriteLine($"Created virtual desktop: {desktopName}"); - } - catch (Exception ex2) - { - // Older Windows version - name setting not supported - Debug.WriteLine($"Created virtual desktop (naming not supported on this Windows version): {ex2.Message}"); - } - } - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to create desktop '{desktopName}': {ex.Message}"); - } - } - } - catch (JsonException ex) - { - Debug.WriteLine($"Failed to parse desktop names JSON: {ex.Message}"); - } - catch (Exception ex) - { - Debug.WriteLine($"Error creating desktops: {ex.Message}"); - } - } - - internal static void SwitchDesktop(string desktopIdentifier) - { - if (!int.TryParse(desktopIdentifier, out int index)) - { - // Try to find the desktop by name - s_virtualDesktopManagerInternal.SwitchDesktop(FindDesktopByName(desktopIdentifier)); - } - else - { - SwitchDesktop(index); - } - } - - private static void SwitchDesktop(int index) - { - s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); - desktops.GetAt(index, typeof(IVirtualDesktop).GUID, out object od); - - // BUGBUG: different windows versions use different COM interfaces - // Different Windows versions use different COM interfaces for desktop switching - // Windows 11 22H2 (build 22621) and later use the updated interface - if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22621)) - { - // Use the BUGBUG interface for Windows 11 22H2+ - s_virtualDesktopManagerInternal_BUGBUG.SwitchDesktopWithAnimation((IVirtualDesktop)od); - } - else if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) - { - // Windows 11 21H2 (build 22000) - s_virtualDesktopManagerInternal.SwitchDesktopWithAnimation((IVirtualDesktop)od); - } - else - { - // Windows 10 - use the original interface - s_virtualDesktopManagerInternal.SwitchDesktopAndMoveForegroundView((IVirtualDesktop)od); - } - - Marshal.ReleaseComObject(desktops); - } - - internal static void BumpDesktopIndex(int bump) - { - IVirtualDesktop desktop = s_virtualDesktopManagerInternal.GetCurrentDesktop(); - int index = GetDesktopIndex(desktop); - int count = s_virtualDesktopManagerInternal.GetCount(); - - if (index == -1) - { - Debug.WriteLine("Undable to get the index of the current desktop"); - return; - } - - index += bump; - - if (index > count) - { - index = 0; - } - else if (index < 0) - { - index = count - 1; - } - - SwitchDesktop(index); - } - - private static IVirtualDesktop FindDesktopByName(string name) - { - int count = s_virtualDesktopManagerInternal.GetCount(); - - s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); - for (int i = 0; i < count; i++) - { - desktops.GetAt(i, typeof(IVirtualDesktop).GUID, out object od); - - if (string.Equals(((IVirtualDesktop)od).GetName(), name, StringComparison.OrdinalIgnoreCase)) - { - Marshal.ReleaseComObject(desktops); - return (IVirtualDesktop)od; - } - } - - Marshal.ReleaseComObject(desktops); - - return null; - } - - private static int GetDesktopIndex(IVirtualDesktop desktop) - { - int index = -1; - int count = s_virtualDesktopManagerInternal.GetCount(); - - s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); - for (int i = 0; i < count; i++) - { - desktops.GetAt(i, typeof(IVirtualDesktop).GUID, out object od); - - if (desktop.GetId() == ((IVirtualDesktop)od).GetId()) - { - Marshal.ReleaseComObject(desktops); - return i; - } - } - - Marshal.ReleaseComObject(desktops); - - return -1; - } - - /// - /// - /// - /// - /// Currently not working correction, returns ACCESS_DENIED // TODO: investigate - internal static void MoveWindowToDesktop(JToken value) - { - string process = value.SelectToken("process").ToString(); - string desktop = value.SelectToken("desktop").ToString(); - if (string.IsNullOrEmpty(process)) - { - Debug.WriteLine("No process name supplied"); - return; - } - - if (string.IsNullOrEmpty(desktop)) - { - Debug.WriteLine("No desktop id supplied"); - return; - } - - IntPtr hWnd = FindProcessWindowHandle(process); - - if (int.TryParse(desktop, out int desktopIndex)) - { - s_virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); - if (desktopIndex < 1 || desktopIndex > s_virtualDesktopManagerInternal.GetCount()) - { - Debug.WriteLine("Desktop index out of range"); - Marshal.ReleaseComObject(desktops); - return; - } - desktops.GetAt(desktopIndex - 1, typeof(IVirtualDesktop).GUID, out object od); - Guid g = ((IVirtualDesktop)od).GetId(); - s_virtualDesktopManager.MoveWindowToDesktop(hWnd, ref g); - Marshal.ReleaseComObject(desktops); - return; - } - - IVirtualDesktop ivd = FindDesktopByName(desktop); - if (ivd is not null) - { - Guid desktopGuid = ivd.GetId(); - s_virtualDesktopManager.MoveWindowToDesktop(hWnd, ref desktopGuid); - } - } - - internal static void PinWindow(string processName) - { - IntPtr hWnd = FindProcessWindowHandle(processName); - - if (hWnd != IntPtr.Zero) - { - s_applicationViewCollection.GetViewForHwnd(hWnd, out IApplicationView view); - - if (view is not null) - { - s_virtualDesktopPinnedApps.PinView((IApplicationView)view); - } - } - else - { - Console.WriteLine($"The window handle for '{processName}' could not be found"); - } - } - - static IVirtualDesktopManagerInternal GetVirtualDesktopManagerInternal() - { - try - { - IServiceProvider shellServiceProvider = (IServiceProvider)Activator.CreateInstance( - Type.GetTypeFromCLSID(CLSID_ImmersiveShell)); - - shellServiceProvider.QueryService( - CLSID_VirtualDesktopManagerInternal, - typeof(IVirtualDesktopManagerInternal).GUID, - out object objVirtualDesktopManagerInternal); - - return (IVirtualDesktopManagerInternal)objVirtualDesktopManagerInternal; - } - catch - { - return null; - } - } private static bool execLine(JObject root) => s_dispatcher.Dispatch(root); - /// - /// Sets the airplane mode state using the Radio Management API. - /// - /// True to enable airplane mode, false to disable. - internal static void SetAirplaneMode(bool enable) - { - IRadioManager radioManager = null; - try - { - // Create the Radio Management API COM object - Type radioManagerType = Type.GetTypeFromCLSID(CLSID_RadioManagementAPI); - if (radioManagerType == null) - { - Debug.WriteLine("Failed to get Radio Management API type"); - return; - } - - object obj = Activator.CreateInstance(radioManagerType); - radioManager = (IRadioManager)obj; - - if (radioManager == null) - { - Debug.WriteLine("Failed to create Radio Manager instance"); - return; - } - - // Get current state (for logging) - int hr = radioManager.GetSystemRadioState(out int currentState, out int _, out int _); - if (hr < 0) - { - Debug.WriteLine($"Failed to get system radio state: HRESULT 0x{hr:X8}"); - return; - } - - // currentState: 0 = airplane mode ON (radios off), 1 = airplane mode OFF (radios on) - bool airplaneModeCurrentlyOn = currentState == 0; - Debug.WriteLine($"Current airplane mode state: {(airplaneModeCurrentlyOn ? "on" : "off")}"); - - // Set the new state - // bEnabled: 0 = turn airplane mode ON (disable radios), 1 = turn airplane mode OFF (enable radios) - int newState = enable ? 0 : 1; - hr = radioManager.SetSystemRadioState(newState); - if (hr < 0) - { - Debug.WriteLine($"Failed to set system radio state: HRESULT 0x{hr:X8}"); - return; - } - - Debug.WriteLine($"Airplane mode set to: {(enable ? "on" : "off")}"); - } - catch (COMException ex) - { - Debug.WriteLine($"COM Exception setting airplane mode: {ex.Message} (HRESULT: 0x{ex.HResult:X8})"); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to set airplane mode: {ex.Message}"); - } - finally - { - if (radioManager != null) - { - Marshal.ReleaseComObject(radioManager); - } - } - } - - /// - /// Lists all WiFi networks currently in range. - /// - internal static void ListWifiNetworks() - { - IntPtr clientHandle = IntPtr.Zero; - IntPtr wlanInterfaceList = IntPtr.Zero; - IntPtr networkList = IntPtr.Zero; - - try - { - // Open WLAN handle - int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); - if (result != 0) - { - Debug.WriteLine($"Failed to open WLAN handle: {result}"); - return; - } - - // Enumerate wireless interfaces - result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); - if (result != 0) - { - Debug.WriteLine($"Failed to enumerate WLAN interfaces: {result}"); - return; - } - - WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); - - if (interfaceList.dwNumberOfItems == 0) - { - Console.WriteLine("[]"); - return; - } - - var allNetworks = new List(); - - for (int i = 0; i < interfaceList.dwNumberOfItems; i++) - { - WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i]; - - // Scan for networks (trigger a refresh) - WlanScan(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - - // Small delay to allow scan to complete - System.Threading.Thread.Sleep(100); - - // Get available networks - result = WlanGetAvailableNetworkList(clientHandle, ref interfaceInfo.InterfaceGuid, 0, IntPtr.Zero, out networkList); - if (result != 0) - { - Debug.WriteLine($"Failed to get network list: {result}"); - continue; - } - - WLAN_AVAILABLE_NETWORK_LIST availableNetworkList = Marshal.PtrToStructure(networkList); - - IntPtr networkPtr = networkList + 8; // Skip dwNumberOfItems and dwIndex - - for (int j = 0; j < availableNetworkList.dwNumberOfItems; j++) - { - WLAN_AVAILABLE_NETWORK network = Marshal.PtrToStructure(networkPtr); - - string ssid = Encoding.ASCII.GetString(network.dot11Ssid.SSID, 0, (int)network.dot11Ssid.SSIDLength); - - if (!string.IsNullOrEmpty(ssid)) - { - allNetworks.Add(new - { - SSID = ssid, - SignalQuality = network.wlanSignalQuality, - Secured = network.bSecurityEnabled, - Connected = (network.dwFlags & 1) != 0 // WLAN_AVAILABLE_NETWORK_CONNECTED - }); - } - - networkPtr += Marshal.SizeOf(); - } - - if (networkList != IntPtr.Zero) - { - WlanFreeMemory(networkList); - networkList = IntPtr.Zero; - } - } - - // Remove duplicates and sort by signal strength - var uniqueNetworks = allNetworks - .GroupBy(n => ((dynamic)n).SSID) - .Select(g => g.OrderByDescending(n => ((dynamic)n).SignalQuality).First()) - .OrderByDescending(n => ((dynamic)n).SignalQuality) - .ToList(); - - Console.WriteLine(JsonConvert.SerializeObject(uniqueNetworks)); - } - catch (Exception ex) - { - Debug.WriteLine($"Error listing WiFi networks: {ex.Message}"); - Console.WriteLine("[]"); - } - finally - { - if (networkList != IntPtr.Zero) - { - WlanFreeMemory(networkList); - } - - if (wlanInterfaceList != IntPtr.Zero) - { - WlanFreeMemory(wlanInterfaceList); - } - - if (clientHandle != IntPtr.Zero) - { - WlanCloseHandle(clientHandle, IntPtr.Zero); - } - } - } - - /// - /// Connects to a WiFi network by name (SSID). If the network requires a password and one is provided, - /// it will create a temporary profile. For networks with existing profiles, it connects using the profile. - /// - /// The SSID of the network to connect to. - /// Optional password for secured networks. - internal static void ConnectToWifi(string ssid, string password = null) - { - IntPtr clientHandle = IntPtr.Zero; - IntPtr wlanInterfaceList = IntPtr.Zero; - - try - { - // Open WLAN handle - int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); - if (result != 0) - { - LogWarning($"Failed to open WLAN handle: {result}"); - return; - } - - // Enumerate wireless interfaces - result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); - if (result != 0) - { - LogWarning($"Failed to enumerate WLAN interfaces: {result}"); - return; - } - - WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); - - if (interfaceList.dwNumberOfItems == 0) - { - LogWarning("No wireless interfaces found."); - return; - } - - // Use the first available wireless interface - WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[0]; - - // If password is provided, create a profile and connect - if (!string.IsNullOrEmpty(password)) - { - string profileXml = GenerateWifiProfileXml(ssid, password); - - result = WlanSetProfile(clientHandle, ref interfaceInfo.InterfaceGuid, 0, profileXml, null, true, IntPtr.Zero, out uint reasonCode); - if (result != 0) - { - LogWarning($"Failed to set WiFi profile: {result}, reason: {reasonCode}"); - return; - } - } - - // Set up connection parameters - WLAN_CONNECTION_PARAMETERS connectionParams = new WLAN_CONNECTION_PARAMETERS - { - wlanConnectionMode = WLAN_CONNECTION_MODE.wlan_connection_mode_profile, - strProfile = ssid, - pDot11Ssid = IntPtr.Zero, - pDesiredBssidList = IntPtr.Zero, - dot11BssType = DOT11_BSS_TYPE.dot11_BSS_type_any, - dwFlags = 0 - }; - - result = WlanConnect(clientHandle, ref interfaceInfo.InterfaceGuid, ref connectionParams, IntPtr.Zero); - if (result != 0) - { - LogWarning($"Failed to connect to WiFi network '{ssid}': {result}"); - return; - } - - Debug.WriteLine($"Successfully initiated connection to WiFi network: {ssid}"); - Console.WriteLine($"Connecting to WiFi network: {ssid}"); - } - catch (Exception ex) - { - LogError(ex); - } - finally - { - if (wlanInterfaceList != IntPtr.Zero) - { - WlanFreeMemory(wlanInterfaceList); - } - - if (clientHandle != IntPtr.Zero) - { - WlanCloseHandle(clientHandle, IntPtr.Zero); - } - } - } - - /// - /// Generates a WiFi profile XML for WPA2-Personal (PSK) networks. - /// - private static string GenerateWifiProfileXml(string ssid, string password) - { - // Convert SSID to hex - string ssidHex = BitConverter.ToString(Encoding.UTF8.GetBytes(ssid)).Replace("-", ""); - - return $@" - - {ssid} - - - {ssidHex} - {ssid} - - - ESS - auto - - - - WPA2PSK - AES - false - - - passPhrase - false - {password} - - - -"; - } - - /// - /// Disconnects from the currently connected WiFi network. - /// - /// - /// Sets the system text scaling factor (percentage). - /// - /// The text scaling percentage (100-225). - internal static void SetTextSize(int percentage) - { - try - { - if (percentage == -1) - { - percentage = new Random().Next(100, 225 + 1); - } - - // Clamp the percentage to valid range - if (percentage < 100) - { - percentage = 100; - } - else if (percentage > 225) - { - percentage = 225; - } - - // Open the Settings app to the ease of access page - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:easeofaccess", - UseShellExecute = true - }); - - // Use UI Automation to navigate and set the text size - UIAutomation.SetTextSizeViaUIAutomation(percentage); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Lists all available display resolutions for the primary monitor. - /// - internal static void ListDisplayResolutions() - { - try - { - var resolutions = new List(); - DEVMODE devMode = new DEVMODE(); - devMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE)); - - int modeNum = 0; - while (EnumDisplaySettings(null, modeNum, ref devMode)) - { - resolutions.Add(new - { - Width = devMode.dmPelsWidth, - Height = devMode.dmPelsHeight, - BitsPerPixel = devMode.dmBitsPerPel, - RefreshRate = devMode.dmDisplayFrequency - }); - modeNum++; - } - - // Remove duplicates and sort by resolution - var uniqueResolutions = resolutions - .GroupBy(r => new { ((dynamic)r).Width, ((dynamic)r).Height, ((dynamic)r).RefreshRate }) - .Select(g => g.First()) - .OrderByDescending(r => ((dynamic)r).Width) - .ThenByDescending(r => ((dynamic)r).Height) - .ThenByDescending(r => ((dynamic)r).RefreshRate) - .ToList(); - - Console.WriteLine(JsonConvert.SerializeObject(uniqueResolutions)); - } - catch (Exception ex) - { - LogError(ex); - } - } - - /// - /// Sets the display resolution. - /// - /// JSON object with "width" and "height" properties, or a string like "1920x1080". - internal static void SetDisplayResolution(JToken value) - { - try - { - uint width; - uint height; - uint? refreshRate = null; - - // Parse the input - can be JSON object or string like "1920x1080" - if (value.Type == JTokenType.Object) - { - width = value.Value("width"); - height = value.Value("height"); - if (value["refreshRate"] != null) - { - refreshRate = value.Value("refreshRate"); - } - } - else - { - string resString = value.ToString(); - string[] parts = resString.ToLowerInvariant().Split('x', '@'); - if (parts.Length < 2) - { - LogWarning("Invalid resolution format. Use 'WIDTHxHEIGHT' or 'WIDTHxHEIGHT@REFRESH' (e.g., '1920x1080' or '1920x1080@60')"); - return; - } - - if (!uint.TryParse(parts[0].Trim(), out width) || !uint.TryParse(parts[1].Trim(), out height)) - { - LogWarning("Invalid resolution values. Width and height must be positive integers."); - return; - } - - if (parts.Length >= 3 && uint.TryParse(parts[2].Trim(), out uint parsedRefresh)) - { - refreshRate = parsedRefresh; - } - } - - // Get the current display settings - DEVMODE currentMode = new DEVMODE(); - currentMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE)); - - if (!EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref currentMode)) - { - LogWarning("Failed to get current display settings."); - return; - } - - // Find a matching display mode - DEVMODE newMode = new DEVMODE(); - newMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE)); - - int modeNum = 0; - bool found = false; - DEVMODE bestMatch = new DEVMODE(); - - while (EnumDisplaySettings(null, modeNum, ref newMode)) - { - if (newMode.dmPelsWidth == width && newMode.dmPelsHeight == height) - { - if (refreshRate.HasValue) - { - if (newMode.dmDisplayFrequency == refreshRate.Value) - { - bestMatch = newMode; - found = true; - break; - } - } - else - { - // Prefer higher refresh rate if not specified - if (!found || newMode.dmDisplayFrequency > bestMatch.dmDisplayFrequency) - { - bestMatch = newMode; - found = true; - } - } - } - modeNum++; - } - - if (!found) - { - LogWarning($"Resolution {width}x{height}" + (refreshRate.HasValue ? $"@{refreshRate}Hz" : "") + " is not supported."); - return; - } - - // Set the required fields - bestMatch.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; - - // TODO: better handle return value from change mode - // Test if the mode change will work - int testResult = ChangeDisplaySettings(ref bestMatch, CDS_TEST); - if (testResult != DISP_CHANGE_SUCCESSFUL && testResult != -2) - { - LogWarning($"Display mode test failed with code: {testResult}"); - return; - } - - // Apply the change - int result = ChangeDisplaySettings(ref bestMatch, CDS_UPDATEREGISTRY); - switch (result) - { - case DISP_CHANGE_SUCCESSFUL: - Console.WriteLine($"Resolution changed to {bestMatch.dmPelsWidth}x{bestMatch.dmPelsHeight}@{bestMatch.dmDisplayFrequency}Hz"); - break; - case DISP_CHANGE_RESTART: - Console.WriteLine($"Resolution will change to {bestMatch.dmPelsWidth}x{bestMatch.dmPelsHeight} after restart."); - break; - default: - LogWarning($"Failed to change resolution. Error code: {result}"); - break; - } - } - catch (Exception ex) - { - LogError(ex); - } - } - - internal static void DisconnectFromWifi() - { - IntPtr clientHandle = IntPtr.Zero; - IntPtr wlanInterfaceList = IntPtr.Zero; - - try - { - // Open WLAN handle - int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); - if (result != 0) - { - LogWarning($"Failed to open WLAN handle: {result}"); - return; - } - - // Enumerate wireless interfaces - result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); - if (result != 0) - { - LogWarning($"Failed to enumerate WLAN interfaces: {result}"); - return; - } - - WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); - - if (interfaceList.dwNumberOfItems == 0) - { - LogWarning("No wireless interfaces found."); - return; - } - - // Disconnect from all wireless interfaces - for (int i = 0; i < interfaceList.dwNumberOfItems; i++) - { - WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i]; - - result = WlanDisconnect(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero); - if (result != 0) - { - LogWarning($"Failed to disconnect from WiFi on interface {i}: {result}"); - } - else - { - Debug.WriteLine($"Successfully disconnected from WiFi on interface: {interfaceInfo.strInterfaceDescription}"); - Console.WriteLine("Disconnected from WiFi"); - } - } - } - catch (Exception ex) - { - LogError(ex); - } - finally - { - if (wlanInterfaceList != IntPtr.Zero) - { - WlanFreeMemory(wlanInterfaceList); - } - - if (clientHandle != IntPtr.Zero) - { - WlanCloseHandle(clientHandle, IntPtr.Zero); - } - } - } } diff --git a/dotnet/autoShell/AutoShell_Win32.cs b/dotnet/autoShell/AutoShell_Win32.cs deleted file mode 100644 index 68a37f99c3..0000000000 --- a/dotnet/autoShell/AutoShell_Win32.cs +++ /dev/null @@ -1,525 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Runtime.InteropServices; -using System.Text; -using System.Windows; - -namespace autoShell -{ - internal partial class AutoShell - { - // Text scaling constants - private const uint WM_SETTINGCHANGE = 0x001A; - private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff); - - // window rect structure - internal struct RECT - { - public int Left; // x position of upper-left corner - public int Top; // y position of upper-left corner - public int Right; // x position of lower-right corner - public int Bottom; // y position of lower-right corner - } - - internal struct Size - { - public int x; - public int y; - } - - // import GetWindowRect - [DllImport("user32.dll")] - private static extern bool GetWindowRect(IntPtr hWnd, ref RECT Rect); - - // import GetDesktopWindow - [DllImport("user32.dll")] - private static extern IntPtr GetDesktopWindow(); - - // import SetForegroundWindow - [System.Runtime.InteropServices.DllImport("user32.dll")] - private static extern bool SetForegroundWindow(IntPtr hWnd); - - [DllImport("user32.dll", EntryPoint = "SendMessage", SetLastError = true)] - private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, UInt32 wParam, IntPtr lParam); - - // import SetWindowPos - [DllImport("user32.dll")] - private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); - - // import ShowWindow - [DllImport("user32.dll")] - private static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow); - - // import FindWindowEx - [DllImport("user32.dll")] - internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName); - - #region Virtual Desktop APIs - - public enum APPLICATION_VIEW_CLOAK_TYPE : int - { - AVCT_NONE = 0, - AVCT_DEFAULT = 1, - AVCT_VIRTUAL_DESKTOP = 2 - } - - public enum APPLICATION_VIEW_COMPATIBILITY_POLICY : int - { - AVCP_NONE = 0, - AVCP_SMALL_SCREEN = 1, - AVCP_TABLET_SMALL_SCREEN = 2, - AVCP_VERY_SMALL_SCREEN = 3, - AVCP_HIGH_SCALE_FACTOR = 4 - } - - // Virtual Desktop COM Interface GUIDs - public static readonly Guid CLSID_ImmersiveShell = new Guid("C2F03A33-21F5-47FA-B4BB-156362A2F239"); - public static readonly Guid CLSID_VirtualDesktopManagerInternal = new Guid("C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B"); - public static readonly Guid CLSID_VirtualDesktopManager = new Guid("AA509086-5CA9-4C25-8F95-589D3C07B48A"); - public static readonly Guid CLSID_VirtualDesktopPinnedApps = new Guid("B5A399E7-1C87-46B8-88E9-FC5747B171BD"); - - // IServiceProvider COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] - private interface IServiceProvider - { - [return: MarshalAs(UnmanagedType.IUnknown)] - void QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject); - } - - // IVirtualDesktopManager COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("A5CD92FF-29BE-454C-8D04-D82879FB3F1B")] - internal interface IVirtualDesktopManager - { - bool IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow); - Guid GetWindowDesktopId(IntPtr topLevelWindow); - void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId); - } - - // IVirtualDesktop COM Interface (Windows 10/11) - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] - internal interface IVirtualDesktop - { - bool IsViewVisible(IApplicationView view); - Guid GetId(); - // TODO: proper HSTRING custom marshaling - [return: MarshalAs(UnmanagedType.HString)] - string GetName(); - [return: MarshalAs(UnmanagedType.HString)] - string GetWallpaperPath(); - bool IsRemote(); - } - - // IVirtualDesktopManagerInternal COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("53F5CA0B-158F-4124-900C-057158060B27")] - internal interface IVirtualDesktopManagerInternal_BUGBUG - { - int GetCount(); - void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); - bool CanViewMoveDesktops(IApplicationView view); - IVirtualDesktop GetCurrentDesktop(); - void GetDesktops(out IObjectArray desktops); - [PreserveSig] - int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); - void SwitchDesktop(IVirtualDesktop desktop); - IVirtualDesktop CreateDesktop(); - void MoveDesktop(IVirtualDesktop desktop, int nIndex); - void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); - IVirtualDesktop FindDesktop(ref Guid desktopid); - void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); - void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); - void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); - void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); - void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); - void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); - void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); - void SwitchDesktopWithAnimation(IVirtualDesktop desktop); - void GetLastActiveDesktop(out IVirtualDesktop desktop); - void WaitForAnimationToComplete(); - } - - // IVirtualDesktopManagerInternal COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("53F5CA0B-158F-4124-900C-057158060B27")] - internal interface IVirtualDesktopManagerInternal - { - int GetCount(); - void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); - bool CanViewMoveDesktops(IApplicationView view); - IVirtualDesktop GetCurrentDesktop(); - void GetDesktops(out IObjectArray desktops); - [PreserveSig] - int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); - void SwitchDesktop(IVirtualDesktop desktop); - void SwitchDesktopAndMoveForegroundView(IVirtualDesktop desktop); - IVirtualDesktop CreateDesktop(); - void MoveDesktop(IVirtualDesktop desktop, int nIndex); - void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); - IVirtualDesktop FindDesktop(ref Guid desktopid); - void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); - void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); - void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); - void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); - void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); - void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); - void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); - void SwitchDesktopWithAnimation(IVirtualDesktop desktop); - void GetLastActiveDesktop(out IVirtualDesktop desktop); - void WaitForAnimationToComplete(); - } - - // IObjectArray COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("92CA9DCD-5622-4BBA-A805-5E9F541BD8C9")] - internal interface IObjectArray - { - void GetCount(out int pcObjects); - void GetAt(int uiIndex, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("372E1D3B-38D3-42E4-A15B-8AB2B178F513")] - internal interface IApplicationView - { - int SetFocus(); - int SwitchTo(); - int TryInvokeBack(IntPtr /* IAsyncCallback* */ callback); - int GetThumbnailWindow(out IntPtr hwnd); - int GetMonitor(out IntPtr /* IImmersiveMonitor */ immersiveMonitor); - int GetVisibility(out int visibility); - int SetCloak(APPLICATION_VIEW_CLOAK_TYPE cloakType, int unknown); - int GetPosition(ref Guid guid /* GUID for IApplicationViewPosition */, out IntPtr /* IApplicationViewPosition** */ position); - int SetPosition(ref IntPtr /* IApplicationViewPosition* */ position); - int InsertAfterWindow(IntPtr hwnd); - int GetExtendedFramePosition(out Rect rect); - int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string id); - int SetAppUserModelId(string id); - int IsEqualByAppUserModelId(string id, out int result); - int GetViewState(out uint state); - int SetViewState(uint state); - int GetNeediness(out int neediness); - int GetLastActivationTimestamp(out ulong timestamp); - int SetLastActivationTimestamp(ulong timestamp); - int GetVirtualDesktopId(out Guid guid); - int SetVirtualDesktopId(ref Guid guid); - int GetShowInSwitchers(out int flag); - int SetShowInSwitchers(int flag); - int GetScaleFactor(out int factor); - int CanReceiveInput(out bool canReceiveInput); - int GetCompatibilityPolicyType(out APPLICATION_VIEW_COMPATIBILITY_POLICY flags); - int SetCompatibilityPolicyType(APPLICATION_VIEW_COMPATIBILITY_POLICY flags); - int GetSizeConstraints(IntPtr /* IImmersiveMonitor* */ monitor, out Size size1, out Size size2); - int GetSizeConstraintsForDpi(uint uint1, out Size size1, out Size size2); - int SetSizeConstraintsForDpi(ref uint uint1, ref Size size1, ref Size size2); - int OnMinSizePreferencesUpdated(IntPtr hwnd); - int ApplyOperation(IntPtr /* IApplicationViewOperation* */ operation); - int IsTray(out bool isTray); - int IsInHighZOrderBand(out bool isInHighZOrderBand); - int IsSplashScreenPresented(out bool isSplashScreenPresented); - int Flash(); - int GetRootSwitchableOwner(out IApplicationView rootSwitchableOwner); - int EnumerateOwnershipTree(out IObjectArray ownershipTree); - int GetEnterpriseId([MarshalAs(UnmanagedType.LPWStr)] out string enterpriseId); - int IsMirrored(out bool isMirrored); - int Unknown1(out int unknown); - int Unknown2(out int unknown); - int Unknown3(out int unknown); - int Unknown4(out int unknown); - int Unknown5(out int unknown); - int Unknown6(int unknown); - int Unknown7(); - int Unknown8(out int unknown); - int Unknown9(int unknown); - int Unknown10(int unknownX, int unknownY); - int Unknown11(int unknown); - int Unknown12(out Size size1); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("1841C6D7-4F9D-42C0-AF41-8747538F10E5")] - internal interface IApplicationViewCollection - { - int GetViews(out IObjectArray array); - int GetViewsByZOrder(out IObjectArray array); - int GetViewsByAppUserModelId(string id, out IObjectArray array); - int GetViewForHwnd(IntPtr hwnd, out IApplicationView view); - int GetViewForApplication(object application, out IApplicationView view); - int GetViewForAppUserModelId(string id, out IApplicationView view); - int GetViewInFocus(out IntPtr view); - int Unknown1(out IntPtr view); - void RefreshCollection(); - int RegisterForApplicationViewChanges(object listener, out int cookie); - int UnregisterForApplicationViewChanges(int cookie); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("4CE81583-1E4C-4632-A621-07A53543148F")] - internal interface IVirtualDesktopPinnedApps - { - bool IsAppIdPinned(string appId); - void PinAppID(string appId); - void UnpinAppID(string appId); - bool IsViewPinned(IApplicationView applicationView); - void PinView(IApplicationView applicationView); - void UnpinView(IApplicationView applicationView); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] - internal interface IServiceProvider10 - { - [return: MarshalAs(UnmanagedType.IUnknown)] - object QueryService(ref Guid service, ref Guid riid); - } - - #endregion Virtual Desktop APIs - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr GetCommandLineW(); - - - #region Window Functions - - // Delegate for EnumWindows callback - internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - #endregion Window Functions - - // GUIDs for Radio Management API - internal static readonly Guid CLSID_RadioManagementAPI = new Guid(0x581333f6, 0x28db, 0x41be, 0xbc, 0x7a, 0xff, 0x20, 0x1f, 0x12, 0xf3, 0xf6); - internal static readonly Guid IID_IRadioManager = new Guid(0xdb3afbfb, 0x08e6, 0x46c6, 0xaa, 0x70, 0xbf, 0x9a, 0x34, 0xc3, 0x0a, 0xb7); - - [ComImport] - [Guid("db3afbfb-08e6-46c6-aa70-bf9a34c30ab7")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IRadioManager - { - [PreserveSig] - int IsRMSupported(out uint pdwState); - - [PreserveSig] - int GetUIRadioInstances([MarshalAs(UnmanagedType.IUnknown)] out object ppCollection); - - [PreserveSig] - int GetSystemRadioState(out int pbEnabled, out int param2, out int pChangeReason); - - [PreserveSig] - int SetSystemRadioState(int bEnabled); - - [PreserveSig] - int Refresh(); - - [PreserveSig] - int OnHardwareSliderChange(int param1, int param2); - } - - #region WiFi - - // WLAN API P/Invoke declarations - [DllImport("wlanapi.dll")] - private static extern int WlanOpenHandle(uint dwClientVersion, IntPtr pReserved, out uint pdwNegotiatedVersion, out IntPtr phClientHandle); - - [DllImport("wlanapi.dll")] - private static extern int WlanCloseHandle(IntPtr hClientHandle, IntPtr pReserved); - - [DllImport("wlanapi.dll")] - private static extern int WlanEnumInterfaces(IntPtr hClientHandle, IntPtr pReserved, out IntPtr ppInterfaceList); - - [DllImport("wlanapi.dll")] - private static extern int WlanGetAvailableNetworkList(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, IntPtr pReserved, out IntPtr ppAvailableNetworkList); - - [DllImport("wlanapi.dll")] - private static extern int WlanScan(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pDot11Ssid, IntPtr pIeData, IntPtr pReserved); - - [DllImport("wlanapi.dll")] - private static extern void WlanFreeMemory(IntPtr pMemory); - - [DllImport("wlanapi.dll")] - private static extern int WlanConnect(IntPtr hClientHandle, ref Guid pInterfaceGuid, ref WLAN_CONNECTION_PARAMETERS pConnectionParameters, IntPtr pReserved); - - [DllImport("wlanapi.dll")] - private static extern int WlanDisconnect(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pReserved); - - [DllImport("wlanapi.dll")] - private static extern int WlanSetProfile(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string strProfileXml, [MarshalAs(UnmanagedType.LPWStr)] string strAllUserProfileSecurity, bool bOverwrite, IntPtr pReserved, out uint pdwReasonCode); - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - struct WLAN_INTERFACE_INFO - { - public Guid InterfaceGuid; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] - public string strInterfaceDescription; - public int isState; - } - - [StructLayout(LayoutKind.Sequential)] - struct WLAN_INTERFACE_INFO_LIST - { - public uint dwNumberOfItems; - public uint dwIndex; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] - public WLAN_INTERFACE_INFO[] InterfaceInfo; - } - - [StructLayout(LayoutKind.Sequential)] - struct DOT11_SSID - { - public uint SSIDLength; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] - public byte[] SSID; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - struct WLAN_AVAILABLE_NETWORK - { - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] - public string strProfileName; - public DOT11_SSID dot11Ssid; - public int dot11BssType; - public uint uNumberOfBssids; - public bool bNetworkConnectable; - public uint wlanNotConnectableReason; - public uint uNumberOfPhyTypes; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] - public int[] dot11PhyTypes; - public bool bMorePhyTypes; - public uint wlanSignalQuality; - public bool bSecurityEnabled; - public int dot11DefaultAuthAlgorithm; - public int dot11DefaultCipherAlgorithm; - public uint dwFlags; - public uint dwReserved; - } - - [StructLayout(LayoutKind.Sequential)] - struct WLAN_AVAILABLE_NETWORK_LIST - { - public uint dwNumberOfItems; - public uint dwIndex; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - struct WLAN_CONNECTION_PARAMETERS - { - public WLAN_CONNECTION_MODE wlanConnectionMode; - [MarshalAs(UnmanagedType.LPWStr)] - public string strProfile; - public IntPtr pDot11Ssid; - public IntPtr pDesiredBssidList; - public DOT11_BSS_TYPE dot11BssType; - public uint dwFlags; - } - - enum WLAN_CONNECTION_MODE - { - wlan_connection_mode_profile = 0, - wlan_connection_mode_temporary_profile = 1, - wlan_connection_mode_discovery_secure = 2, - wlan_connection_mode_discovery_unsecure = 3, - wlan_connection_mode_auto = 4 - } - - enum DOT11_BSS_TYPE - { - dot11_BSS_type_infrastructure = 1, - dot11_BSS_type_independent = 2, - dot11_BSS_type_any = 3 - } - - #endregion WiFi - - #region Display Resolution - - private const int ENUM_CURRENT_SETTINGS = -1; - private const int DISP_CHANGE_SUCCESSFUL = 0; - private const int DISP_CHANGE_RESTART = 1; - - private const int DM_PELSWIDTH = 0x80000; - private const int DM_PELSHEIGHT = 0x100000; - private const int DM_BITSPERPEL = 0x40000; - private const int DM_DISPLAYFREQUENCY = 0x400000; - - private const int CDS_UPDATEREGISTRY = 0x01; - private const int CDS_TEST = 0x02; - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] - internal struct DEVMODE - { - private const int CCHDEVICENAME = 32; - private const int CCHFORMNAME = 32; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)] - public string dmDeviceName; - public ushort dmSpecVersion; - public ushort dmDriverVersion; - public ushort dmSize; - public ushort dmDriverExtra; - public uint dmFields; - public int dmPositionX; - public int dmPositionY; - public uint dmDisplayOrientation; - public uint dmDisplayFixedOutput; - public short dmColor; - public short dmDuplex; - public short dmYResolution; - public short dmTTOption; - public short dmCollate; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHFORMNAME)] - public string dmFormName; - public ushort dmLogPixels; - public uint dmBitsPerPel; - public uint dmPelsWidth; - public uint dmPelsHeight; - public uint dmDisplayFlags; - public uint dmDisplayFrequency; - public uint dmICMMethod; - public uint dmICMIntent; - public uint dmMediaType; - public uint dmDitherType; - public uint dmReserved1; - public uint dmReserved2; - public uint dmPanningWidth; - public uint dmPanningHeight; - } - - [DllImport("user32.dll", CharSet = CharSet.Ansi)] - internal static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode); - - [DllImport("user32.dll", CharSet = CharSet.Ansi)] - internal static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags); - - [DllImport("user32.dll", CharSet = CharSet.Ansi)] - internal static extern int ChangeDisplaySettingsEx(string deviceName, ref DEVMODE devMode, IntPtr hwnd, int dwFlags, IntPtr lParam); - - #endregion Display Resolution - } -} diff --git a/dotnet/autoShell/Handlers/AppCommandHandler.cs b/dotnet/autoShell/Handlers/AppCommandHandler.cs index 565ae6c789..a4fd2d9796 100644 --- a/dotnet/autoShell/Handlers/AppCommandHandler.cs +++ b/dotnet/autoShell/Handlers/AppCommandHandler.cs @@ -3,16 +3,27 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using autoShell.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; /// -/// Handles application lifecycle commands: launchProgram, closeProgram, listAppNames. +/// Handles application lifecycle commands: launchProgram, closeProgram. /// internal class AppCommandHandler : ICommandHandler { + private readonly IAppRegistry _appRegistry; + private readonly IProcessService _processService; + + public AppCommandHandler(IAppRegistry appRegistry, IProcessService processService) + { + _appRegistry = appRegistry; + _processService = processService; + } + /// public IEnumerable SupportedCommands { get; } = [ @@ -27,17 +38,92 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "LaunchProgram": - AutoShell.OpenApplication(value); + OpenApplication(value); break; case "CloseProgram": - AutoShell.CloseApplication(value); + CloseApplication(value); break; case "ListAppNames": - var installedApps = AutoShell.GetAllInstalledAppsIds(); - Console.WriteLine(JsonConvert.SerializeObject(installedApps.Keys)); + Console.WriteLine(JsonConvert.SerializeObject(_appRegistry.GetAllAppNames())); break; } } + + private void OpenApplication(string friendlyName) + { + string processName = _appRegistry.ResolveProcessName(friendlyName); + Process[] processes = _processService.GetProcessesByName(processName); + + if (processes.Length == 0) + { + Debug.WriteLine("Starting " + friendlyName); + string path = _appRegistry.GetExecutablePath(friendlyName); + if (path != null) + { + var psi = new ProcessStartInfo + { + FileName = path, + UseShellExecute = true + }; + + string workDirEnvVar = _appRegistry.GetWorkingDirectoryEnvVar(friendlyName); + if (workDirEnvVar != null) + { + psi.WorkingDirectory = Environment.ExpandEnvironmentVariables("%" + workDirEnvVar + "%") ?? string.Empty; + } + + string arguments = _appRegistry.GetArguments(friendlyName); + if (arguments != null) + { + psi.Arguments = arguments; + } + + try + { + _processService.Start(psi); + } + catch (System.ComponentModel.Win32Exception) + { + psi.FileName = friendlyName; + _processService.Start(psi); + } + } + else + { + string appModelUserID = _appRegistry.GetAppUserModelId(friendlyName); + if (appModelUserID != null) + { + try + { + _processService.Start(new ProcessStartInfo("explorer.exe", @" shell:appsFolder\" + appModelUserID)); + } + catch { } + } + } + } + else + { + Debug.WriteLine("Raising " + friendlyName); + WindowCommandHandler.RaiseWindow(friendlyName, _appRegistry); + } + } + + private void CloseApplication(string friendlyName) + { + string processName = _appRegistry.ResolveProcessName(friendlyName); + Process[] processes = _processService.GetProcessesByName(processName); + if (processes.Length != 0) + { + Debug.WriteLine("Closing " + friendlyName); + foreach (Process p in processes) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + p.CloseMainWindow(); + } + } + } + } } diff --git a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs index 91326f7339..0d6ef038a7 100644 --- a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs +++ b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs @@ -1,7 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; @@ -11,6 +16,67 @@ namespace autoShell.Handlers; /// internal class DisplayCommandHandler : ICommandHandler { + #region P/Invoke + + private const int ENUM_CURRENT_SETTINGS = -1; + private const int DISP_CHANGE_SUCCESSFUL = 0; + private const int DISP_CHANGE_RESTART = 1; + + private const int DM_PELSWIDTH = 0x80000; + private const int DM_PELSHEIGHT = 0x100000; + private const int DM_DISPLAYFREQUENCY = 0x400000; + + private const int CDS_UPDATEREGISTRY = 0x01; + private const int CDS_TEST = 0x02; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + private struct DEVMODE + { + private const int CCHDEVICENAME = 32; + private const int CCHFORMNAME = 32; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)] + public string dmDeviceName; + public ushort dmSpecVersion; + public ushort dmDriverVersion; + public ushort dmSize; + public ushort dmDriverExtra; + public uint dmFields; + public int dmPositionX; + public int dmPositionY; + public uint dmDisplayOrientation; + public uint dmDisplayFixedOutput; + public short dmColor; + public short dmDuplex; + public short dmYResolution; + public short dmTTOption; + public short dmCollate; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHFORMNAME)] + public string dmFormName; + public ushort dmLogPixels; + public uint dmBitsPerPel; + public uint dmPelsWidth; + public uint dmPelsHeight; + public uint dmDisplayFlags; + public uint dmDisplayFrequency; + public uint dmICMMethod; + public uint dmICMIntent; + public uint dmMediaType; + public uint dmDitherType; + public uint dmReserved1; + public uint dmReserved2; + public uint dmPanningWidth; + public uint dmPanningHeight; + } + + [DllImport("user32.dll", CharSet = CharSet.Ansi)] + private static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode); + + [DllImport("user32.dll", CharSet = CharSet.Ansi)] + private static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags); + + #endregion P/Invoke + /// public IEnumerable SupportedCommands { get; } = [ @@ -27,17 +93,213 @@ public void Handle(string key, string value, JToken rawValue) case "SetTextSize": if (int.TryParse(value, out int textSizePct)) { - AutoShell.SetTextSize(textSizePct); + SetTextSize(textSizePct); } break; case "SetScreenResolution": - AutoShell.SetDisplayResolution(rawValue); + SetDisplayResolution(rawValue); break; case "ListResolutions": - AutoShell.ListDisplayResolutions(); + ListDisplayResolutions(); break; } } + + /// + /// Sets the system text scaling factor (percentage). + /// + private void SetTextSize(int percentage) + { + try + { + if (percentage == -1) + { + percentage = new Random().Next(100, 225 + 1); + } + + if (percentage < 100) + { + percentage = 100; + } + else if (percentage > 225) + { + percentage = 225; + } + + Process.Start(new ProcessStartInfo + { + FileName = "ms-settings:easeofaccess", + UseShellExecute = true + }); + + #pragma warning disable CS0618 // UIAutomation is intentionally marked obsolete as a last-resort approach + UIAutomation.SetTextSizeViaUIAutomation(percentage); + #pragma warning restore CS0618 + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + /// + /// Lists all available display resolutions for the primary monitor. + /// + private void ListDisplayResolutions() + { + try + { + var resolutions = new List(); + DEVMODE devMode = new DEVMODE(); + devMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE)); + + int modeNum = 0; + while (EnumDisplaySettings(null, modeNum, ref devMode)) + { + resolutions.Add(new + { + Width = devMode.dmPelsWidth, + Height = devMode.dmPelsHeight, + BitsPerPixel = devMode.dmBitsPerPel, + RefreshRate = devMode.dmDisplayFrequency + }); + modeNum++; + } + + var uniqueResolutions = resolutions + .GroupBy(r => new { ((dynamic)r).Width, ((dynamic)r).Height, ((dynamic)r).RefreshRate }) + .Select(g => g.First()) + .OrderByDescending(r => ((dynamic)r).Width) + .ThenByDescending(r => ((dynamic)r).Height) + .ThenByDescending(r => ((dynamic)r).RefreshRate) + .ToList(); + + Console.WriteLine(JsonConvert.SerializeObject(uniqueResolutions)); + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } + + /// + /// Sets the display resolution. + /// + private void SetDisplayResolution(JToken value) + { + try + { + uint width; + uint height; + uint? refreshRate = null; + + if (value.Type == JTokenType.Object) + { + width = value.Value("width"); + height = value.Value("height"); + if (value["refreshRate"] != null) + { + refreshRate = value.Value("refreshRate"); + } + } + else + { + string resString = value.ToString(); + string[] parts = resString.ToLowerInvariant().Split('x', '@'); + if (parts.Length < 2) + { + AutoShell.LogWarning("Invalid resolution format. Use 'WIDTHxHEIGHT' or 'WIDTHxHEIGHT@REFRESH' (e.g., '1920x1080' or '1920x1080@60')"); + return; + } + + if (!uint.TryParse(parts[0].Trim(), out width) || !uint.TryParse(parts[1].Trim(), out height)) + { + AutoShell.LogWarning("Invalid resolution values. Width and height must be positive integers."); + return; + } + + if (parts.Length >= 3 && uint.TryParse(parts[2].Trim(), out uint parsedRefresh)) + { + refreshRate = parsedRefresh; + } + } + + DEVMODE currentMode = new DEVMODE(); + currentMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE)); + + if (!EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref currentMode)) + { + AutoShell.LogWarning("Failed to get current display settings."); + return; + } + + DEVMODE newMode = new DEVMODE(); + newMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE)); + + int modeNum = 0; + bool found = false; + DEVMODE bestMatch = new DEVMODE(); + + while (EnumDisplaySettings(null, modeNum, ref newMode)) + { + if (newMode.dmPelsWidth == width && newMode.dmPelsHeight == height) + { + if (refreshRate.HasValue) + { + if (newMode.dmDisplayFrequency == refreshRate.Value) + { + bestMatch = newMode; + found = true; + break; + } + } + else + { + if (!found || newMode.dmDisplayFrequency > bestMatch.dmDisplayFrequency) + { + bestMatch = newMode; + found = true; + } + } + } + modeNum++; + } + + if (!found) + { + AutoShell.LogWarning($"Resolution {width}x{height}" + (refreshRate.HasValue ? $"@{refreshRate}Hz" : "") + " is not supported."); + return; + } + + bestMatch.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + + // TODO: better handle return value from change mode + int testResult = ChangeDisplaySettings(ref bestMatch, CDS_TEST); + if (testResult != DISP_CHANGE_SUCCESSFUL && testResult != -2) + { + AutoShell.LogWarning($"Display mode test failed with code: {testResult}"); + return; + } + + int result = ChangeDisplaySettings(ref bestMatch, CDS_UPDATEREGISTRY); + switch (result) + { + case DISP_CHANGE_SUCCESSFUL: + Console.WriteLine($"Resolution changed to {bestMatch.dmPelsWidth}x{bestMatch.dmPelsHeight}@{bestMatch.dmDisplayFrequency}Hz"); + break; + case DISP_CHANGE_RESTART: + Console.WriteLine($"Resolution will change to {bestMatch.dmPelsWidth}x{bestMatch.dmPelsHeight} after restart."); + break; + default: + AutoShell.LogWarning($"Failed to change resolution. Error code: {result}"); + break; + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } } diff --git a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs index aff0e6dc9b..e668a3665b 100644 --- a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs +++ b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs @@ -4,6 +4,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; @@ -14,6 +18,146 @@ namespace autoShell.Handlers; /// internal class NetworkCommandHandler : ICommandHandler { + #region COM / P/Invoke + + private static readonly Guid CLSID_RadioManagementAPI = new Guid(0x581333f6, 0x28db, 0x41be, 0xbc, 0x7a, 0xff, 0x20, 0x1f, 0x12, 0xf3, 0xf6); + + [ComImport] + [Guid("db3afbfb-08e6-46c6-aa70-bf9a34c30ab7")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IRadioManager + { + [PreserveSig] + int IsRMSupported(out uint pdwState); + + [PreserveSig] + int GetUIRadioInstances([MarshalAs(UnmanagedType.IUnknown)] out object ppCollection); + + [PreserveSig] + int GetSystemRadioState(out int pbEnabled, out int param2, out int pChangeReason); + + [PreserveSig] + int SetSystemRadioState(int bEnabled); + + [PreserveSig] + int Refresh(); + + [PreserveSig] + int OnHardwareSliderChange(int param1, int param2); + } + + [DllImport("wlanapi.dll")] + private static extern int WlanOpenHandle(uint dwClientVersion, IntPtr pReserved, out uint pdwNegotiatedVersion, out IntPtr phClientHandle); + + [DllImport("wlanapi.dll")] + private static extern int WlanCloseHandle(IntPtr hClientHandle, IntPtr pReserved); + + [DllImport("wlanapi.dll")] + private static extern int WlanEnumInterfaces(IntPtr hClientHandle, IntPtr pReserved, out IntPtr ppInterfaceList); + + [DllImport("wlanapi.dll")] + private static extern int WlanGetAvailableNetworkList(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, IntPtr pReserved, out IntPtr ppAvailableNetworkList); + + [DllImport("wlanapi.dll")] + private static extern int WlanScan(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pDot11Ssid, IntPtr pIeData, IntPtr pReserved); + + [DllImport("wlanapi.dll")] + private static extern void WlanFreeMemory(IntPtr pMemory); + + [DllImport("wlanapi.dll")] + private static extern int WlanConnect(IntPtr hClientHandle, ref Guid pInterfaceGuid, ref WLAN_CONNECTION_PARAMETERS pConnectionParameters, IntPtr pReserved); + + [DllImport("wlanapi.dll")] + private static extern int WlanDisconnect(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pReserved); + + [DllImport("wlanapi.dll")] + private static extern int WlanSetProfile(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string strProfileXml, [MarshalAs(UnmanagedType.LPWStr)] string strAllUserProfileSecurity, bool bOverwrite, IntPtr pReserved, out uint pdwReasonCode); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WLAN_INTERFACE_INFO + { + public Guid InterfaceGuid; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string strInterfaceDescription; + public int isState; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WLAN_INTERFACE_INFO_LIST + { + public uint dwNumberOfItems; + public uint dwIndex; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + public WLAN_INTERFACE_INFO[] InterfaceInfo; + } + + [StructLayout(LayoutKind.Sequential)] + private struct DOT11_SSID + { + public uint SSIDLength; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public byte[] SSID; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WLAN_AVAILABLE_NETWORK + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string strProfileName; + public DOT11_SSID dot11Ssid; + public int dot11BssType; + public uint uNumberOfBssids; + public bool bNetworkConnectable; + public uint wlanNotConnectableReason; + public uint uNumberOfPhyTypes; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public int[] dot11PhyTypes; + public bool bMorePhyTypes; + public uint wlanSignalQuality; + public bool bSecurityEnabled; + public int dot11DefaultAuthAlgorithm; + public int dot11DefaultCipherAlgorithm; + public uint dwFlags; + public uint dwReserved; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WLAN_AVAILABLE_NETWORK_LIST + { + public uint dwNumberOfItems; + public uint dwIndex; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WLAN_CONNECTION_PARAMETERS + { + public WLAN_CONNECTION_MODE wlanConnectionMode; + [MarshalAs(UnmanagedType.LPWStr)] + public string strProfile; + public IntPtr pDot11Ssid; + public IntPtr pDesiredBssidList; + public DOT11_BSS_TYPE dot11BssType; + public uint dwFlags; + } + + private enum WLAN_CONNECTION_MODE + { + wlan_connection_mode_profile = 0, + wlan_connection_mode_temporary_profile = 1, + wlan_connection_mode_discovery_secure = 2, + wlan_connection_mode_discovery_unsecure = 3, + wlan_connection_mode_auto = 4 + } + + private enum DOT11_BSS_TYPE + { + dot11_BSS_type_infrastructure = 1, + dot11_BSS_type_independent = 2, + dot11_BSS_type_any = 3 + } + + #endregion COM / P/Invoke + /// public IEnumerable SupportedCommands { get; } = [ @@ -32,22 +176,22 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "ToggleAirplaneMode": - AutoShell.SetAirplaneMode(bool.Parse(value)); + SetAirplaneMode(bool.Parse(value)); break; case "ListWifiNetworks": - AutoShell.ListWifiNetworks(); + ListWifiNetworks(); break; case "ConnectWifi": var netInfo = JObject.Parse(value); string ssid = netInfo.Value("ssid"); string password = netInfo["password"] is not null ? netInfo.Value("password") : ""; - AutoShell.ConnectToWifi(ssid, password); + ConnectToWifi(ssid, password); break; case "DisconnectWifi": - AutoShell.DisconnectFromWifi(); + DisconnectFromWifi(); break; case "BluetoothToggle": @@ -58,4 +202,363 @@ public void Handle(string key, string value, JToken rawValue) break; } } + + /// + /// Sets the airplane mode state using the Radio Management API. + /// + private void SetAirplaneMode(bool enable) + { + IRadioManager radioManager = null; + try + { + Type radioManagerType = Type.GetTypeFromCLSID(CLSID_RadioManagementAPI); + if (radioManagerType == null) + { + Debug.WriteLine("Failed to get Radio Management API type"); + return; + } + + object obj = Activator.CreateInstance(radioManagerType); + radioManager = (IRadioManager)obj; + + if (radioManager == null) + { + Debug.WriteLine("Failed to create Radio Manager instance"); + return; + } + + int hr = radioManager.GetSystemRadioState(out int currentState, out int _, out int _); + if (hr < 0) + { + Debug.WriteLine($"Failed to get system radio state: HRESULT 0x{hr:X8}"); + return; + } + + bool airplaneModeCurrentlyOn = currentState == 0; + Debug.WriteLine($"Current airplane mode state: {(airplaneModeCurrentlyOn ? "on" : "off")}"); + + int newState = enable ? 0 : 1; + hr = radioManager.SetSystemRadioState(newState); + if (hr < 0) + { + Debug.WriteLine($"Failed to set system radio state: HRESULT 0x{hr:X8}"); + return; + } + + Debug.WriteLine($"Airplane mode set to: {(enable ? "on" : "off")}"); + } + catch (COMException ex) + { + Debug.WriteLine($"COM Exception setting airplane mode: {ex.Message} (HRESULT: 0x{ex.HResult:X8})"); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to set airplane mode: {ex.Message}"); + } + finally + { + if (radioManager != null) + { + Marshal.ReleaseComObject(radioManager); + } + } + } + + /// + /// Lists all WiFi networks currently in range. + /// + private void ListWifiNetworks() + { + IntPtr clientHandle = IntPtr.Zero; + IntPtr wlanInterfaceList = IntPtr.Zero; + IntPtr networkList = IntPtr.Zero; + + try + { + int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); + if (result != 0) + { + Debug.WriteLine($"Failed to open WLAN handle: {result}"); + return; + } + + result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); + if (result != 0) + { + Debug.WriteLine($"Failed to enumerate WLAN interfaces: {result}"); + return; + } + + WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + + if (interfaceList.dwNumberOfItems == 0) + { + Console.WriteLine("[]"); + return; + } + + var allNetworks = new List(); + + for (int i = 0; i < interfaceList.dwNumberOfItems; i++) + { + WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i]; + + WlanScan(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + + System.Threading.Thread.Sleep(100); + + result = WlanGetAvailableNetworkList(clientHandle, ref interfaceInfo.InterfaceGuid, 0, IntPtr.Zero, out networkList); + if (result != 0) + { + Debug.WriteLine($"Failed to get network list: {result}"); + continue; + } + + WLAN_AVAILABLE_NETWORK_LIST availableNetworkList = Marshal.PtrToStructure(networkList); + + IntPtr networkPtr = networkList + 8; // Skip dwNumberOfItems and dwIndex + + for (int j = 0; j < availableNetworkList.dwNumberOfItems; j++) + { + WLAN_AVAILABLE_NETWORK network = Marshal.PtrToStructure(networkPtr); + + string ssid = Encoding.ASCII.GetString(network.dot11Ssid.SSID, 0, (int)network.dot11Ssid.SSIDLength); + + if (!string.IsNullOrEmpty(ssid)) + { + allNetworks.Add(new + { + SSID = ssid, + SignalQuality = network.wlanSignalQuality, + Secured = network.bSecurityEnabled, + Connected = (network.dwFlags & 1) != 0 // WLAN_AVAILABLE_NETWORK_CONNECTED + }); + } + + networkPtr += Marshal.SizeOf(); + } + + if (networkList != IntPtr.Zero) + { + WlanFreeMemory(networkList); + networkList = IntPtr.Zero; + } + } + + var uniqueNetworks = allNetworks + .GroupBy(n => ((dynamic)n).SSID) + .Select(g => g.OrderByDescending(n => ((dynamic)n).SignalQuality).First()) + .OrderByDescending(n => ((dynamic)n).SignalQuality) + .ToList(); + + Console.WriteLine(JsonConvert.SerializeObject(uniqueNetworks)); + } + catch (Exception ex) + { + Debug.WriteLine($"Error listing WiFi networks: {ex.Message}"); + Console.WriteLine("[]"); + } + finally + { + if (networkList != IntPtr.Zero) + { + WlanFreeMemory(networkList); + } + + if (wlanInterfaceList != IntPtr.Zero) + { + WlanFreeMemory(wlanInterfaceList); + } + + if (clientHandle != IntPtr.Zero) + { + WlanCloseHandle(clientHandle, IntPtr.Zero); + } + } + } + + /// + /// Connects to a WiFi network by name (SSID). + /// + private void ConnectToWifi(string ssid, string password = null) + { + IntPtr clientHandle = IntPtr.Zero; + IntPtr wlanInterfaceList = IntPtr.Zero; + + try + { + int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); + if (result != 0) + { + AutoShell.LogWarning($"Failed to open WLAN handle: {result}"); + return; + } + + result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); + if (result != 0) + { + AutoShell.LogWarning($"Failed to enumerate WLAN interfaces: {result}"); + return; + } + + WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + + if (interfaceList.dwNumberOfItems == 0) + { + AutoShell.LogWarning("No wireless interfaces found."); + return; + } + + WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[0]; + + if (!string.IsNullOrEmpty(password)) + { + string profileXml = GenerateWifiProfileXml(ssid, password); + + result = WlanSetProfile(clientHandle, ref interfaceInfo.InterfaceGuid, 0, profileXml, null, true, IntPtr.Zero, out uint reasonCode); + if (result != 0) + { + AutoShell.LogWarning($"Failed to set WiFi profile: {result}, reason: {reasonCode}"); + return; + } + } + + WLAN_CONNECTION_PARAMETERS connectionParams = new WLAN_CONNECTION_PARAMETERS + { + wlanConnectionMode = WLAN_CONNECTION_MODE.wlan_connection_mode_profile, + strProfile = ssid, + pDot11Ssid = IntPtr.Zero, + pDesiredBssidList = IntPtr.Zero, + dot11BssType = DOT11_BSS_TYPE.dot11_BSS_type_any, + dwFlags = 0 + }; + + result = WlanConnect(clientHandle, ref interfaceInfo.InterfaceGuid, ref connectionParams, IntPtr.Zero); + if (result != 0) + { + AutoShell.LogWarning($"Failed to connect to WiFi network '{ssid}': {result}"); + return; + } + + Debug.WriteLine($"Successfully initiated connection to WiFi network: {ssid}"); + Console.WriteLine($"Connecting to WiFi network: {ssid}"); + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + finally + { + if (wlanInterfaceList != IntPtr.Zero) + { + WlanFreeMemory(wlanInterfaceList); + } + + if (clientHandle != IntPtr.Zero) + { + WlanCloseHandle(clientHandle, IntPtr.Zero); + } + } + } + + /// + /// Generates a WiFi profile XML for WPA2-Personal (PSK) networks. + /// + private static string GenerateWifiProfileXml(string ssid, string password) + { + string ssidHex = BitConverter.ToString(Encoding.UTF8.GetBytes(ssid)).Replace("-", ""); + + return $@" + + {ssid} + + + {ssidHex} + {ssid} + + + ESS + auto + + + + WPA2PSK + AES + false + + + passPhrase + false + {password} + + + +"; + } + + /// + /// Disconnects from the currently connected WiFi network. + /// + private void DisconnectFromWifi() + { + IntPtr clientHandle = IntPtr.Zero; + IntPtr wlanInterfaceList = IntPtr.Zero; + + try + { + int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); + if (result != 0) + { + AutoShell.LogWarning($"Failed to open WLAN handle: {result}"); + return; + } + + result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); + if (result != 0) + { + AutoShell.LogWarning($"Failed to enumerate WLAN interfaces: {result}"); + return; + } + + WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + + if (interfaceList.dwNumberOfItems == 0) + { + AutoShell.LogWarning("No wireless interfaces found."); + return; + } + + for (int i = 0; i < interfaceList.dwNumberOfItems; i++) + { + WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i]; + + result = WlanDisconnect(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero); + if (result != 0) + { + AutoShell.LogWarning($"Failed to disconnect from WiFi on interface {i}: {result}"); + } + else + { + Debug.WriteLine($"Successfully disconnected from WiFi on interface: {interfaceInfo.strInterfaceDescription}"); + Console.WriteLine("Disconnected from WiFi"); + } + } + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + finally + { + if (wlanInterfaceList != IntPtr.Zero) + { + WlanFreeMemory(wlanInterfaceList); + } + + if (clientHandle != IntPtr.Zero) + { + WlanCloseHandle(clientHandle, IntPtr.Zero); + } + } + } } diff --git a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs index 31ea24e807..55724699d5 100644 --- a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs +++ b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs @@ -1,7 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Windows; +using autoShell.Services; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; @@ -12,6 +18,27 @@ namespace autoShell.Handlers; /// internal class VirtualDesktopCommandHandler : ICommandHandler { + private readonly IAppRegistry _appRegistry; + private readonly IServiceProvider10 _shell; + private readonly IVirtualDesktopManagerInternal _virtualDesktopManagerInternal; + private readonly IVirtualDesktopManagerInternal_BUGBUG _virtualDesktopManagerInternal_BUGBUG; + private readonly IVirtualDesktopManager _virtualDesktopManager; + private readonly IApplicationViewCollection _applicationViewCollection; + private readonly IVirtualDesktopPinnedApps _virtualDesktopPinnedApps; + + public VirtualDesktopCommandHandler(IAppRegistry appRegistry) + { + _appRegistry = appRegistry; + + // Desktop management COM initialization + _shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ImmersiveShell)); + _virtualDesktopManagerInternal = (IVirtualDesktopManagerInternal)_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); + _virtualDesktopManagerInternal_BUGBUG = (IVirtualDesktopManagerInternal_BUGBUG)_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); + _virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_VirtualDesktopManager)); + _applicationViewCollection = (IApplicationViewCollection)_shell.QueryService(typeof(IApplicationViewCollection).GUID, typeof(IApplicationViewCollection).GUID); + _virtualDesktopPinnedApps = (IVirtualDesktopPinnedApps)_shell.QueryService(CLSID_VirtualDesktopPinnedApps, typeof(IVirtualDesktopPinnedApps).GUID); + } + /// public IEnumerable SupportedCommands { get; } = [ @@ -29,28 +56,527 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "CreateDesktop": - AutoShell.CreateDesktop(value); + CreateDesktop(value); break; case "SwitchDesktop": - AutoShell.SwitchDesktop(value); + SwitchDesktop(value); break; case "NextDesktop": - AutoShell.BumpDesktopIndex(1); + BumpDesktopIndex(1); break; case "PreviousDesktop": - AutoShell.BumpDesktopIndex(-1); + BumpDesktopIndex(-1); break; case "MoveWindowToDesktop": - AutoShell.MoveWindowToDesktop(rawValue); + MoveWindowToDesktop(rawValue); break; case "PinWindow": - AutoShell.PinWindow(value); + PinWindow(value); break; } } + + #region Virtual Desktop Methods + + /// + /// Creates virtual desktops from a JSON array of desktop names. + /// + /// JSON array containing desktop names, e.g., ["Work", "Personal", "Gaming"] + private void CreateDesktop(string jsonValue) + { + try + { + // Parse the JSON array of desktop names + JArray desktopNames = JArray.Parse(jsonValue); + + if (desktopNames == null || desktopNames.Count == 0) + { + desktopNames = ["desktop X"]; + } + + if (_virtualDesktopManagerInternal == null) + { + Debug.WriteLine($"Failed to get Virtual Desktop Manager Internal"); + return; + } + + foreach (JToken desktopNameToken in desktopNames) + { + string desktopName = desktopNameToken.ToString(); + + + try + { + // Create a new virtual desktop + IVirtualDesktop newDesktop = _virtualDesktopManagerInternal.CreateDesktop(); + + if (newDesktop != null) + { + // Set the desktop name (Windows 10 build 20231+ / Windows 11) + try + { + // TODO: debug & get working + // Works in .NET framework but not .NET + //s_virtualDesktopManagerInternal_BUGBUG.SetDesktopName(newDesktop, desktopName); + //Debug.WriteLine($"Created virtual desktop: {desktopName}"); + } + catch (Exception ex2) + { + // Older Windows version - name setting not supported + Debug.WriteLine($"Created virtual desktop (naming not supported on this Windows version): {ex2.Message}"); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to create desktop '{desktopName}': {ex.Message}"); + } + } + } + catch (JsonException ex) + { + Debug.WriteLine($"Failed to parse desktop names JSON: {ex.Message}"); + } + catch (Exception ex) + { + Debug.WriteLine($"Error creating desktops: {ex.Message}"); + } + } + + private void SwitchDesktop(string desktopIdentifier) + { + if (!int.TryParse(desktopIdentifier, out int index)) + { + // Try to find the desktop by name + _virtualDesktopManagerInternal.SwitchDesktop(FindDesktopByName(desktopIdentifier)); + } + else + { + SwitchDesktop(index); + } + } + + private void SwitchDesktop(int index) + { + _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + desktops.GetAt(index, typeof(IVirtualDesktop).GUID, out object od); + + // BUGBUG: different windows versions use different COM interfaces + // Different Windows versions use different COM interfaces for desktop switching + // Windows 11 22H2 (build 22621) and later use the updated interface + if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22621)) + { + // Use the BUGBUG interface for Windows 11 22H2+ + _virtualDesktopManagerInternal_BUGBUG.SwitchDesktopWithAnimation((IVirtualDesktop)od); + } + else if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) + { + // Windows 11 21H2 (build 22000) + _virtualDesktopManagerInternal.SwitchDesktopWithAnimation((IVirtualDesktop)od); + } + else + { + // Windows 10 - use the original interface + _virtualDesktopManagerInternal.SwitchDesktopAndMoveForegroundView((IVirtualDesktop)od); + } + + Marshal.ReleaseComObject(desktops); + } + + private void BumpDesktopIndex(int bump) + { + IVirtualDesktop desktop = _virtualDesktopManagerInternal.GetCurrentDesktop(); + int index = GetDesktopIndex(desktop); + int count = _virtualDesktopManagerInternal.GetCount(); + + if (index == -1) + { + Debug.WriteLine("Undable to get the index of the current desktop"); + return; + } + + index += bump; + + if (index > count) + { + index = 0; + } + else if (index < 0) + { + index = count - 1; + } + + SwitchDesktop(index); + } + + private IVirtualDesktop FindDesktopByName(string name) + { + int count = _virtualDesktopManagerInternal.GetCount(); + + _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + for (int i = 0; i < count; i++) + { + desktops.GetAt(i, typeof(IVirtualDesktop).GUID, out object od); + + if (string.Equals(((IVirtualDesktop)od).GetName(), name, StringComparison.OrdinalIgnoreCase)) + { + Marshal.ReleaseComObject(desktops); + return (IVirtualDesktop)od; + } + } + + Marshal.ReleaseComObject(desktops); + + return null; + } + + private int GetDesktopIndex(IVirtualDesktop desktop) + { + int index = -1; + int count = _virtualDesktopManagerInternal.GetCount(); + + _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + for (int i = 0; i < count; i++) + { + desktops.GetAt(i, typeof(IVirtualDesktop).GUID, out object od); + + if (desktop.GetId() == ((IVirtualDesktop)od).GetId()) + { + Marshal.ReleaseComObject(desktops); + return i; + } + } + + Marshal.ReleaseComObject(desktops); + + return -1; + } + + /// + /// + /// + /// + /// Currently not working correction, returns ACCESS_DENIED // TODO: investigate + private void MoveWindowToDesktop(JToken value) + { + string process = value.SelectToken("process").ToString(); + string desktop = value.SelectToken("desktop").ToString(); + if (string.IsNullOrEmpty(process)) + { + Debug.WriteLine("No process name supplied"); + return; + } + + if (string.IsNullOrEmpty(desktop)) + { + Debug.WriteLine("No desktop id supplied"); + return; + } + + IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(process, _appRegistry); + + if (int.TryParse(desktop, out int desktopIndex)) + { + _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + if (desktopIndex < 1 || desktopIndex > _virtualDesktopManagerInternal.GetCount()) + { + Debug.WriteLine("Desktop index out of range"); + Marshal.ReleaseComObject(desktops); + return; + } + desktops.GetAt(desktopIndex - 1, typeof(IVirtualDesktop).GUID, out object od); + Guid g = ((IVirtualDesktop)od).GetId(); + _virtualDesktopManager.MoveWindowToDesktop(hWnd, ref g); + Marshal.ReleaseComObject(desktops); + return; + } + + IVirtualDesktop ivd = FindDesktopByName(desktop); + if (ivd is not null) + { + Guid desktopGuid = ivd.GetId(); + _virtualDesktopManager.MoveWindowToDesktop(hWnd, ref desktopGuid); + } + } + + private void PinWindow(string processName) + { + IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(processName, _appRegistry); + + if (hWnd != IntPtr.Zero) + { + _applicationViewCollection.GetViewForHwnd(hWnd, out IApplicationView view); + + if (view is not null) + { + _virtualDesktopPinnedApps.PinView((IApplicationView)view); + } + } + else + { + Console.WriteLine($"The window handle for '{processName}' could not be found"); + } + } + + private IVirtualDesktopManagerInternal GetVirtualDesktopManagerInternal() + { + try + { + IServiceProvider shellServiceProvider = (IServiceProvider)Activator.CreateInstance( + Type.GetTypeFromCLSID(CLSID_ImmersiveShell)); + + Guid guidService = CLSID_VirtualDesktopManagerInternal; + Guid riid = typeof(IVirtualDesktopManagerInternal).GUID; + shellServiceProvider.QueryService( + ref guidService, + ref riid, + out object objVirtualDesktopManagerInternal); + + return (IVirtualDesktopManagerInternal)objVirtualDesktopManagerInternal; + } + catch + { + return null; + } + } + + #endregion + + #region Virtual Desktop COM Interfaces + + private enum APPLICATION_VIEW_CLOAK_TYPE : int + { + AVCT_NONE = 0, + AVCT_DEFAULT = 1, + AVCT_VIRTUAL_DESKTOP = 2 + } + + private enum APPLICATION_VIEW_COMPATIBILITY_POLICY : int + { + AVCP_NONE = 0, + AVCP_SMALL_SCREEN = 1, + AVCP_TABLET_SMALL_SCREEN = 2, + AVCP_VERY_SMALL_SCREEN = 3, + AVCP_HIGH_SCALE_FACTOR = 4 + } + + // Virtual Desktop COM Interface GUIDs + private static readonly Guid CLSID_ImmersiveShell = new Guid("C2F03A33-21F5-47FA-B4BB-156362A2F239"); + private static readonly Guid CLSID_VirtualDesktopManagerInternal = new Guid("C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B"); + private static readonly Guid CLSID_VirtualDesktopManager = new Guid("AA509086-5CA9-4C25-8F95-589D3C07B48A"); + private static readonly Guid CLSID_VirtualDesktopPinnedApps = new Guid("B5A399E7-1C87-46B8-88E9-FC5747B171BD"); + + // IServiceProvider COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] + private interface IServiceProvider + { + [return: MarshalAs(UnmanagedType.IUnknown)] + void QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject); + } + + // IVirtualDesktopManager COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("A5CD92FF-29BE-454C-8D04-D82879FB3F1B")] + private interface IVirtualDesktopManager + { + bool IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow); + Guid GetWindowDesktopId(IntPtr topLevelWindow); + void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId); + } + + // IVirtualDesktop COM Interface (Windows 10/11) + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] + private interface IVirtualDesktop + { + bool IsViewVisible(IApplicationView view); + Guid GetId(); + // TODO: proper HSTRING custom marshaling + [return: MarshalAs(UnmanagedType.HString)] + string GetName(); + [return: MarshalAs(UnmanagedType.HString)] + string GetWallpaperPath(); + bool IsRemote(); + } + + // IVirtualDesktopManagerInternal COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("53F5CA0B-158F-4124-900C-057158060B27")] + private interface IVirtualDesktopManagerInternal_BUGBUG + { + int GetCount(); + void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); + bool CanViewMoveDesktops(IApplicationView view); + IVirtualDesktop GetCurrentDesktop(); + void GetDesktops(out IObjectArray desktops); + [PreserveSig] + int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); + void SwitchDesktop(IVirtualDesktop desktop); + IVirtualDesktop CreateDesktop(); + void MoveDesktop(IVirtualDesktop desktop, int nIndex); + void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); + IVirtualDesktop FindDesktop(ref Guid desktopid); + void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); + void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); + void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); + void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); + void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); + void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); + void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); + void SwitchDesktopWithAnimation(IVirtualDesktop desktop); + void GetLastActiveDesktop(out IVirtualDesktop desktop); + void WaitForAnimationToComplete(); + } + + // IVirtualDesktopManagerInternal COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("53F5CA0B-158F-4124-900C-057158060B27")] + private interface IVirtualDesktopManagerInternal + { + int GetCount(); + void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); + bool CanViewMoveDesktops(IApplicationView view); + IVirtualDesktop GetCurrentDesktop(); + void GetDesktops(out IObjectArray desktops); + [PreserveSig] + int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); + void SwitchDesktop(IVirtualDesktop desktop); + void SwitchDesktopAndMoveForegroundView(IVirtualDesktop desktop); + IVirtualDesktop CreateDesktop(); + void MoveDesktop(IVirtualDesktop desktop, int nIndex); + void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); + IVirtualDesktop FindDesktop(ref Guid desktopid); + void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); + void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); + void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); + void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); + void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); + void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); + void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); + void SwitchDesktopWithAnimation(IVirtualDesktop desktop); + void GetLastActiveDesktop(out IVirtualDesktop desktop); + void WaitForAnimationToComplete(); + } + + // IObjectArray COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("92CA9DCD-5622-4BBA-A805-5E9F541BD8C9")] + private interface IObjectArray + { + void GetCount(out int pcObjects); + void GetAt(int uiIndex, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("372E1D3B-38D3-42E4-A15B-8AB2B178F513")] + private interface IApplicationView + { + int SetFocus(); + int SwitchTo(); + int TryInvokeBack(IntPtr /* IAsyncCallback* */ callback); + int GetThumbnailWindow(out IntPtr hwnd); + int GetMonitor(out IntPtr /* IImmersiveMonitor */ immersiveMonitor); + int GetVisibility(out int visibility); + int SetCloak(APPLICATION_VIEW_CLOAK_TYPE cloakType, int unknown); + int GetPosition(ref Guid guid /* GUID for IApplicationViewPosition */, out IntPtr /* IApplicationViewPosition** */ position); + int SetPosition(ref IntPtr /* IApplicationViewPosition* */ position); + int InsertAfterWindow(IntPtr hwnd); + int GetExtendedFramePosition(out Rect rect); + int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string id); + int SetAppUserModelId(string id); + int IsEqualByAppUserModelId(string id, out int result); + int GetViewState(out uint state); + int SetViewState(uint state); + int GetNeediness(out int neediness); + int GetLastActivationTimestamp(out ulong timestamp); + int SetLastActivationTimestamp(ulong timestamp); + int GetVirtualDesktopId(out Guid guid); + int SetVirtualDesktopId(ref Guid guid); + int GetShowInSwitchers(out int flag); + int SetShowInSwitchers(int flag); + int GetScaleFactor(out int factor); + int CanReceiveInput(out bool canReceiveInput); + int GetCompatibilityPolicyType(out APPLICATION_VIEW_COMPATIBILITY_POLICY flags); + int SetCompatibilityPolicyType(APPLICATION_VIEW_COMPATIBILITY_POLICY flags); + int GetSizeConstraints(IntPtr /* IImmersiveMonitor* */ monitor, out Size size1, out Size size2); + int GetSizeConstraintsForDpi(uint uint1, out Size size1, out Size size2); + int SetSizeConstraintsForDpi(ref uint uint1, ref Size size1, ref Size size2); + int OnMinSizePreferencesUpdated(IntPtr hwnd); + int ApplyOperation(IntPtr /* IApplicationViewOperation* */ operation); + int IsTray(out bool isTray); + int IsInHighZOrderBand(out bool isInHighZOrderBand); + int IsSplashScreenPresented(out bool isSplashScreenPresented); + int Flash(); + int GetRootSwitchableOwner(out IApplicationView rootSwitchableOwner); + int EnumerateOwnershipTree(out IObjectArray ownershipTree); + int GetEnterpriseId([MarshalAs(UnmanagedType.LPWStr)] out string enterpriseId); + int IsMirrored(out bool isMirrored); + int Unknown1(out int unknown); + int Unknown2(out int unknown); + int Unknown3(out int unknown); + int Unknown4(out int unknown); + int Unknown5(out int unknown); + int Unknown6(int unknown); + int Unknown7(); + int Unknown8(out int unknown); + int Unknown9(int unknown); + int Unknown10(int unknownX, int unknownY); + int Unknown11(int unknown); + int Unknown12(out Size size1); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("1841C6D7-4F9D-42C0-AF41-8747538F10E5")] + private interface IApplicationViewCollection + { + int GetViews(out IObjectArray array); + int GetViewsByZOrder(out IObjectArray array); + int GetViewsByAppUserModelId(string id, out IObjectArray array); + int GetViewForHwnd(IntPtr hwnd, out IApplicationView view); + int GetViewForApplication(object application, out IApplicationView view); + int GetViewForAppUserModelId(string id, out IApplicationView view); + int GetViewInFocus(out IntPtr view); + int Unknown1(out IntPtr view); + void RefreshCollection(); + int RegisterForApplicationViewChanges(object listener, out int cookie); + int UnregisterForApplicationViewChanges(int cookie); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("4CE81583-1E4C-4632-A621-07A53543148F")] + private interface IVirtualDesktopPinnedApps + { + bool IsAppIdPinned(string appId); + void PinAppID(string appId); + void UnpinAppID(string appId); + bool IsViewPinned(IApplicationView applicationView); + void PinView(IApplicationView applicationView); + void UnpinView(IApplicationView applicationView); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] + private interface IServiceProvider10 + { + [return: MarshalAs(UnmanagedType.IUnknown)] + object QueryService(ref Guid service, ref Guid riid); + } + + #endregion } diff --git a/dotnet/autoShell/Handlers/WindowCommandHandler.cs b/dotnet/autoShell/Handlers/WindowCommandHandler.cs index a16aa69205..b4aca14e7b 100644 --- a/dotnet/autoShell/Handlers/WindowCommandHandler.cs +++ b/dotnet/autoShell/Handlers/WindowCommandHandler.cs @@ -1,7 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using autoShell.Services; +using Microsoft.VisualBasic; using Newtonsoft.Json.Linq; namespace autoShell.Handlers; @@ -11,6 +17,13 @@ namespace autoShell.Handlers; /// internal class WindowCommandHandler : ICommandHandler { + private readonly IAppRegistry _appRegistry; + + public WindowCommandHandler(IAppRegistry appRegistry) + { + _appRegistry = appRegistry; + } + /// public IEnumerable SupportedCommands { get; } = [ @@ -26,24 +39,304 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "Maximize": - AutoShell.MaximizeWindow(value); + MaximizeWindow(value); break; case "Minimize": - AutoShell.MinimizeWindow(value); + MinimizeWindow(value); break; case "SwitchTo": - AutoShell.RaiseWindow(value); + RaiseWindow(value, _appRegistry); break; case "Tile": string[] apps = value.Split(','); if (apps.Length == 2) { - AutoShell.TileWindowPair(apps[0], apps[1]); + TileWindowPair(apps[0], apps[1]); } break; } } + + internal static void RaiseWindow(string friendlyName, IAppRegistry appRegistry) + { + string processName = appRegistry.ResolveProcessName(friendlyName); + Process[] processes = Process.GetProcessesByName(processName); + foreach (Process p in processes) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + SetForegroundWindow(p.MainWindowHandle); + Interaction.AppActivate(p.Id); + return; + } + } + + // All processes are background-only (e.g. Edge, Chrome). Try launching by path. + string path = appRegistry.GetExecutablePath(friendlyName); + if (path != null) + { + Process.Start(path); + } + else + { + // Try to find by window title + (nint hWnd1, int pid) = FindWindowByTitle(processName); + if (hWnd1 != nint.Zero) + { + SetForegroundWindow(hWnd1); + Interaction.AppActivate(pid); + } + } + } + + private void MaximizeWindow(string friendlyName) + { + string processName = _appRegistry.ResolveProcessName(friendlyName); + Process[] processes = Process.GetProcessesByName(processName); + foreach (Process p in processes) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero); + SetForegroundWindow(p.MainWindowHandle); + Interaction.AppActivate(p.Id); + return; + } + } + + (nint hWnd, int pid) = FindWindowByTitle(processName); + if (hWnd != nint.Zero) + { + SendMessage(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero); + SetForegroundWindow(hWnd); + Interaction.AppActivate(pid); + } + } + + private void MinimizeWindow(string friendlyName) + { + string processName = _appRegistry.ResolveProcessName(friendlyName); + Process[] processes = Process.GetProcessesByName(processName); + foreach (Process p in processes) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero); + break; + } + } + + (nint hWnd, int pid) = FindWindowByTitle(processName); + if (hWnd != nint.Zero) + { + SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero); + SetForegroundWindow(hWnd); + Interaction.AppActivate(pid); + } + } + + private void TileWindowPair(string name1, string name2) + { + // TODO: Update this to account for UWP apps (e.g. calculator). UWPs are hosted by ApplicationFrameHost.exe + string processName1 = _appRegistry.ResolveProcessName(name1); + Process[] processes1 = Process.GetProcessesByName(processName1); + IntPtr hWnd1 = IntPtr.Zero; + IntPtr hWnd2 = IntPtr.Zero; + int pid1 = -1; + int pid2 = -1; + + foreach (Process p in processes1) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + hWnd1 = p.MainWindowHandle; + pid1 = p.Id; + break; + } + } + + if (hWnd1 == IntPtr.Zero) + { + (hWnd1, pid1) = FindWindowByTitle(processName1); + } + + string processName2 = _appRegistry.ResolveProcessName(name2); + Process[] processes2 = Process.GetProcessesByName(processName2); + foreach (Process p in processes2) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + hWnd2 = p.MainWindowHandle; + pid2 = p.Id; + break; + } + } + + if (hWnd2 == IntPtr.Zero) + { + (hWnd2, pid2) = FindWindowByTitle(processName2); + } + + if (hWnd1 != IntPtr.Zero && hWnd2 != IntPtr.Zero) + { + // TODO: handle multiple monitors + IntPtr desktopHandle = GetDesktopWindow(); + RECT desktopRect = new RECT(); + GetWindowRect(desktopHandle, ref desktopRect); + + // Find the taskbar to subtract its height + IntPtr taskbarHandle = IntPtr.Zero; + IntPtr hWnd = IntPtr.Zero; + while ((hWnd = FindWindowEx(IntPtr.Zero, hWnd, "Shell_TrayWnd", null)) != IntPtr.Zero) + { + taskbarHandle = FindWindowEx(hWnd, IntPtr.Zero, "ReBarWindow32", null); + if (taskbarHandle != IntPtr.Zero) + { + break; + } + } + if (hWnd == IntPtr.Zero) + { + Debug.WriteLine("Taskbar not found"); + return; + } + else + { + RECT taskbarRect = new RECT(); + GetWindowRect(hWnd, ref taskbarRect); + Debug.WriteLine("Taskbar Rect: " + taskbarRect.Left + ", " + taskbarRect.Top + ", " + taskbarRect.Right + ", " + taskbarRect.Bottom); + // TODO: handle left, top, right and nonexistent taskbars + desktopRect.Bottom -= (int)((taskbarRect.Bottom - taskbarRect.Top) / 2); + } + + int halfwidth = (desktopRect.Right - desktopRect.Left) / 2; + int height = desktopRect.Bottom - desktopRect.Top; + IntPtr HWND_TOP = IntPtr.Zero; + uint showWindow = 0x40; + + // Restore windows first (SetWindowPos won't work on maximized windows) + uint SW_RESTORE = 9; + ShowWindow(hWnd1, SW_RESTORE); + ShowWindow(hWnd2, SW_RESTORE); + + SetWindowPos(hWnd1, HWND_TOP, desktopRect.Left, desktopRect.Top, halfwidth, height, showWindow); + SetForegroundWindow(hWnd1); + Interaction.AppActivate(pid1); + SetWindowPos(hWnd2, HWND_TOP, desktopRect.Left + halfwidth, desktopRect.Top, halfwidth, height, showWindow); + SetForegroundWindow(hWnd2); + Interaction.AppActivate(pid2); + } + } + + #region Shared window helpers + + /// + /// Finds a top-level window by partial title match (case-insensitive). + /// Shared with VirtualDesktopCommandHandler. + /// + internal static (IntPtr hWnd, int pid) FindWindowByTitle(string titleSearch) + { + IntPtr foundHandle = IntPtr.Zero; + int foundPid = -1; + StringBuilder windowTitle = new StringBuilder(256); + + EnumWindows((hWnd, lParam) => + { + if (!IsWindowVisible(hWnd)) + { + return true; + } + + int length = GetWindowText(hWnd, windowTitle, windowTitle.Capacity); + if (length > 0) + { + string title = windowTitle.ToString(); + if (title.Contains(titleSearch, StringComparison.OrdinalIgnoreCase)) + { + foundHandle = hWnd; + GetWindowThreadProcessId(hWnd, out uint pid); + foundPid = (int)pid; + return false; + } + } + return true; + }, IntPtr.Zero); + + return (foundHandle, foundPid); + } + + /// + /// Finds the main window handle for a process, resolving friendly names via the app registry. + /// Shared with VirtualDesktopCommandHandler. + /// + internal static IntPtr FindProcessWindowHandle(string processName, IAppRegistry appRegistry) + { + processName = appRegistry.ResolveProcessName(processName); + Process[] processes = Process.GetProcessesByName(processName); + foreach (Process p in processes) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + return p.MainWindowHandle; + } + } + + return FindWindowByTitle(processName).hWnd; + } + + #endregion + + #region P/Invoke + + private const uint WM_SYSCOMMAND = 0x112; + private const uint SC_MAXIMIZE = 0xF030; + private const uint SC_MINIMIZE = 0xF020; + + internal struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hWnd, ref RECT rect); + + [DllImport("user32.dll")] + private static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll", EntryPoint = "SendMessage", SetLastError = true)] + private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, uint wParam, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow); + + [DllImport("user32.dll")] + internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName); + + internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + #endregion } diff --git a/dotnet/autoShell/Services/IAppRegistry.cs b/dotnet/autoShell/Services/IAppRegistry.cs new file mode 100644 index 0000000000..39ea911f04 --- /dev/null +++ b/dotnet/autoShell/Services/IAppRegistry.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace autoShell.Services; + +/// +/// Registry of known applications, mapping friendly names to executable paths, +/// AppUserModelIDs, and startup metadata. Shared across handlers that need +/// to resolve, launch, or manipulate applications by friendly name. +/// +internal interface IAppRegistry +{ + /// + /// Gets the executable path for a friendly app name, or null if unknown. + /// + string GetExecutablePath(string friendlyName); + + /// + /// Gets the AppUserModelID for a friendly app name, or null if unknown. + /// Used as a fallback to launch apps via the shell AppsFolder. + /// + string GetAppUserModelId(string friendlyName); + + /// + /// Resolves a friendly name to a process name (filename without extension). + /// Returns the input unchanged if the friendly name is not in the registry. + /// + string ResolveProcessName(string friendlyName); + + /// + /// Gets the working directory environment variable for a friendly app name, or null. + /// The caller should expand it via Environment.ExpandEnvironmentVariables. + /// + string GetWorkingDirectoryEnvVar(string friendlyName); + + /// + /// Gets extra command-line arguments for a friendly app name, or null. + /// + string GetArguments(string friendlyName); + + /// + /// Returns all known installed application names (from the shell AppsFolder). + /// + IEnumerable GetAllAppNames(); +} diff --git a/dotnet/autoShell/Services/WindowsAppRegistry.cs b/dotnet/autoShell/Services/WindowsAppRegistry.cs new file mode 100644 index 0000000000..1de8c4d52e --- /dev/null +++ b/dotnet/autoShell/Services/WindowsAppRegistry.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.WindowsAPICodePack.Shell; + +namespace autoShell.Services; + +/// +/// Windows implementation of . +/// Builds lookups from a hardcoded list of well-known apps and +/// dynamically discovered AppUserModelIDs from the shell AppsFolder. +/// +internal sealed class WindowsAppRegistry : IAppRegistry +{ + private readonly Hashtable _friendlyNameToPath = []; + private readonly Hashtable _friendlyNameToId = []; + private readonly SortedList _appMetadata; + + public WindowsAppRegistry() + { + string userName = Environment.UserName; + + _appMetadata = new SortedList + { + { "chrome", ["chrome.exe"] }, + { "power point", ["C:\\Program Files\\Microsoft Office\\root\\Office16\\POWERPNT.EXE"] }, + { "powerpoint", ["C:\\Program Files\\Microsoft Office\\root\\Office16\\POWERPNT.EXE"] }, + { "word", ["C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE"] }, + { "winword", ["C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE"] }, + { "excel", ["C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE"] }, + { "outlook", ["C:\\Program Files\\Microsoft Office\\root\\Office16\\OUTLOOK.EXE"] }, + { "visual studio", ["devenv.exe"] }, + { "visual studio code", [$"C:\\Users\\{userName}\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe"] }, + { "edge", ["C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"] }, + { "microsoft edge", ["C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"] }, + { "notepad", ["C:\\Windows\\System32\\notepad.exe"] }, + { "paint", ["mspaint.exe"] }, + { "calculator", ["calc.exe"] }, + { "file explorer", ["C:\\Windows\\explorer.exe"] }, + { "control panel", ["C:\\Windows\\System32\\control.exe"] }, + { "task manager", ["C:\\Windows\\System32\\Taskmgr.exe"] }, + { "cmd", ["C:\\Windows\\System32\\cmd.exe"] }, + { "powershell", ["C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"] }, + { "snipping tool", ["C:\\Windows\\System32\\SnippingTool.exe"] }, + { "magnifier", ["C:\\Windows\\System32\\Magnify.exe"] }, + { "paint 3d", ["C:\\Program Files\\WindowsApps\\Microsoft.MSPaint_10.1807.18022.0_x64__8wekyb3d8bbwe\\"] }, + { "m365 copilot", ["C:\\Program Files\\WindowsApps\\Microsoft.MicrosoftOfficeHub_19.2512.45041.0_x64__8wekyb3d8bbwe\\M365Copilot.exe"] }, + { "copilot", ["C:\\Program Files\\WindowsApps\\Microsoft.MicrosoftOfficeHub_19.2512.45041.0_x64__8wekyb3d8bbwe\\M365Copilot.exe"] }, + { "spotify", ["C:\\Program Files\\WindowsApps\\SpotifyAB.SpotifyMusic_1.278.418.0_x64__zpdnekdrzrea0\\spotify.exe"] }, + { "github copilot", [$"{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"] }, + }; + + foreach (var kvp in _appMetadata) + { + _friendlyNameToPath.Add(kvp.Key, kvp.Value[0]); + } + + try + { + var installedApps = GetAllInstalledAppIds(); + foreach (var kvp in installedApps) + { + _friendlyNameToId.Add(kvp.Key, kvp.Value); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to enumerate installed apps: {ex.Message}"); + } + } + + public string GetExecutablePath(string friendlyName) + { + return (string)_friendlyNameToPath[friendlyName.ToLowerInvariant()]; + } + + public string GetAppUserModelId(string friendlyName) + { + return (string)_friendlyNameToId[friendlyName.ToLowerInvariant()]; + } + + public string ResolveProcessName(string friendlyName) + { + string path = GetExecutablePath(friendlyName); + return path != null ? Path.GetFileNameWithoutExtension(path) : friendlyName; + } + + public string GetWorkingDirectoryEnvVar(string friendlyName) + { + if (_appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 1) + { + return value[1]; + } + return null; + } + + public string GetArguments(string friendlyName) + { + if (_appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 2) + { + return string.Join(" ", value.Skip(2)); + } + return null; + } + + public IEnumerable GetAllAppNames() + { + return _friendlyNameToId.Keys.Cast(); + } + + private static SortedList GetAllInstalledAppIds() + { + var FOLDERID_AppsFolder = new Guid("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}"); + ShellObject appsFolder = (ShellObject)KnownFolderHelper.FromKnownFolderId(FOLDERID_AppsFolder); + var appIds = new SortedList(); + + foreach (var app in (IKnownFolder)appsFolder) + { + string appName = app.Name.ToLowerInvariant(); + if (appIds.ContainsKey(appName)) + { + Debug.WriteLine("Key has multiple values: " + appName); + } + else + { + appIds.Add(appName, app.ParsingName); + } + } + + return appIds; + } +} diff --git a/dotnet/autoShell/UIAutomation.cs b/dotnet/autoShell/UIAutomation.cs index 5a78c70a8d..e579c63052 100644 --- a/dotnet/autoShell/UIAutomation.cs +++ b/dotnet/autoShell/UIAutomation.cs @@ -47,10 +47,10 @@ internal static void SetTextSizeViaUIAutomation(int percentage) // UWP apps use ApplicationFrameWindow class IntPtr hWnd = IntPtr.Zero; while ((hWnd = - AutoShell.FindWindowEx(IntPtr.Zero, hWnd, "ApplicationFrameWindow", null)) != IntPtr.Zero) + Handlers.WindowCommandHandler.FindWindowEx(IntPtr.Zero, hWnd, "ApplicationFrameWindow", null)) != IntPtr.Zero) { StringBuilder windowTitle = new StringBuilder(256); - int hr = AutoShell.GetWindowText(hWnd, windowTitle, windowTitle.Capacity); + int hr = Handlers.WindowCommandHandler.GetWindowText(hWnd, windowTitle, windowTitle.Capacity); Debug.WriteLine(windowTitle + $"(hResult: {hr})"); if (windowTitle.ToString().Contains("Settings", StringComparison.OrdinalIgnoreCase)) { From d1003af71394eec5644d72e68bfc283a44c29631 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 1 Apr 2026 21:15:56 -0700 Subject: [PATCH 10/27] Adding tests --- .../autoShell.Tests/AppCommandHandlerTests.cs | 118 +++ .../HandlerRegistrationTests.cs | 67 ++ .../autoShell.Tests/SettingsHandlerTests.cs | 769 ++++++++++++++++++ .../SystemCommandHandlerTests.cs | 33 + .../ThemeCommandHandlerTests.cs | 75 ++ 5 files changed, 1062 insertions(+) create mode 100644 dotnet/autoShell.Tests/AppCommandHandlerTests.cs create mode 100644 dotnet/autoShell.Tests/SettingsHandlerTests.cs create mode 100644 dotnet/autoShell.Tests/SystemCommandHandlerTests.cs create mode 100644 dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs diff --git a/dotnet/autoShell.Tests/AppCommandHandlerTests.cs b/dotnet/autoShell.Tests/AppCommandHandlerTests.cs new file mode 100644 index 0000000000..61318facfd --- /dev/null +++ b/dotnet/autoShell.Tests/AppCommandHandlerTests.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using autoShell.Handlers; +using autoShell.Services; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +public class AppCommandHandlerTests +{ + private readonly Mock _appRegistryMock = new(); + private readonly Mock _processMock = new(); + private readonly AppCommandHandler _handler; + + public AppCommandHandlerTests() + { + _handler = new AppCommandHandler(_appRegistryMock.Object, _processMock.Object); + } + + // --- LaunchProgram --- + + [Fact] + public void LaunchProgram_AppNotRunning_StartsViaPath() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("chrome")).Returns("chrome"); + _processMock.Setup(p => p.GetProcessesByName("chrome")).Returns(Array.Empty()); + _appRegistryMock.Setup(a => a.GetExecutablePath("chrome")).Returns("chrome.exe"); + + Handle("LaunchProgram", "chrome"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.FileName == "chrome.exe" && psi.UseShellExecute == true)), Times.Once); + } + + [Fact] + public void LaunchProgram_WithWorkingDir_SetsWorkingDirectory() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("github copilot")).Returns("github copilot"); + _processMock.Setup(p => p.GetProcessesByName("github copilot")).Returns(Array.Empty()); + _appRegistryMock.Setup(a => a.GetExecutablePath("github copilot")).Returns("copilot.exe"); + _appRegistryMock.Setup(a => a.GetWorkingDirectoryEnvVar("github copilot")).Returns("GITHUB_COPILOT_ROOT_DIR"); + + Handle("LaunchProgram", "github copilot"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.WorkingDirectory != "")), Times.Once); + } + + [Fact] + public void LaunchProgram_WithArguments_SetsArguments() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("github copilot")).Returns("github copilot"); + _processMock.Setup(p => p.GetProcessesByName("github copilot")).Returns(Array.Empty()); + _appRegistryMock.Setup(a => a.GetExecutablePath("github copilot")).Returns("copilot.exe"); + _appRegistryMock.Setup(a => a.GetArguments("github copilot")).Returns("--allow-all-tools"); + + Handle("LaunchProgram", "github copilot"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.Arguments == "--allow-all-tools")), Times.Once); + } + + [Fact] + public void LaunchProgram_NoPath_UsesAppUserModelId() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("calculator")).Returns("calculator"); + _processMock.Setup(p => p.GetProcessesByName("calculator")).Returns(Array.Empty()); + _appRegistryMock.Setup(a => a.GetExecutablePath("calculator")).Returns((string)null!); + _appRegistryMock.Setup(a => a.GetAppUserModelId("calculator")).Returns("Microsoft.WindowsCalculator"); + + Handle("LaunchProgram", "calculator"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.FileName == "explorer.exe")), Times.Once); + } + + [Fact] + public void CloseProgram_RunningProcess_CallsGetProcessesByName() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); + // Return a real (albeit useless in test) empty array to avoid null ref; + // We cannot easily mock Process objects, so we verify the lookup was attempted. + _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns(Array.Empty()); + + Handle("CloseProgram", "notepad"); + + _processMock.Verify(p => p.GetProcessesByName("notepad"), Times.Once); + } + + [Fact] + public void CloseProgram_NotRunning_DoesNothing() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); + _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns(Array.Empty()); + + var ex = Record.Exception(() => Handle("CloseProgram", "notepad")); + + Assert.Null(ex); + } + + [Fact] + public void ListAppNames_CallsGetAllAppNames() + { + _appRegistryMock.Setup(a => a.GetAllAppNames()).Returns(new List { "notepad", "chrome" }); + + Handle("ListAppNames", ""); + + _appRegistryMock.Verify(a => a.GetAllAppNames(), Times.Once); + } + + private void Handle(string key, string value) + { + _handler.Handle(key, value, JToken.FromObject(value)); + } +} diff --git a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs index 125869c024..a4114d521e 100644 --- a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs +++ b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs @@ -95,4 +95,71 @@ public void AllHandlers_HaveNoDuplicateCommandsAcrossHandlers() $"Duplicate commands across handlers: {string.Join("; ", duplicates)}"); } + [Fact] + public void AllCommands_HaveAtLeastOneUnitTest() + { + // Commands that use P/Invoke, COM, or static APIs directly and cannot be + // unit-tested without further abstraction layers. + var untestableCommands = new HashSet(StringComparer.Ordinal) + { + // WindowCommandHandler — direct P/Invoke + "Maximize", "Minimize", "SwitchTo", "Tile", + // VirtualDesktopCommandHandler — COM interop + "CreateDesktop", "MoveWindowToDesktop", "NextDesktop", + "PinWindow", "PreviousDesktop", "SwitchDesktop", + // NetworkCommandHandler — WLAN P/Invoke + COM + "BluetoothToggle", "ConnectWifi", "DisconnectWifi", + "EnableMeteredConnections", "EnableWifi", "ListWifiNetworks", "ToggleAirplaneMode", + // DisplayCommandHandler — direct P/Invoke + "ListResolutions", "SetScreenResolution", "SetTextSize", + // SystemCommandHandler.Debug — Debugger.Launch() + "Debug", + // MouseSettingsHandler.SetPrimaryMouseButton — native SwapMouseButton + "SetPrimaryMouseButton", + // DisplaySettingsHandler.AdjustScreenBrightness — WMI + direct Registry + "AdjustScreenBrightness", + // SystemSettingsHandler.AutomaticDSTAdjustment — direct Registry.LocalMachine + "AutomaticDSTAdjustment", + }; + + // Discover all test classes in this assembly + var testAssembly = typeof(HandlerRegistrationTests).Assembly; + var testMethods = testAssembly.GetTypes() + .Where(t => t.IsClass && t.IsPublic) + .SelectMany(t => t.GetMethods() + .Where(m => m.GetCustomAttributes(typeof(Xunit.FactAttribute), false).Length > 0 + || m.GetCustomAttributes(typeof(Xunit.TheoryAttribute), false).Length > 0) + .Select(m => new { ClassName = t.Name, MethodName = m.Name })) + .ToList(); + + var untested = new List(); + + foreach (var handler in _handlers) + { + string handlerTypeName = handler.GetType().Name; + // Expected test class: "{HandlerTypeName}Tests" + string expectedTestClass = handlerTypeName + "Tests"; + + var classTests = testMethods + .Where(t => t.ClassName == expectedTestClass) + .ToList(); + + foreach (string command in handler.SupportedCommands) + { + bool hasCoverage = classTests.Any(t => + t.MethodName.StartsWith(command + "_", StringComparison.Ordinal) || + t.MethodName.Contains("_" + command + "_", StringComparison.Ordinal)); + + if (!hasCoverage && !untestableCommands.Contains(command)) + { + untested.Add($"{handlerTypeName}.{command}"); + } + } + } + + Assert.True( + untested.Count == 0, + $"Commands missing unit tests ({untested.Count}):\n " + string.Join("\n ", untested)); + } + } diff --git a/dotnet/autoShell.Tests/SettingsHandlerTests.cs b/dotnet/autoShell.Tests/SettingsHandlerTests.cs new file mode 100644 index 0000000000..432cc4eb11 --- /dev/null +++ b/dotnet/autoShell.Tests/SettingsHandlerTests.cs @@ -0,0 +1,769 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using autoShell.Handlers; +using autoShell.Services; +using Microsoft.Win32; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +#region PersonalizationSettingsHandler + +public class PersonalizationSettingsHandlerTests +{ + private readonly Mock _registryMock = new(); + private readonly Mock _processMock = new(); + private readonly PersonalizationSettingsHandler _handler; + + public PersonalizationSettingsHandlerTests() + { + _handler = new PersonalizationSettingsHandler(_registryMock.Object, _processMock.Object); + } + + [Fact] + public void ApplyColorToTitleBar_Enable_SetsColorPrevalence1() + { + Handle("ApplyColorToTitleBar", @"{""enableColor"":true}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\DWM", "ColorPrevalence", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ApplyColorToTitleBar_Disable_SetsColorPrevalence0() + { + Handle("ApplyColorToTitleBar", @"{""enableColor"":false}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\DWM", "ColorPrevalence", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void EnableTransparency_Enable_SetsTransparency1() + { + Handle("EnableTransparency", @"{""enable"":true}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + "EnableTransparency", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void EnableTransparency_Disable_SetsTransparency0() + { + Handle("EnableTransparency", @"{""enable"":false}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + "EnableTransparency", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void SystemThemeMode_Light_SetsBothKeys() + { + Handle("SystemThemeMode", @"{""mode"":""light""}"); + + const string path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(path, "AppsUseLightTheme", 1, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(path, "SystemUsesLightTheme", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void SystemThemeMode_Dark_SetsBothKeys() + { + Handle("SystemThemeMode", @"{""mode"":""dark""}"); + + const string path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(path, "AppsUseLightTheme", 0, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(path, "SystemUsesLightTheme", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void HighContrastTheme_OpensSettings() + { + Handle("HighContrastTheme", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:easeofaccess-highcontrast"), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region PrivacySettingsHandler + +public class PrivacySettingsHandlerTests +{ + private readonly Mock _registryMock = new(); + private readonly PrivacySettingsHandler _handler; + + public PrivacySettingsHandlerTests() + { + _handler = new PrivacySettingsHandler(_registryMock.Object); + } + + [Fact] + public void ManageCameraAccess_Deny_WritesDeny() + { + Handle("ManageCameraAccess", @"{""accessSetting"":""deny""}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam", + "Value", "Deny", RegistryValueKind.String), Times.Once); + } + + [Fact] + public void ManageCameraAccess_Allow_WritesAllow() + { + Handle("ManageCameraAccess", @"{""accessSetting"":""allow""}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam", + "Value", "Allow", RegistryValueKind.String), Times.Once); + } + + [Fact] + public void ManageMicrophoneAccess_Deny_WritesDeny() + { + Handle("ManageMicrophoneAccess", @"{""accessSetting"":""deny""}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone", + "Value", "Deny", RegistryValueKind.String), Times.Once); + } + + [Fact] + public void ManageLocationAccess_Allow_WritesAllow() + { + Handle("ManageLocationAccess", @"{""accessSetting"":""allow""}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location", + "Value", "Allow", RegistryValueKind.String), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region FileExplorerSettingsHandler + +public class FileExplorerSettingsHandlerTests +{ + private readonly Mock _registryMock = new(); + private readonly FileExplorerSettingsHandler _handler; + + public FileExplorerSettingsHandlerTests() + { + _handler = new FileExplorerSettingsHandler(_registryMock.Object); + } + + [Fact] + public void ShowFileExtensions_Enable_SetsHideFileExt0() + { + Handle("ShowFileExtensions", @"{""enable"":true}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced", + "HideFileExt", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ShowFileExtensions_Disable_SetsHideFileExt1() + { + Handle("ShowFileExtensions", @"{""enable"":false}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced", + "HideFileExt", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ShowHiddenAndSystemFiles_Enable_SetsBothKeys() + { + Handle("ShowHiddenAndSystemFiles", @"{""enable"":true}"); + + const string path = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; + _registryMock.Verify(r => r.SetValue(path, "Hidden", 1, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(path, "ShowSuperHidden", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ShowHiddenAndSystemFiles_Disable_SetsBothKeys() + { + Handle("ShowHiddenAndSystemFiles", @"{""enable"":false}"); + + const string path = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; + _registryMock.Verify(r => r.SetValue(path, "Hidden", 2, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(path, "ShowSuperHidden", 0, RegistryValueKind.DWord), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region PowerSettingsHandler + +public class PowerSettingsHandlerTests +{ + private readonly Mock _registryMock = new(); + private readonly Mock _processMock = new(); + private readonly PowerSettingsHandler _handler; + + public PowerSettingsHandlerTests() + { + _handler = new PowerSettingsHandler(_registryMock.Object, _processMock.Object); + } + + [Fact] + public void BatterySaverActivationLevel_SetsThreshold() + { + Handle("BatterySaverActivationLevel", @"{""thresholdValue"":30}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\Power\BatterySaver", + "ActivationThreshold", 30, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void SetPowerModeOnBattery_OpensSettings() + { + Handle("SetPowerModeOnBattery", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:powersleep"), Times.Once); + } + + [Fact] + public void SetPowerModePluggedIn_OpensSettings() + { + Handle("SetPowerModePluggedIn", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:powersleep"), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region AccessibilitySettingsHandler + +public class AccessibilitySettingsHandlerTests +{ + private readonly Mock _registryMock = new(); + private readonly Mock _processMock = new(); + private readonly AccessibilitySettingsHandler _handler; + + public AccessibilitySettingsHandlerTests() + { + _handler = new AccessibilitySettingsHandler(_registryMock.Object, _processMock.Object); + } + + [Fact] + public void EnableStickyKeys_Enable_SetsFlags510() + { + Handle("EnableStickyKeys", @"{""enable"":true}"); + + _registryMock.Verify(r => r.SetValue( + @"Control Panel\Accessibility\StickyKeys", + "Flags", "510", RegistryValueKind.String), Times.Once); + } + + [Fact] + public void EnableStickyKeys_Disable_SetsFlags506() + { + Handle("EnableStickyKeys", @"{""enable"":false}"); + + _registryMock.Verify(r => r.SetValue( + @"Control Panel\Accessibility\StickyKeys", + "Flags", "506", RegistryValueKind.String), Times.Once); + } + + [Fact] + public void EnableFilterKeysAction_Enable_SetsFlags2() + { + Handle("EnableFilterKeysAction", @"{""enable"":true}"); + + _registryMock.Verify(r => r.SetValue( + @"Control Panel\Accessibility\Keyboard Response", + "Flags", "2", RegistryValueKind.String), Times.Once); + } + + [Fact] + public void EnableFilterKeysAction_Disable_SetsFlags126() + { + Handle("EnableFilterKeysAction", @"{""enable"":false}"); + + _registryMock.Verify(r => r.SetValue( + @"Control Panel\Accessibility\Keyboard Response", + "Flags", "126", RegistryValueKind.String), Times.Once); + } + + [Fact] + public void MonoAudioToggle_Enable_SetsMonoMix1() + { + Handle("MonoAudioToggle", @"{""enable"":true}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Multimedia\Audio", + "AccessibilityMonoMixState", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void EnableMagnifier_Enable_StartsProcess() + { + Handle("EnableMagnifier", @"{""enable"":true}"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.FileName == "magnify.exe")), Times.Once); + } + + [Fact] + public void EnableNarratorAction_Enable_StartsNarrator() + { + Handle("EnableNarratorAction", @"{""enable"":true}"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.FileName == "narrator.exe")), Times.Once); + } + + [Fact] + public void EnableNarratorAction_Disable_CallsGetProcessesByName() + { + _processMock.Setup(p => p.GetProcessesByName("Narrator")).Returns(Array.Empty()); + + Handle("EnableNarratorAction", @"{""enable"":false}"); + + _processMock.Verify(p => p.GetProcessesByName("Narrator"), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region MouseSettingsHandler + +public class MouseSettingsHandlerTests +{ + private readonly Mock _systemParamsMock = new(); + private readonly Mock _processMock = new(); + private readonly MouseSettingsHandler _handler; + + public MouseSettingsHandlerTests() + { + _handler = new MouseSettingsHandler(_systemParamsMock.Object, _processMock.Object); + } + + [Fact] + public void MouseCursorSpeed_SetsSpeed() + { + Handle("MouseCursorSpeed", @"{""speedLevel"":10}"); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x0071, 0, (IntPtr)10, 3), Times.Once); + } + + [Fact] + public void MouseWheelScrollLines_SetsLines() + { + Handle("MouseWheelScrollLines", @"{""scrollLines"":5}"); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x0069, 5, IntPtr.Zero, 3), Times.Once); + } + + [Fact] + public void EnhancePointerPrecision_Enable() + { + Handle("EnhancePointerPrecision", @"{""enable"":true}"); + + _systemParamsMock.Verify(s => s.GetParameter(3, 0, It.IsAny(), 0), Times.Once); + _systemParamsMock.Verify(s => s.SetParameter( + 4, 0, It.Is(a => a[2] == 1), 3), Times.Once); + } + + [Fact] + public void EnhancePointerPrecision_Disable() + { + Handle("EnhancePointerPrecision", @"{""enable"":false}"); + + _systemParamsMock.Verify(s => s.GetParameter(3, 0, It.IsAny(), 0), Times.Once); + _systemParamsMock.Verify(s => s.SetParameter( + 4, 0, It.Is(a => a[2] == 0), 3), Times.Once); + } + + [Fact] + public void AdjustMousePointerSize_OpensMouseSettings() + { + Handle("AdjustMousePointerSize", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:easeofaccess-mouse"), Times.Once); + } + + [Fact] + public void EnableTouchPad_OpensTouchpadSettings() + { + Handle("EnableTouchPad", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:devices-touchpad"), Times.Once); + } + + [Fact] + public void MousePointerCustomization_OpensMouseSettings() + { + Handle("MousePointerCustomization", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:easeofaccess-mouse"), Times.Once); + } + + [Fact] + public void TouchpadCursorSpeed_OpensTouchpadSettings() + { + Handle("TouchpadCursorSpeed", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:devices-touchpad"), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region TaskbarSettingsHandler + +public class TaskbarSettingsHandlerTests +{ + private const string ExplorerAdvanced = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; + private const string StuckRects3 = @"Software\Microsoft\Windows\CurrentVersion\Explorer\StuckRects3"; + + private readonly Mock _registryMock = new(); + private readonly TaskbarSettingsHandler _handler; + + public TaskbarSettingsHandlerTests() + { + _handler = new TaskbarSettingsHandler(_registryMock.Object); + } + + [Fact] + public void AutoHideTaskbar_Enable_SetsAutoHideBit() + { + byte[] settings = new byte[9]; + _registryMock.Setup(r => r.GetValue(StuckRects3, "Settings", null)).Returns(settings); + + Handle("AutoHideTaskbar", @"{""hideWhenNotUsing"":true}"); + + _registryMock.Verify(r => r.SetValue(StuckRects3, "Settings", + It.Is(b => (b[8] & 0x01) == 0x01), RegistryValueKind.Binary), Times.Once); + } + + [Fact] + public void AutoHideTaskbar_Disable_ClearsAutoHideBit() + { + byte[] settings = new byte[9]; + settings[8] = 0x01; // start with auto-hide on + _registryMock.Setup(r => r.GetValue(StuckRects3, "Settings", null)).Returns(settings); + + Handle("AutoHideTaskbar", @"{""hideWhenNotUsing"":false}"); + + _registryMock.Verify(r => r.SetValue(StuckRects3, "Settings", + It.Is(b => (b[8] & 0x01) == 0x00), RegistryValueKind.Binary), Times.Once); + } + + [Fact] + public void DisplaySecondsInSystrayClock_Enable_SetsShowSeconds1() + { + Handle("DisplaySecondsInSystrayClock", @"{""enable"":true}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "ShowSecondsInSystemClock", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void DisplaySecondsInSystrayClock_Disable_SetsShowSeconds0() + { + Handle("DisplaySecondsInSystrayClock", @"{""enable"":false}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "ShowSecondsInSystemClock", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void DisplayTaskbarOnAllMonitors_Enable_SetsMMTaskbar1() + { + Handle("DisplayTaskbarOnAllMonitors", @"{""enable"":true}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "MMTaskbarEnabled", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void DisplayTaskbarOnAllMonitors_Disable_SetsMMTaskbar0() + { + Handle("DisplayTaskbarOnAllMonitors", @"{""enable"":false}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "MMTaskbarEnabled", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ShowBadgesOnTaskbar_Enable_SetsBadges1() + { + Handle("ShowBadgesOnTaskbar", @"{""enableBadging"":true}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarBadges", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ShowBadgesOnTaskbar_Disable_SetsBadges0() + { + Handle("ShowBadgesOnTaskbar", @"{""enableBadging"":false}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarBadges", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void TaskbarAlignment_Center_SetsTaskbarAl1() + { + Handle("TaskbarAlignment", @"{""alignment"":""center""}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarAl", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void TaskbarAlignment_Left_SetsTaskbarAl0() + { + Handle("TaskbarAlignment", @"{""alignment"":""left""}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarAl", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void TaskViewVisibility_Show_SetsButton1() + { + Handle("TaskViewVisibility", @"{""visibility"":true}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "ShowTaskViewButton", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void TaskViewVisibility_Hide_SetsButton0() + { + Handle("TaskViewVisibility", @"{""visibility"":false}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "ShowTaskViewButton", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ToggleWidgetsButtonVisibility_Show_SetsTaskbarDa1() + { + Handle("ToggleWidgetsButtonVisibility", @"{""visibility"":""show""}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarDa", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ToggleWidgetsButtonVisibility_Hide_SetsTaskbarDa0() + { + Handle("ToggleWidgetsButtonVisibility", @"{""visibility"":""hide""}"); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarDa", 0, RegistryValueKind.DWord), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region DisplaySettingsHandler + +public class DisplaySettingsHandlerTests +{ + private readonly Mock _registryMock = new(); + private readonly Mock _processMock = new(); + private readonly DisplaySettingsHandler _handler; + + public DisplaySettingsHandlerTests() + { + _handler = new DisplaySettingsHandler(_registryMock.Object, _processMock.Object); + } + + [Fact] + public void AdjustColorTemperature_OpensNightLightSettings() + { + Handle("AdjustColorTemperature", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:nightlight"), Times.Once); + } + + [Fact] + public void AdjustScreenOrientation_OpensDisplaySettings() + { + Handle("AdjustScreenOrientation", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + [Fact] + public void DisplayResolutionAndAspectRatio_OpensDisplaySettings() + { + Handle("DisplayResolutionAndAspectRatio", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + [Fact] + public void DisplayScaling_WithPercentage_OpensDisplaySettings() + { + Handle("DisplayScaling", @"{""sizeOverride"":""150""}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + [Fact] + public void EnableBlueLightFilterSchedule_Enable_WritesEnableBytes() + { + Handle("EnableBlueLightFilterSchedule", @"{""nightLightScheduleDisabled"":false}"); + + _registryMock.Verify(r => r.SetValue( + It.Is(s => s.Contains("bluelightreduction")), + "Data", + It.Is(b => b.Length == 4 && b[3] == 0x01), + RegistryValueKind.Binary), Times.Once); + } + + [Fact] + public void EnableBlueLightFilterSchedule_Disable_WritesDisableBytes() + { + Handle("EnableBlueLightFilterSchedule", @"{""nightLightScheduleDisabled"":true}"); + + _registryMock.Verify(r => r.SetValue( + It.Is(s => s.Contains("bluelightreduction")), + "Data", + It.Is(b => b.Length == 4 && b[3] == 0x00), + RegistryValueKind.Binary), Times.Once); + } + + [Fact] + public void RotationLock_Enable_SetsPreference1() + { + Handle("RotationLock", @"{""enable"":true}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell", + "RotationLockPreference", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void RotationLock_Disable_SetsPreference0() + { + Handle("RotationLock", @"{""enable"":false}"); + + _registryMock.Verify(r => r.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell", + "RotationLockPreference", 0, RegistryValueKind.DWord), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region SystemSettingsHandler + +public class SystemSettingsHandlerTests +{ + private readonly Mock _processMock = new(); + private readonly SystemSettingsHandler _handler; + + public SystemSettingsHandlerTests() + { + _handler = new SystemSettingsHandler(_processMock.Object); + } + + [Fact] + public void AutomaticTimeSettingAction_OpensDateTimeSettings() + { + Handle("AutomaticTimeSettingAction", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:dateandtime"), Times.Once); + } + + [Fact] + public void EnableGameMode_OpensGamingSettings() + { + Handle("EnableGameMode", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:gaming-gamemode"), Times.Once); + } + + [Fact] + public void EnableQuietHours_OpensQuietHoursSettings() + { + Handle("EnableQuietHours", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:quiethours"), Times.Once); + } + + [Fact] + public void MinimizeWindowsOnMonitorDisconnectAction_OpensDisplaySettings() + { + Handle("MinimizeWindowsOnMonitorDisconnectAction", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + [Fact] + public void RememberWindowLocations_OpensDisplaySettings() + { + Handle("RememberWindowLocations", @"{}"); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion diff --git a/dotnet/autoShell.Tests/SystemCommandHandlerTests.cs b/dotnet/autoShell.Tests/SystemCommandHandlerTests.cs new file mode 100644 index 0000000000..ef45e70b2b --- /dev/null +++ b/dotnet/autoShell.Tests/SystemCommandHandlerTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Handlers; +using autoShell.Services; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +public class SystemCommandHandlerTests +{ + private readonly Mock _processMock = new(); + private readonly SystemCommandHandler _handler; + + public SystemCommandHandlerTests() + { + _handler = new SystemCommandHandler(_processMock.Object); + } + + [Fact] + public void ToggleNotifications_OpensActionCenter() + { + Handle("ToggleNotifications", ""); + + _processMock.Verify(p => p.StartShellExecute("ms-actioncenter:"), Times.Once); + } + + private void Handle(string key, string value) + { + _handler.Handle(key, value, JToken.FromObject(value)); + } +} diff --git a/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs b/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs new file mode 100644 index 0000000000..32449b10cc --- /dev/null +++ b/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using autoShell.Handlers; +using autoShell.Services; +using Microsoft.Win32; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +public class ThemeCommandHandlerTests +{ + private readonly Mock _registryMock = new(); + private readonly Mock _processMock = new(); + private readonly Mock _systemParamsMock = new(); + private readonly ThemeCommandHandler _handler; + + public ThemeCommandHandlerTests() + { + _handler = new ThemeCommandHandler( + _registryMock.Object, _processMock.Object, _systemParamsMock.Object); + } + + [Fact] + public void SetWallpaper_CallsSetParameter() + { + Handle("SetWallpaper", @"C:\wallpaper.jpg"); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x0014, 0, @"C:\wallpaper.jpg", 3), Times.Once); + } + + [Fact] + public void SetThemeMode_Dark_WritesRegistryValues() + { + Handle("SetThemeMode", "dark"); + + const string path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(path, "AppsUseLightTheme", 0, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(path, "SystemUsesLightTheme", 0, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void SetThemeMode_Light_WritesRegistryValues() + { + Handle("SetThemeMode", "light"); + + const string path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(path, "AppsUseLightTheme", 1, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(path, "SystemUsesLightTheme", 1, RegistryValueKind.DWord), Times.Once); + } + + [Fact] + public void ApplyTheme_UnknownTheme_DoesNotCallProcess() + { + Handle("ApplyTheme", "nonexistent_theme_xyz_12345"); + + _processMock.Verify(p => p.StartShellExecute(It.IsAny()), Times.Never); + } + + [Fact] + public void ListThemes_ReturnsWithoutError() + { + var ex = Record.Exception(() => Handle("ListThemes", "{}")); + + Assert.Null(ex); + } + + private void Handle(string key, string value) + { + _handler.Handle(key, value, JToken.FromObject(value)); + } +} From d61dcc0e79990dd2c98a9e92f5aae4cff9deb98e Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 1 Apr 2026 23:46:39 -0700 Subject: [PATCH 11/27] Formatting + cleaning up warnings --- .../autoShell.Tests/AppCommandHandlerTests.cs | 14 +- .../autoShell.Tests/CommandDispatcherTests.cs | 2 +- .../HandlerRegistrationTests.cs | 1 + .../autoShell.Tests/SettingsHandlerTests.cs | 149 +++++++++--------- .../ThemeCommandHandlerTests.cs | 12 +- dotnet/autoShell.Tests/autoShell.Tests.csproj | 2 + dotnet/autoShell/AutoShell.cs | 1 + dotnet/autoShell/CoreAudioInterop.cs | 130 ++++++++------- .../Handlers/DisplayCommandHandler.cs | 4 +- .../Handlers/NetworkCommandHandler.cs | 12 +- .../AccessibilitySettingsHandler.cs | 2 +- .../DisplaySettingsHandler.cs | 13 +- .../FileExplorerSettingsHandler.cs | 2 +- .../MouseSettingsHandler.cs | 2 +- .../PersonalizationSettingsHandler.cs | 8 +- .../PowerSettingsHandler.cs | 2 +- .../PrivacySettingsHandler.cs | 2 +- .../SystemSettingsHandler.cs | 7 +- .../TaskbarSettingsHandler.cs | 9 +- .../autoShell/Handlers/ThemeCommandHandler.cs | 20 +-- .../Handlers/VirtualDesktopCommandHandler.cs | 23 ++- .../Handlers/WindowCommandHandler.cs | 2 +- .../autoShell/Services/WindowsAppRegistry.cs | 16 +- dotnet/autoShell/UIAutomation.cs | 16 +- dotnet/autoShell/autoShell.csproj | 2 + 25 files changed, 228 insertions(+), 225 deletions(-) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/AccessibilitySettingsHandler.cs (98%) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/DisplaySettingsHandler.cs (94%) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/FileExplorerSettingsHandler.cs (98%) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/MouseSettingsHandler.cs (98%) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/PersonalizationSettingsHandler.cs (92%) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/PowerSettingsHandler.cs (97%) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/PrivacySettingsHandler.cs (97%) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/SystemSettingsHandler.cs (93%) rename dotnet/autoShell/Handlers/{SettingsHandlers => Settings}/TaskbarSettingsHandler.cs (95%) diff --git a/dotnet/autoShell.Tests/AppCommandHandlerTests.cs b/dotnet/autoShell.Tests/AppCommandHandlerTests.cs index 61318facfd..b09075a077 100644 --- a/dotnet/autoShell.Tests/AppCommandHandlerTests.cs +++ b/dotnet/autoShell.Tests/AppCommandHandlerTests.cs @@ -26,7 +26,7 @@ public AppCommandHandlerTests() public void LaunchProgram_AppNotRunning_StartsViaPath() { _appRegistryMock.Setup(a => a.ResolveProcessName("chrome")).Returns("chrome"); - _processMock.Setup(p => p.GetProcessesByName("chrome")).Returns(Array.Empty()); + _processMock.Setup(p => p.GetProcessesByName("chrome")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("chrome")).Returns("chrome.exe"); Handle("LaunchProgram", "chrome"); @@ -39,7 +39,7 @@ public void LaunchProgram_AppNotRunning_StartsViaPath() public void LaunchProgram_WithWorkingDir_SetsWorkingDirectory() { _appRegistryMock.Setup(a => a.ResolveProcessName("github copilot")).Returns("github copilot"); - _processMock.Setup(p => p.GetProcessesByName("github copilot")).Returns(Array.Empty()); + _processMock.Setup(p => p.GetProcessesByName("github copilot")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("github copilot")).Returns("copilot.exe"); _appRegistryMock.Setup(a => a.GetWorkingDirectoryEnvVar("github copilot")).Returns("GITHUB_COPILOT_ROOT_DIR"); @@ -53,7 +53,7 @@ public void LaunchProgram_WithWorkingDir_SetsWorkingDirectory() public void LaunchProgram_WithArguments_SetsArguments() { _appRegistryMock.Setup(a => a.ResolveProcessName("github copilot")).Returns("github copilot"); - _processMock.Setup(p => p.GetProcessesByName("github copilot")).Returns(Array.Empty()); + _processMock.Setup(p => p.GetProcessesByName("github copilot")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("github copilot")).Returns("copilot.exe"); _appRegistryMock.Setup(a => a.GetArguments("github copilot")).Returns("--allow-all-tools"); @@ -67,7 +67,7 @@ public void LaunchProgram_WithArguments_SetsArguments() public void LaunchProgram_NoPath_UsesAppUserModelId() { _appRegistryMock.Setup(a => a.ResolveProcessName("calculator")).Returns("calculator"); - _processMock.Setup(p => p.GetProcessesByName("calculator")).Returns(Array.Empty()); + _processMock.Setup(p => p.GetProcessesByName("calculator")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("calculator")).Returns((string)null!); _appRegistryMock.Setup(a => a.GetAppUserModelId("calculator")).Returns("Microsoft.WindowsCalculator"); @@ -83,7 +83,7 @@ public void CloseProgram_RunningProcess_CallsGetProcessesByName() _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); // Return a real (albeit useless in test) empty array to avoid null ref; // We cannot easily mock Process objects, so we verify the lookup was attempted. - _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns(Array.Empty()); + _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([]); Handle("CloseProgram", "notepad"); @@ -94,7 +94,7 @@ public void CloseProgram_RunningProcess_CallsGetProcessesByName() public void CloseProgram_NotRunning_DoesNothing() { _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); - _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns(Array.Empty()); + _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([]); var ex = Record.Exception(() => Handle("CloseProgram", "notepad")); @@ -104,7 +104,7 @@ public void CloseProgram_NotRunning_DoesNothing() [Fact] public void ListAppNames_CallsGetAllAppNames() { - _appRegistryMock.Setup(a => a.GetAllAppNames()).Returns(new List { "notepad", "chrome" }); + _appRegistryMock.Setup(a => a.GetAllAppNames()).Returns(["notepad", "chrome"]); Handle("ListAppNames", ""); diff --git a/dotnet/autoShell.Tests/CommandDispatcherTests.cs b/dotnet/autoShell.Tests/CommandDispatcherTests.cs index 48276bfe7d..2c161271c7 100644 --- a/dotnet/autoShell.Tests/CommandDispatcherTests.cs +++ b/dotnet/autoShell.Tests/CommandDispatcherTests.cs @@ -53,7 +53,7 @@ public void Dispatch_UnknownCommand_DoesNotThrow() [Fact] public void Dispatch_EmptyObject_ReturnsFalse() { - bool result = _dispatcher.Dispatch(new JObject()); + bool result = _dispatcher.Dispatch([]); Assert.False(result); } diff --git a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs index a4114d521e..3c8b9d583c 100644 --- a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs +++ b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using autoShell.Handlers; +using autoShell.Handlers.Settings; namespace autoShell.Tests; diff --git a/dotnet/autoShell.Tests/SettingsHandlerTests.cs b/dotnet/autoShell.Tests/SettingsHandlerTests.cs index 432cc4eb11..4864abd457 100644 --- a/dotnet/autoShell.Tests/SettingsHandlerTests.cs +++ b/dotnet/autoShell.Tests/SettingsHandlerTests.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using autoShell.Handlers; +using autoShell.Handlers.Settings; using autoShell.Services; using Microsoft.Win32; using Moq; @@ -27,7 +28,7 @@ public PersonalizationSettingsHandlerTests() [Fact] public void ApplyColorToTitleBar_Enable_SetsColorPrevalence1() { - Handle("ApplyColorToTitleBar", @"{""enableColor"":true}"); + Handle("ApplyColorToTitleBar", """{"enableColor":true}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\DWM", "ColorPrevalence", 1, RegistryValueKind.DWord), Times.Once); @@ -36,7 +37,7 @@ public void ApplyColorToTitleBar_Enable_SetsColorPrevalence1() [Fact] public void ApplyColorToTitleBar_Disable_SetsColorPrevalence0() { - Handle("ApplyColorToTitleBar", @"{""enableColor"":false}"); + Handle("ApplyColorToTitleBar", """{"enableColor":false}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\DWM", "ColorPrevalence", 0, RegistryValueKind.DWord), Times.Once); @@ -45,7 +46,7 @@ public void ApplyColorToTitleBar_Disable_SetsColorPrevalence0() [Fact] public void EnableTransparency_Enable_SetsTransparency1() { - Handle("EnableTransparency", @"{""enable"":true}"); + Handle("EnableTransparency", """{"enable":true}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", @@ -55,7 +56,7 @@ public void EnableTransparency_Enable_SetsTransparency1() [Fact] public void EnableTransparency_Disable_SetsTransparency0() { - Handle("EnableTransparency", @"{""enable"":false}"); + Handle("EnableTransparency", """{"enable":false}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", @@ -65,27 +66,27 @@ public void EnableTransparency_Disable_SetsTransparency0() [Fact] public void SystemThemeMode_Light_SetsBothKeys() { - Handle("SystemThemeMode", @"{""mode"":""light""}"); + Handle("SystemThemeMode", """{"mode":"light"}"""); - const string path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - _registryMock.Verify(r => r.SetValue(path, "AppsUseLightTheme", 1, RegistryValueKind.DWord), Times.Once); - _registryMock.Verify(r => r.SetValue(path, "SystemUsesLightTheme", 1, RegistryValueKind.DWord), Times.Once); + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(Path, "AppsUseLightTheme", 1, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(Path, "SystemUsesLightTheme", 1, RegistryValueKind.DWord), Times.Once); } [Fact] public void SystemThemeMode_Dark_SetsBothKeys() { - Handle("SystemThemeMode", @"{""mode"":""dark""}"); + Handle("SystemThemeMode", """{"mode":"dark"}"""); - const string path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - _registryMock.Verify(r => r.SetValue(path, "AppsUseLightTheme", 0, RegistryValueKind.DWord), Times.Once); - _registryMock.Verify(r => r.SetValue(path, "SystemUsesLightTheme", 0, RegistryValueKind.DWord), Times.Once); + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(Path, "AppsUseLightTheme", 0, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(Path, "SystemUsesLightTheme", 0, RegistryValueKind.DWord), Times.Once); } [Fact] public void HighContrastTheme_OpensSettings() { - Handle("HighContrastTheme", @"{}"); + Handle("HighContrastTheme", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:easeofaccess-highcontrast"), Times.Once); } @@ -113,7 +114,7 @@ public PrivacySettingsHandlerTests() [Fact] public void ManageCameraAccess_Deny_WritesDeny() { - Handle("ManageCameraAccess", @"{""accessSetting"":""deny""}"); + Handle("ManageCameraAccess", """{"accessSetting":"deny"}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam", @@ -123,7 +124,7 @@ public void ManageCameraAccess_Deny_WritesDeny() [Fact] public void ManageCameraAccess_Allow_WritesAllow() { - Handle("ManageCameraAccess", @"{""accessSetting"":""allow""}"); + Handle("ManageCameraAccess", """{"accessSetting":"allow"}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam", @@ -133,7 +134,7 @@ public void ManageCameraAccess_Allow_WritesAllow() [Fact] public void ManageMicrophoneAccess_Deny_WritesDeny() { - Handle("ManageMicrophoneAccess", @"{""accessSetting"":""deny""}"); + Handle("ManageMicrophoneAccess", """{"accessSetting":"deny"}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone", @@ -143,7 +144,7 @@ public void ManageMicrophoneAccess_Deny_WritesDeny() [Fact] public void ManageLocationAccess_Allow_WritesAllow() { - Handle("ManageLocationAccess", @"{""accessSetting"":""allow""}"); + Handle("ManageLocationAccess", """{"accessSetting":"allow"}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location", @@ -173,7 +174,7 @@ public FileExplorerSettingsHandlerTests() [Fact] public void ShowFileExtensions_Enable_SetsHideFileExt0() { - Handle("ShowFileExtensions", @"{""enable"":true}"); + Handle("ShowFileExtensions", """{"enable":true}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced", @@ -183,7 +184,7 @@ public void ShowFileExtensions_Enable_SetsHideFileExt0() [Fact] public void ShowFileExtensions_Disable_SetsHideFileExt1() { - Handle("ShowFileExtensions", @"{""enable"":false}"); + Handle("ShowFileExtensions", """{"enable":false}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced", @@ -193,21 +194,21 @@ public void ShowFileExtensions_Disable_SetsHideFileExt1() [Fact] public void ShowHiddenAndSystemFiles_Enable_SetsBothKeys() { - Handle("ShowHiddenAndSystemFiles", @"{""enable"":true}"); + Handle("ShowHiddenAndSystemFiles", """{"enable":true}"""); - const string path = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; - _registryMock.Verify(r => r.SetValue(path, "Hidden", 1, RegistryValueKind.DWord), Times.Once); - _registryMock.Verify(r => r.SetValue(path, "ShowSuperHidden", 1, RegistryValueKind.DWord), Times.Once); + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; + _registryMock.Verify(r => r.SetValue(Path, "Hidden", 1, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(Path, "ShowSuperHidden", 1, RegistryValueKind.DWord), Times.Once); } [Fact] public void ShowHiddenAndSystemFiles_Disable_SetsBothKeys() { - Handle("ShowHiddenAndSystemFiles", @"{""enable"":false}"); + Handle("ShowHiddenAndSystemFiles", """{"enable":false}"""); - const string path = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; - _registryMock.Verify(r => r.SetValue(path, "Hidden", 2, RegistryValueKind.DWord), Times.Once); - _registryMock.Verify(r => r.SetValue(path, "ShowSuperHidden", 0, RegistryValueKind.DWord), Times.Once); + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; + _registryMock.Verify(r => r.SetValue(Path, "Hidden", 2, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(Path, "ShowSuperHidden", 0, RegistryValueKind.DWord), Times.Once); } private void Handle(string key, string jsonValue) @@ -234,7 +235,7 @@ public PowerSettingsHandlerTests() [Fact] public void BatterySaverActivationLevel_SetsThreshold() { - Handle("BatterySaverActivationLevel", @"{""thresholdValue"":30}"); + Handle("BatterySaverActivationLevel", """{"thresholdValue":30}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\Power\BatterySaver", @@ -244,7 +245,7 @@ public void BatterySaverActivationLevel_SetsThreshold() [Fact] public void SetPowerModeOnBattery_OpensSettings() { - Handle("SetPowerModeOnBattery", @"{}"); + Handle("SetPowerModeOnBattery", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:powersleep"), Times.Once); } @@ -252,7 +253,7 @@ public void SetPowerModeOnBattery_OpensSettings() [Fact] public void SetPowerModePluggedIn_OpensSettings() { - Handle("SetPowerModePluggedIn", @"{}"); + Handle("SetPowerModePluggedIn", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:powersleep"), Times.Once); } @@ -281,7 +282,7 @@ public AccessibilitySettingsHandlerTests() [Fact] public void EnableStickyKeys_Enable_SetsFlags510() { - Handle("EnableStickyKeys", @"{""enable"":true}"); + Handle("EnableStickyKeys", """{"enable":true}"""); _registryMock.Verify(r => r.SetValue( @"Control Panel\Accessibility\StickyKeys", @@ -291,7 +292,7 @@ public void EnableStickyKeys_Enable_SetsFlags510() [Fact] public void EnableStickyKeys_Disable_SetsFlags506() { - Handle("EnableStickyKeys", @"{""enable"":false}"); + Handle("EnableStickyKeys", """{"enable":false}"""); _registryMock.Verify(r => r.SetValue( @"Control Panel\Accessibility\StickyKeys", @@ -301,7 +302,7 @@ public void EnableStickyKeys_Disable_SetsFlags506() [Fact] public void EnableFilterKeysAction_Enable_SetsFlags2() { - Handle("EnableFilterKeysAction", @"{""enable"":true}"); + Handle("EnableFilterKeysAction", """{"enable":true}"""); _registryMock.Verify(r => r.SetValue( @"Control Panel\Accessibility\Keyboard Response", @@ -311,7 +312,7 @@ public void EnableFilterKeysAction_Enable_SetsFlags2() [Fact] public void EnableFilterKeysAction_Disable_SetsFlags126() { - Handle("EnableFilterKeysAction", @"{""enable"":false}"); + Handle("EnableFilterKeysAction", """{"enable":false}"""); _registryMock.Verify(r => r.SetValue( @"Control Panel\Accessibility\Keyboard Response", @@ -321,7 +322,7 @@ public void EnableFilterKeysAction_Disable_SetsFlags126() [Fact] public void MonoAudioToggle_Enable_SetsMonoMix1() { - Handle("MonoAudioToggle", @"{""enable"":true}"); + Handle("MonoAudioToggle", """{"enable":true}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Multimedia\Audio", @@ -331,7 +332,7 @@ public void MonoAudioToggle_Enable_SetsMonoMix1() [Fact] public void EnableMagnifier_Enable_StartsProcess() { - Handle("EnableMagnifier", @"{""enable"":true}"); + Handle("EnableMagnifier", """{"enable":true}"""); _processMock.Verify(p => p.Start(It.Is( psi => psi.FileName == "magnify.exe")), Times.Once); @@ -340,7 +341,7 @@ public void EnableMagnifier_Enable_StartsProcess() [Fact] public void EnableNarratorAction_Enable_StartsNarrator() { - Handle("EnableNarratorAction", @"{""enable"":true}"); + Handle("EnableNarratorAction", """{"enable":true}"""); _processMock.Verify(p => p.Start(It.Is( psi => psi.FileName == "narrator.exe")), Times.Once); @@ -349,9 +350,9 @@ public void EnableNarratorAction_Enable_StartsNarrator() [Fact] public void EnableNarratorAction_Disable_CallsGetProcessesByName() { - _processMock.Setup(p => p.GetProcessesByName("Narrator")).Returns(Array.Empty()); + _processMock.Setup(p => p.GetProcessesByName("Narrator")).Returns([]); - Handle("EnableNarratorAction", @"{""enable"":false}"); + Handle("EnableNarratorAction", """{"enable":false}"""); _processMock.Verify(p => p.GetProcessesByName("Narrator"), Times.Once); } @@ -380,7 +381,7 @@ public MouseSettingsHandlerTests() [Fact] public void MouseCursorSpeed_SetsSpeed() { - Handle("MouseCursorSpeed", @"{""speedLevel"":10}"); + Handle("MouseCursorSpeed", """{"speedLevel":10}"""); _systemParamsMock.Verify(s => s.SetParameter( 0x0071, 0, (IntPtr)10, 3), Times.Once); @@ -389,7 +390,7 @@ public void MouseCursorSpeed_SetsSpeed() [Fact] public void MouseWheelScrollLines_SetsLines() { - Handle("MouseWheelScrollLines", @"{""scrollLines"":5}"); + Handle("MouseWheelScrollLines", """{"scrollLines":5}"""); _systemParamsMock.Verify(s => s.SetParameter( 0x0069, 5, IntPtr.Zero, 3), Times.Once); @@ -398,7 +399,7 @@ public void MouseWheelScrollLines_SetsLines() [Fact] public void EnhancePointerPrecision_Enable() { - Handle("EnhancePointerPrecision", @"{""enable"":true}"); + Handle("EnhancePointerPrecision", """{"enable":true}"""); _systemParamsMock.Verify(s => s.GetParameter(3, 0, It.IsAny(), 0), Times.Once); _systemParamsMock.Verify(s => s.SetParameter( @@ -408,7 +409,7 @@ public void EnhancePointerPrecision_Enable() [Fact] public void EnhancePointerPrecision_Disable() { - Handle("EnhancePointerPrecision", @"{""enable"":false}"); + Handle("EnhancePointerPrecision", """{"enable":false}"""); _systemParamsMock.Verify(s => s.GetParameter(3, 0, It.IsAny(), 0), Times.Once); _systemParamsMock.Verify(s => s.SetParameter( @@ -418,7 +419,7 @@ public void EnhancePointerPrecision_Disable() [Fact] public void AdjustMousePointerSize_OpensMouseSettings() { - Handle("AdjustMousePointerSize", @"{}"); + Handle("AdjustMousePointerSize", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:easeofaccess-mouse"), Times.Once); } @@ -426,7 +427,7 @@ public void AdjustMousePointerSize_OpensMouseSettings() [Fact] public void EnableTouchPad_OpensTouchpadSettings() { - Handle("EnableTouchPad", @"{}"); + Handle("EnableTouchPad", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:devices-touchpad"), Times.Once); } @@ -434,7 +435,7 @@ public void EnableTouchPad_OpensTouchpadSettings() [Fact] public void MousePointerCustomization_OpensMouseSettings() { - Handle("MousePointerCustomization", @"{}"); + Handle("MousePointerCustomization", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:easeofaccess-mouse"), Times.Once); } @@ -442,7 +443,7 @@ public void MousePointerCustomization_OpensMouseSettings() [Fact] public void TouchpadCursorSpeed_OpensTouchpadSettings() { - Handle("TouchpadCursorSpeed", @"{}"); + Handle("TouchpadCursorSpeed", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:devices-touchpad"), Times.Once); } @@ -476,7 +477,7 @@ public void AutoHideTaskbar_Enable_SetsAutoHideBit() byte[] settings = new byte[9]; _registryMock.Setup(r => r.GetValue(StuckRects3, "Settings", null)).Returns(settings); - Handle("AutoHideTaskbar", @"{""hideWhenNotUsing"":true}"); + Handle("AutoHideTaskbar", """{"hideWhenNotUsing":true}"""); _registryMock.Verify(r => r.SetValue(StuckRects3, "Settings", It.Is(b => (b[8] & 0x01) == 0x01), RegistryValueKind.Binary), Times.Once); @@ -489,7 +490,7 @@ public void AutoHideTaskbar_Disable_ClearsAutoHideBit() settings[8] = 0x01; // start with auto-hide on _registryMock.Setup(r => r.GetValue(StuckRects3, "Settings", null)).Returns(settings); - Handle("AutoHideTaskbar", @"{""hideWhenNotUsing"":false}"); + Handle("AutoHideTaskbar", """{"hideWhenNotUsing":false}"""); _registryMock.Verify(r => r.SetValue(StuckRects3, "Settings", It.Is(b => (b[8] & 0x01) == 0x00), RegistryValueKind.Binary), Times.Once); @@ -498,7 +499,7 @@ public void AutoHideTaskbar_Disable_ClearsAutoHideBit() [Fact] public void DisplaySecondsInSystrayClock_Enable_SetsShowSeconds1() { - Handle("DisplaySecondsInSystrayClock", @"{""enable"":true}"); + Handle("DisplaySecondsInSystrayClock", """{"enable":true}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "ShowSecondsInSystemClock", 1, RegistryValueKind.DWord), Times.Once); @@ -507,7 +508,7 @@ public void DisplaySecondsInSystrayClock_Enable_SetsShowSeconds1() [Fact] public void DisplaySecondsInSystrayClock_Disable_SetsShowSeconds0() { - Handle("DisplaySecondsInSystrayClock", @"{""enable"":false}"); + Handle("DisplaySecondsInSystrayClock", """{"enable":false}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "ShowSecondsInSystemClock", 0, RegistryValueKind.DWord), Times.Once); @@ -516,7 +517,7 @@ public void DisplaySecondsInSystrayClock_Disable_SetsShowSeconds0() [Fact] public void DisplayTaskbarOnAllMonitors_Enable_SetsMMTaskbar1() { - Handle("DisplayTaskbarOnAllMonitors", @"{""enable"":true}"); + Handle("DisplayTaskbarOnAllMonitors", """{"enable":true}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "MMTaskbarEnabled", 1, RegistryValueKind.DWord), Times.Once); @@ -525,7 +526,7 @@ public void DisplayTaskbarOnAllMonitors_Enable_SetsMMTaskbar1() [Fact] public void DisplayTaskbarOnAllMonitors_Disable_SetsMMTaskbar0() { - Handle("DisplayTaskbarOnAllMonitors", @"{""enable"":false}"); + Handle("DisplayTaskbarOnAllMonitors", """{"enable":false}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "MMTaskbarEnabled", 0, RegistryValueKind.DWord), Times.Once); @@ -534,7 +535,7 @@ public void DisplayTaskbarOnAllMonitors_Disable_SetsMMTaskbar0() [Fact] public void ShowBadgesOnTaskbar_Enable_SetsBadges1() { - Handle("ShowBadgesOnTaskbar", @"{""enableBadging"":true}"); + Handle("ShowBadgesOnTaskbar", """{"enableBadging":true}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "TaskbarBadges", 1, RegistryValueKind.DWord), Times.Once); @@ -543,7 +544,7 @@ public void ShowBadgesOnTaskbar_Enable_SetsBadges1() [Fact] public void ShowBadgesOnTaskbar_Disable_SetsBadges0() { - Handle("ShowBadgesOnTaskbar", @"{""enableBadging"":false}"); + Handle("ShowBadgesOnTaskbar", """{"enableBadging":false}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "TaskbarBadges", 0, RegistryValueKind.DWord), Times.Once); @@ -552,7 +553,7 @@ public void ShowBadgesOnTaskbar_Disable_SetsBadges0() [Fact] public void TaskbarAlignment_Center_SetsTaskbarAl1() { - Handle("TaskbarAlignment", @"{""alignment"":""center""}"); + Handle("TaskbarAlignment", """{"alignment":"center"}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "TaskbarAl", 1, RegistryValueKind.DWord), Times.Once); @@ -561,7 +562,7 @@ public void TaskbarAlignment_Center_SetsTaskbarAl1() [Fact] public void TaskbarAlignment_Left_SetsTaskbarAl0() { - Handle("TaskbarAlignment", @"{""alignment"":""left""}"); + Handle("TaskbarAlignment", """{"alignment":"left"}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "TaskbarAl", 0, RegistryValueKind.DWord), Times.Once); @@ -570,7 +571,7 @@ public void TaskbarAlignment_Left_SetsTaskbarAl0() [Fact] public void TaskViewVisibility_Show_SetsButton1() { - Handle("TaskViewVisibility", @"{""visibility"":true}"); + Handle("TaskViewVisibility", """{"visibility":true}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "ShowTaskViewButton", 1, RegistryValueKind.DWord), Times.Once); @@ -579,7 +580,7 @@ public void TaskViewVisibility_Show_SetsButton1() [Fact] public void TaskViewVisibility_Hide_SetsButton0() { - Handle("TaskViewVisibility", @"{""visibility"":false}"); + Handle("TaskViewVisibility", """{"visibility":false}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "ShowTaskViewButton", 0, RegistryValueKind.DWord), Times.Once); @@ -588,7 +589,7 @@ public void TaskViewVisibility_Hide_SetsButton0() [Fact] public void ToggleWidgetsButtonVisibility_Show_SetsTaskbarDa1() { - Handle("ToggleWidgetsButtonVisibility", @"{""visibility"":""show""}"); + Handle("ToggleWidgetsButtonVisibility", """{"visibility":"show"}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "TaskbarDa", 1, RegistryValueKind.DWord), Times.Once); @@ -597,7 +598,7 @@ public void ToggleWidgetsButtonVisibility_Show_SetsTaskbarDa1() [Fact] public void ToggleWidgetsButtonVisibility_Hide_SetsTaskbarDa0() { - Handle("ToggleWidgetsButtonVisibility", @"{""visibility"":""hide""}"); + Handle("ToggleWidgetsButtonVisibility", """{"visibility":"hide"}"""); _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, "TaskbarDa", 0, RegistryValueKind.DWord), Times.Once); @@ -627,7 +628,7 @@ public DisplaySettingsHandlerTests() [Fact] public void AdjustColorTemperature_OpensNightLightSettings() { - Handle("AdjustColorTemperature", @"{}"); + Handle("AdjustColorTemperature", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:nightlight"), Times.Once); } @@ -635,7 +636,7 @@ public void AdjustColorTemperature_OpensNightLightSettings() [Fact] public void AdjustScreenOrientation_OpensDisplaySettings() { - Handle("AdjustScreenOrientation", @"{}"); + Handle("AdjustScreenOrientation", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); } @@ -643,7 +644,7 @@ public void AdjustScreenOrientation_OpensDisplaySettings() [Fact] public void DisplayResolutionAndAspectRatio_OpensDisplaySettings() { - Handle("DisplayResolutionAndAspectRatio", @"{}"); + Handle("DisplayResolutionAndAspectRatio", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); } @@ -651,7 +652,7 @@ public void DisplayResolutionAndAspectRatio_OpensDisplaySettings() [Fact] public void DisplayScaling_WithPercentage_OpensDisplaySettings() { - Handle("DisplayScaling", @"{""sizeOverride"":""150""}"); + Handle("DisplayScaling", """{"sizeOverride":"150"}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); } @@ -659,7 +660,7 @@ public void DisplayScaling_WithPercentage_OpensDisplaySettings() [Fact] public void EnableBlueLightFilterSchedule_Enable_WritesEnableBytes() { - Handle("EnableBlueLightFilterSchedule", @"{""nightLightScheduleDisabled"":false}"); + Handle("EnableBlueLightFilterSchedule", """{"nightLightScheduleDisabled":false}"""); _registryMock.Verify(r => r.SetValue( It.Is(s => s.Contains("bluelightreduction")), @@ -671,7 +672,7 @@ public void EnableBlueLightFilterSchedule_Enable_WritesEnableBytes() [Fact] public void EnableBlueLightFilterSchedule_Disable_WritesDisableBytes() { - Handle("EnableBlueLightFilterSchedule", @"{""nightLightScheduleDisabled"":true}"); + Handle("EnableBlueLightFilterSchedule", """{"nightLightScheduleDisabled":true}"""); _registryMock.Verify(r => r.SetValue( It.Is(s => s.Contains("bluelightreduction")), @@ -683,7 +684,7 @@ public void EnableBlueLightFilterSchedule_Disable_WritesDisableBytes() [Fact] public void RotationLock_Enable_SetsPreference1() { - Handle("RotationLock", @"{""enable"":true}"); + Handle("RotationLock", """{"enable":true}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell", @@ -693,7 +694,7 @@ public void RotationLock_Enable_SetsPreference1() [Fact] public void RotationLock_Disable_SetsPreference0() { - Handle("RotationLock", @"{""enable"":false}"); + Handle("RotationLock", """{"enable":false}"""); _registryMock.Verify(r => r.SetValue( @"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell", @@ -723,7 +724,7 @@ public SystemSettingsHandlerTests() [Fact] public void AutomaticTimeSettingAction_OpensDateTimeSettings() { - Handle("AutomaticTimeSettingAction", @"{}"); + Handle("AutomaticTimeSettingAction", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:dateandtime"), Times.Once); } @@ -731,7 +732,7 @@ public void AutomaticTimeSettingAction_OpensDateTimeSettings() [Fact] public void EnableGameMode_OpensGamingSettings() { - Handle("EnableGameMode", @"{}"); + Handle("EnableGameMode", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:gaming-gamemode"), Times.Once); } @@ -739,7 +740,7 @@ public void EnableGameMode_OpensGamingSettings() [Fact] public void EnableQuietHours_OpensQuietHoursSettings() { - Handle("EnableQuietHours", @"{}"); + Handle("EnableQuietHours", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:quiethours"), Times.Once); } @@ -747,7 +748,7 @@ public void EnableQuietHours_OpensQuietHoursSettings() [Fact] public void MinimizeWindowsOnMonitorDisconnectAction_OpensDisplaySettings() { - Handle("MinimizeWindowsOnMonitorDisconnectAction", @"{}"); + Handle("MinimizeWindowsOnMonitorDisconnectAction", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); } @@ -755,7 +756,7 @@ public void MinimizeWindowsOnMonitorDisconnectAction_OpensDisplaySettings() [Fact] public void RememberWindowLocations_OpensDisplaySettings() { - Handle("RememberWindowLocations", @"{}"); + Handle("RememberWindowLocations", """{}"""); _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); } diff --git a/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs b/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs index 32449b10cc..afa511d8f2 100644 --- a/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs +++ b/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs @@ -37,9 +37,9 @@ public void SetThemeMode_Dark_WritesRegistryValues() { Handle("SetThemeMode", "dark"); - const string path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - _registryMock.Verify(r => r.SetValue(path, "AppsUseLightTheme", 0, RegistryValueKind.DWord), Times.Once); - _registryMock.Verify(r => r.SetValue(path, "SystemUsesLightTheme", 0, RegistryValueKind.DWord), Times.Once); + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(Path, "AppsUseLightTheme", 0, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(Path, "SystemUsesLightTheme", 0, RegistryValueKind.DWord), Times.Once); } [Fact] @@ -47,9 +47,9 @@ public void SetThemeMode_Light_WritesRegistryValues() { Handle("SetThemeMode", "light"); - const string path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - _registryMock.Verify(r => r.SetValue(path, "AppsUseLightTheme", 1, RegistryValueKind.DWord), Times.Once); - _registryMock.Verify(r => r.SetValue(path, "SystemUsesLightTheme", 1, RegistryValueKind.DWord), Times.Once); + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(Path, "AppsUseLightTheme", 1, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(Path, "SystemUsesLightTheme", 1, RegistryValueKind.DWord), Times.Once); } [Fact] diff --git a/dotnet/autoShell.Tests/autoShell.Tests.csproj b/dotnet/autoShell.Tests/autoShell.Tests.csproj index 6f2ae595dd..28b99ca8c7 100644 --- a/dotnet/autoShell.Tests/autoShell.Tests.csproj +++ b/dotnet/autoShell.Tests/autoShell.Tests.csproj @@ -6,6 +6,8 @@ enable false true + + $(NoWarn);JSON002 diff --git a/dotnet/autoShell/AutoShell.cs b/dotnet/autoShell/AutoShell.cs index af333d5e42..e6f8ab6b5b 100644 --- a/dotnet/autoShell/AutoShell.cs +++ b/dotnet/autoShell/AutoShell.cs @@ -6,6 +6,7 @@ using System.IO; using System.Runtime.InteropServices; using autoShell.Handlers; +using autoShell.Handlers.Settings; using autoShell.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/dotnet/autoShell/CoreAudioInterop.cs b/dotnet/autoShell/CoreAudioInterop.cs index 4c3b32b3fb..e14dac30c0 100644 --- a/dotnet/autoShell/CoreAudioInterop.cs +++ b/dotnet/autoShell/CoreAudioInterop.cs @@ -4,76 +4,74 @@ using System; using System.Runtime.InteropServices; -namespace autoShell -{ - // Windows Core Audio API COM interop definitions - // These replace the AudioSwitcher.AudioApi package for volume control +namespace autoShell; +// Windows Core Audio API COM interop definitions +// These replace the AudioSwitcher.AudioApi package for volume control - internal enum EDataFlow - { - eRender = 0, - eCapture = 1, - eAll = 2 - } +internal enum EDataFlow +{ + eRender = 0, + eCapture = 1, + eAll = 2 +} - internal enum ERole - { - eConsole = 0, - eMultimedia = 1, - eCommunications = 2 - } +internal enum ERole +{ + eConsole = 0, + eMultimedia = 1, + eCommunications = 2 +} - [ComImport] - [Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] - internal class MMDeviceEnumerator - { - } +[ComImport] +[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] +internal class MMDeviceEnumerator +{ +} - [ComImport] - [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IMMDeviceEnumerator - { - int EnumAudioEndpoints(EDataFlow dataFlow, int dwStateMask, out IntPtr ppDevices); - int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice); - int GetDevice(string pwstrId, out IMMDevice ppDevice); - int RegisterEndpointNotificationCallback(IntPtr pClient); - int UnregisterEndpointNotificationCallback(IntPtr pClient); - } +[ComImport] +[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IMMDeviceEnumerator +{ + int EnumAudioEndpoints(EDataFlow dataFlow, int dwStateMask, out IntPtr ppDevices); + int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice); + int GetDevice(string pwstrId, out IMMDevice ppDevice); + int RegisterEndpointNotificationCallback(IntPtr pClient); + int UnregisterEndpointNotificationCallback(IntPtr pClient); +} - [ComImport] - [Guid("D666063F-1587-4E43-81F1-B948E807363F")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IMMDevice - { - int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface); - int OpenPropertyStore(int stgmAccess, out IntPtr ppProperties); - int GetId([MarshalAs(UnmanagedType.LPWStr)] out string ppstrId); - int GetState(out int pdwState); - } +[ComImport] +[Guid("D666063F-1587-4E43-81F1-B948E807363F")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IMMDevice +{ + int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface); + int OpenPropertyStore(int stgmAccess, out IntPtr ppProperties); + int GetId([MarshalAs(UnmanagedType.LPWStr)] out string ppstrId); + int GetState(out int pdwState); +} - [ComImport] - [Guid("5CDF2C82-841E-4546-9722-0CF74078229A")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IAudioEndpointVolume - { - int RegisterControlChangeNotify(IntPtr pNotify); - int UnregisterControlChangeNotify(IntPtr pNotify); - int GetChannelCount(out int pnChannelCount); - int SetMasterVolumeLevel(float fLevelDB, Guid pguidEventContext); - int SetMasterVolumeLevelScalar(float fLevel, Guid pguidEventContext); - int GetMasterVolumeLevel(out float pfLevelDB); - int GetMasterVolumeLevelScalar(out float pfLevel); - int SetChannelVolumeLevel(int nChannel, float fLevelDB, Guid pguidEventContext); - int SetChannelVolumeLevelScalar(int nChannel, float fLevel, Guid pguidEventContext); - int GetChannelVolumeLevel(int nChannel, out float pfLevelDB); - int GetChannelVolumeLevelScalar(int nChannel, out float pfLevel); - int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, Guid pguidEventContext); - int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute); - int GetVolumeStepInfo(out int pnStep, out int pnStepCount); - int VolumeStepUp(Guid pguidEventContext); - int VolumeStepDown(Guid pguidEventContext); - int QueryHardwareSupport(out int pdwHardwareSupportMask); - int GetVolumeRange(out float pflVolumeMindB, out float pflVolumeMaxdB, out float pflVolumeIncrementdB); - } +[ComImport] +[Guid("5CDF2C82-841E-4546-9722-0CF74078229A")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IAudioEndpointVolume +{ + int RegisterControlChangeNotify(IntPtr pNotify); + int UnregisterControlChangeNotify(IntPtr pNotify); + int GetChannelCount(out int pnChannelCount); + int SetMasterVolumeLevel(float fLevelDB, Guid pguidEventContext); + int SetMasterVolumeLevelScalar(float fLevel, Guid pguidEventContext); + int GetMasterVolumeLevel(out float pfLevelDB); + int GetMasterVolumeLevelScalar(out float pfLevel); + int SetChannelVolumeLevel(int nChannel, float fLevelDB, Guid pguidEventContext); + int SetChannelVolumeLevelScalar(int nChannel, float fLevel, Guid pguidEventContext); + int GetChannelVolumeLevel(int nChannel, out float pfLevelDB); + int GetChannelVolumeLevelScalar(int nChannel, out float pfLevel); + int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, Guid pguidEventContext); + int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute); + int GetVolumeStepInfo(out int pnStep, out int pnStepCount); + int VolumeStepUp(Guid pguidEventContext); + int VolumeStepDown(Guid pguidEventContext); + int QueryHardwareSupport(out int pdwHardwareSupportMask); + int GetVolumeRange(out float pflVolumeMindB, out float pflVolumeMaxdB, out float pflVolumeIncrementdB); } diff --git a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs index 0d6ef038a7..160a316bdc 100644 --- a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs +++ b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs @@ -134,9 +134,9 @@ private void SetTextSize(int percentage) UseShellExecute = true }); - #pragma warning disable CS0618 // UIAutomation is intentionally marked obsolete as a last-resort approach +#pragma warning disable CS0618 // UIAutomation is intentionally marked obsolete as a last-resort approach UIAutomation.SetTextSizeViaUIAutomation(percentage); - #pragma warning restore CS0618 +#pragma warning restore CS0618 } catch (Exception ex) { diff --git a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs index e668a3665b..a21e81f71f 100644 --- a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs +++ b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs @@ -20,7 +20,7 @@ internal class NetworkCommandHandler : ICommandHandler { #region COM / P/Invoke - private static readonly Guid CLSID_RadioManagementAPI = new Guid(0x581333f6, 0x28db, 0x41be, 0xbc, 0x7a, 0xff, 0x20, 0x1f, 0x12, 0xf3, 0xf6); + private static readonly Guid s_clsidRadioManagementAPI = new Guid(0x581333f6, 0x28db, 0x41be, 0xbc, 0x7a, 0xff, 0x20, 0x1f, 0x12, 0xf3, 0xf6); [ComImport] [Guid("db3afbfb-08e6-46c6-aa70-bf9a34c30ab7")] @@ -211,7 +211,7 @@ private void SetAirplaneMode(bool enable) IRadioManager radioManager = null; try { - Type radioManagerType = Type.GetTypeFromCLSID(CLSID_RadioManagementAPI); + Type radioManagerType = Type.GetTypeFromCLSID(s_clsidRadioManagementAPI); if (radioManagerType == null) { Debug.WriteLine("Failed to get Radio Management API type"); @@ -303,7 +303,7 @@ private void ListWifiNetworks() { WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i]; - WlanScan(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + _ = WlanScan(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); System.Threading.Thread.Sleep(100); @@ -372,7 +372,7 @@ private void ListWifiNetworks() if (clientHandle != IntPtr.Zero) { - WlanCloseHandle(clientHandle, IntPtr.Zero); + _ = WlanCloseHandle(clientHandle, IntPtr.Zero); } } } @@ -456,7 +456,7 @@ private void ConnectToWifi(string ssid, string password = null) if (clientHandle != IntPtr.Zero) { - WlanCloseHandle(clientHandle, IntPtr.Zero); + _ = WlanCloseHandle(clientHandle, IntPtr.Zero); } } } @@ -557,7 +557,7 @@ private void DisconnectFromWifi() if (clientHandle != IntPtr.Zero) { - WlanCloseHandle(clientHandle, IntPtr.Zero); + _ = WlanCloseHandle(clientHandle, IntPtr.Zero); } } } diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/AccessibilitySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs similarity index 98% rename from dotnet/autoShell/Handlers/SettingsHandlers/AccessibilitySettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs index f16e5f7d5f..3b0ff7713c 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/AccessibilitySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs @@ -7,7 +7,7 @@ using Microsoft.Win32; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles accessibility settings: filter keys, magnifier, narrator, sticky keys, and mono audio. diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/DisplaySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs similarity index 94% rename from dotnet/autoShell/Handlers/SettingsHandlers/DisplaySettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs index 4ca91fde74..082b6acd9d 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/DisplaySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using autoShell.Services; using Microsoft.Win32; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles display settings: brightness, color temperature, orientation, resolution, scaling, @@ -123,7 +124,9 @@ private static byte GetCurrentBrightness() { object value = key.GetValue("Data"); if (value is byte[] data && data.Length > 0) + { return data[0]; + } } } catch { } @@ -137,9 +140,9 @@ private static void SetBrightness(byte brightness) using var searcher = new System.Management.ManagementObjectSearcher( "root\\WMI", "SELECT * FROM WmiMonitorBrightnessMethods"); using var objectCollection = searcher.Get(); - foreach (System.Management.ManagementObject obj in objectCollection) + foreach (System.Management.ManagementObject obj in objectCollection.Cast()) { - obj.InvokeMethod("WmiSetBrightness", new object[] { 1, brightness }); + obj.InvokeMethod("WmiSetBrightness", [1, brightness]); } } catch (Exception ex) @@ -152,8 +155,8 @@ private void HandleBlueLightFilter(JObject param) { bool disabled = param.Value("nightLightScheduleDisabled") ?? false; byte[] data = disabled - ? new byte[] { 0x02, 0x00, 0x00, 0x00 } - : new byte[] { 0x02, 0x00, 0x00, 0x01 }; + ? [0x02, 0x00, 0x00, 0x00] + : [0x02, 0x00, 0x00, 0x01]; _registry.SetValue( @"Software\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount\Current\default$windows.data.bluelightreduction.settings\windows.data.bluelightreduction.settings", diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/FileExplorerSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs similarity index 98% rename from dotnet/autoShell/Handlers/SettingsHandlers/FileExplorerSettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs index 26654264b6..1282af3271 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/FileExplorerSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs @@ -8,7 +8,7 @@ using Microsoft.Win32; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles File Explorer settings: file extensions and hidden/system files visibility. diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/MouseSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs similarity index 98% rename from dotnet/autoShell/Handlers/SettingsHandlers/MouseSettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs index 91d94bfa8c..35e80729cd 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/MouseSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs @@ -7,7 +7,7 @@ using autoShell.Services; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles mouse and touchpad settings: pointer size, precision, cursor speed, scroll lines, diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/PersonalizationSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs similarity index 92% rename from dotnet/autoShell/Handlers/SettingsHandlers/PersonalizationSettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs index f040a1c0f0..b3f42f6966 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/PersonalizationSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs @@ -7,7 +7,7 @@ using Microsoft.Win32; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles personalization settings: title bar color, transparency, high contrast, and theme mode. @@ -89,8 +89,8 @@ private void HandleSystemThemeMode(JObject param) string mode = param.Value("mode") ?? "dark"; int value = mode.Equals("light", StringComparison.OrdinalIgnoreCase) ? 1 : 0; - const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - _registry.SetValue(personalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); - _registry.SetValue(personalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); + const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); + _registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); } } diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/PowerSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs similarity index 97% rename from dotnet/autoShell/Handlers/SettingsHandlers/PowerSettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs index 79d843efd7..43a8fc6f2c 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/PowerSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs @@ -7,7 +7,7 @@ using Microsoft.Win32; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles power settings: battery saver threshold and power mode (on battery / plugged in). diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/PrivacySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs similarity index 97% rename from dotnet/autoShell/Handlers/SettingsHandlers/PrivacySettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs index 25d5c77ac0..f709f5c46a 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/PrivacySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs @@ -7,7 +7,7 @@ using Microsoft.Win32; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles privacy settings: camera, location, and microphone access. diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/SystemSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs similarity index 93% rename from dotnet/autoShell/Handlers/SettingsHandlers/SystemSettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs index 8da90db9c1..d93405aec6 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/SystemSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs @@ -8,7 +8,7 @@ using Microsoft.Win32; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles miscellaneous system settings: time/region, focus assist, gaming, and multi-monitor. @@ -74,10 +74,7 @@ private static void HandleAutomaticDSTAdjustment(string jsonParams) bool enable = param.Value("enable") ?? true; using var key = Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"); - if (key != null) - { - key.SetValue("DynamicDaylightTimeDisabled", enable ? 0 : 1, RegistryValueKind.DWord); - } + key?.SetValue("DynamicDaylightTimeDisabled", enable ? 0 : 1, RegistryValueKind.DWord); Debug.WriteLine($"Automatic DST adjustment {(enable ? "enabled" : "disabled")}"); } diff --git a/dotnet/autoShell/Handlers/SettingsHandlers/TaskbarSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs similarity index 95% rename from dotnet/autoShell/Handlers/SettingsHandlers/TaskbarSettingsHandler.cs rename to dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs index 1292b609b4..7227b85f1e 100644 --- a/dotnet/autoShell/Handlers/SettingsHandlers/TaskbarSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs @@ -9,7 +9,7 @@ using Microsoft.Win32; using Newtonsoft.Json.Linq; -namespace autoShell.Handlers; +namespace autoShell.Handlers.Settings; /// /// Handles taskbar settings: auto-hide, alignment, task view, widgets, badges, multi-monitor, clock. @@ -107,14 +107,17 @@ private void HandleAutoHideTaskbar(JObject param) bool hide = param.Value("hideWhenNotUsing"); // Auto-hide uses a binary blob in a different registry path - var settings = _registry.GetValue(StuckRects3, "Settings", null) as byte[]; - if (settings != null && settings.Length >= 9) + if (_registry.GetValue(StuckRects3, "Settings", null) is byte[] settings && settings.Length >= 9) { // Bit 0 of byte 8 controls auto-hide if (hide) + { settings[8] |= 0x01; + } else + { settings[8] &= 0xFE; + } _registry.SetValue(StuckRects3, "Settings", settings, RegistryValueKind.Binary); } diff --git a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs index 6560ae1240..0a1dd5eaec 100644 --- a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs +++ b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs @@ -167,7 +167,7 @@ private static string GetThemeDisplayName(string themeFilePath) { string displayName = line["DisplayName=".Length..].Trim(); // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013) - if (displayName.StartsWith("@")) + if (displayName.StartsWith('@')) { displayName = ResolveLocalizedString(displayName); } @@ -246,8 +246,8 @@ public string GetCurrentTheme() { try { - const string themesPath = @"Software\Microsoft\Windows\CurrentVersion\Themes"; - string currentThemePath = _registry.GetValue(themesPath, "CurrentTheme") as string; + const string ThemesPath = @"Software\Microsoft\Windows\CurrentVersion\Themes"; + string currentThemePath = _registry.GetValue(ThemesPath, "CurrentTheme") as string; if (!string.IsNullOrEmpty(currentThemePath)) { return Path.GetFileNameWithoutExtension(currentThemePath); @@ -275,7 +275,7 @@ public bool ApplyTheme(string themeName) { string previous = GetCurrentTheme(); - if (!themeName.Equals("previous", StringComparison.InvariantCultureIgnoreCase)) + if (!themeName.Equals("previous", StringComparison.OrdinalIgnoreCase)) { _process.StartShellExecute(themePath); _previousTheme = previous; @@ -369,11 +369,11 @@ public bool SetLightDarkMode(bool useLightMode) { try { - const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; int value = useLightMode ? 1 : 0; - _registry.SetValue(personalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); - _registry.SetValue(personalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); + _registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); + _registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); // Broadcast settings change notification to update UI BroadcastSettingsChange(); @@ -393,7 +393,7 @@ public bool SetLightDarkMode(bool useLightMode) public bool ToggleLightDarkMode() { bool? currentMode = GetCurrentLightMode(); - return currentMode.HasValue ? SetLightDarkMode(!currentMode.Value) : false; + return currentMode.HasValue && SetLightDarkMode(!currentMode.Value); } /// @@ -422,8 +422,8 @@ private static void BroadcastSettingsChange() { try { - const string personalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - object value = _registry.GetValue(personalizePath, "AppsUseLightTheme"); + const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + object value = _registry.GetValue(PersonalizePath, "AppsUseLightTheme"); return value is int intValue ? intValue == 1 : null; } catch diff --git a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs index 55724699d5..0c8d8201a3 100644 --- a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs +++ b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs @@ -31,12 +31,12 @@ public VirtualDesktopCommandHandler(IAppRegistry appRegistry) _appRegistry = appRegistry; // Desktop management COM initialization - _shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ImmersiveShell)); - _virtualDesktopManagerInternal = (IVirtualDesktopManagerInternal)_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); - _virtualDesktopManagerInternal_BUGBUG = (IVirtualDesktopManagerInternal_BUGBUG)_shell.QueryService(CLSID_VirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); - _virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_VirtualDesktopManager)); + _shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(s_clsidImmersiveShell)); + _virtualDesktopManagerInternal = (IVirtualDesktopManagerInternal)_shell.QueryService(s_clsidVirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); + _virtualDesktopManagerInternal_BUGBUG = (IVirtualDesktopManagerInternal_BUGBUG)_shell.QueryService(s_clsidVirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); + _virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(s_clsidVirtualDesktopManager)); _applicationViewCollection = (IApplicationViewCollection)_shell.QueryService(typeof(IApplicationViewCollection).GUID, typeof(IApplicationViewCollection).GUID); - _virtualDesktopPinnedApps = (IVirtualDesktopPinnedApps)_shell.QueryService(CLSID_VirtualDesktopPinnedApps, typeof(IVirtualDesktopPinnedApps).GUID); + _virtualDesktopPinnedApps = (IVirtualDesktopPinnedApps)_shell.QueryService(s_clsidVirtualDesktopPinnedApps, typeof(IVirtualDesktopPinnedApps).GUID); } /// @@ -237,7 +237,6 @@ private IVirtualDesktop FindDesktopByName(string name) private int GetDesktopIndex(IVirtualDesktop desktop) { - int index = -1; int count = _virtualDesktopManagerInternal.GetCount(); _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); @@ -328,9 +327,9 @@ private IVirtualDesktopManagerInternal GetVirtualDesktopManagerInternal() try { IServiceProvider shellServiceProvider = (IServiceProvider)Activator.CreateInstance( - Type.GetTypeFromCLSID(CLSID_ImmersiveShell)); + Type.GetTypeFromCLSID(s_clsidImmersiveShell)); - Guid guidService = CLSID_VirtualDesktopManagerInternal; + Guid guidService = s_clsidVirtualDesktopManagerInternal; Guid riid = typeof(IVirtualDesktopManagerInternal).GUID; shellServiceProvider.QueryService( ref guidService, @@ -366,10 +365,10 @@ private enum APPLICATION_VIEW_COMPATIBILITY_POLICY : int } // Virtual Desktop COM Interface GUIDs - private static readonly Guid CLSID_ImmersiveShell = new Guid("C2F03A33-21F5-47FA-B4BB-156362A2F239"); - private static readonly Guid CLSID_VirtualDesktopManagerInternal = new Guid("C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B"); - private static readonly Guid CLSID_VirtualDesktopManager = new Guid("AA509086-5CA9-4C25-8F95-589D3C07B48A"); - private static readonly Guid CLSID_VirtualDesktopPinnedApps = new Guid("B5A399E7-1C87-46B8-88E9-FC5747B171BD"); + private static readonly Guid s_clsidImmersiveShell = new Guid("C2F03A33-21F5-47FA-B4BB-156362A2F239"); + private static readonly Guid s_clsidVirtualDesktopManagerInternal = new Guid("C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B"); + private static readonly Guid s_clsidVirtualDesktopManager = new Guid("AA509086-5CA9-4C25-8F95-589D3C07B48A"); + private static readonly Guid s_clsidVirtualDesktopPinnedApps = new Guid("B5A399E7-1C87-46B8-88E9-FC5747B171BD"); // IServiceProvider COM Interface [ComImport] diff --git a/dotnet/autoShell/Handlers/WindowCommandHandler.cs b/dotnet/autoShell/Handlers/WindowCommandHandler.cs index b4aca14e7b..1a70b145ab 100644 --- a/dotnet/autoShell/Handlers/WindowCommandHandler.cs +++ b/dotnet/autoShell/Handlers/WindowCommandHandler.cs @@ -257,7 +257,7 @@ internal static (IntPtr hWnd, int pid) FindWindowByTitle(string titleSearch) if (title.Contains(titleSearch, StringComparison.OrdinalIgnoreCase)) { foundHandle = hWnd; - GetWindowThreadProcessId(hWnd, out uint pid); + _ = GetWindowThreadProcessId(hWnd, out uint pid); foundPid = (int)pid; return false; } diff --git a/dotnet/autoShell/Services/WindowsAppRegistry.cs b/dotnet/autoShell/Services/WindowsAppRegistry.cs index 1de8c4d52e..d96b45c4b5 100644 --- a/dotnet/autoShell/Services/WindowsAppRegistry.cs +++ b/dotnet/autoShell/Services/WindowsAppRegistry.cs @@ -93,20 +93,16 @@ public string ResolveProcessName(string friendlyName) public string GetWorkingDirectoryEnvVar(string friendlyName) { - if (_appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 1) - { - return value[1]; - } - return null; + return _appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 1 + ? value[1] + : null; } public string GetArguments(string friendlyName) { - if (_appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 2) - { - return string.Join(" ", value.Skip(2)); - } - return null; + return _appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 2 + ? string.Join(" ", value.Skip(2)) + : null; } public IEnumerable GetAllAppNames() diff --git a/dotnet/autoShell/UIAutomation.cs b/dotnet/autoShell/UIAutomation.cs index e579c63052..fa9fbfab8a 100644 --- a/dotnet/autoShell/UIAutomation.cs +++ b/dotnet/autoShell/UIAutomation.cs @@ -31,8 +31,8 @@ internal static void SetTextSizeViaUIAutomation(int percentage) // UI Automation Pattern IDs const int UIA_RangeValuePatternId = 10003; - const int maxRetries = 10; - const int retryDelayMs = 500; + const int MaxRetries = 10; + const int RetryDelayMs = 500; try { @@ -41,7 +41,7 @@ internal static void SetTextSizeViaUIAutomation(int percentage) UIAutomationClient.IUIAutomationElement settingsWindow = null; // Wait for Settings window to appear and get it via FindWindow - for (int i = 0; i < maxRetries; i++) + for (int i = 0; i < MaxRetries; i++) { // Find the Settings window by enumerating top-level windows with "Settings" in the title // UWP apps use ApplicationFrameWindow class @@ -65,7 +65,7 @@ internal static void SetTextSizeViaUIAutomation(int percentage) break; } - System.Threading.Thread.Sleep(retryDelayMs); + System.Threading.Thread.Sleep(RetryDelayMs); } if (settingsWindow == null) @@ -98,7 +98,7 @@ internal static void SetTextSizeViaUIAutomation(int percentage) "SystemSettings_EaseOfAccess_Experience_TextScalingDesktop_Slider"); UIAutomationClient.IUIAutomationElement slider = null; - for (int i = 0; i < maxRetries; i++) + for (int i = 0; i < MaxRetries; i++) { slider = settingsWindow.FindFirst( UIAutomationClient.TreeScope.TreeScope_Descendants, @@ -109,7 +109,7 @@ internal static void SetTextSizeViaUIAutomation(int percentage) break; } - System.Threading.Thread.Sleep(retryDelayMs); + System.Threading.Thread.Sleep(RetryDelayMs); } if (slider == null) @@ -166,7 +166,7 @@ internal static void SetTextSizeViaUIAutomation(int percentage) "SystemSettings_EaseOfAccess_Experience_TextScalingDesktop_ButtonRemove"); UIAutomationClient.IUIAutomationElement applyButton = null; - for (int i = 0; i < maxRetries; i++) + for (int i = 0; i < MaxRetries; i++) { applyButton = settingsWindow.FindFirst( UIAutomationClient.TreeScope.TreeScope_Descendants, @@ -177,7 +177,7 @@ internal static void SetTextSizeViaUIAutomation(int percentage) break; } - System.Threading.Thread.Sleep(retryDelayMs); + System.Threading.Thread.Sleep(RetryDelayMs); } if (applyButton != null) diff --git a/dotnet/autoShell/autoShell.csproj b/dotnet/autoShell/autoShell.csproj index 514231c6ef..b14bb30e85 100644 --- a/dotnet/autoShell/autoShell.csproj +++ b/dotnet/autoShell/autoShell.csproj @@ -8,6 +8,8 @@ true bin\$(Configuration) false + + $(NoWarn);SYSLIB1054;SYSLIB1096;CA1712;CA2101;CA1838 From d6e71487051dd9cfbc88a47e5aa35a4203d902d1 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 2 Apr 2026 12:36:04 -0700 Subject: [PATCH 12/27] Add this. qualifier to all instance member accesses in autoShell Add explicit this. prefix to all instance field accesses, property accesses, and method calls across 19 files in the autoShell project to comply with .editorconfig rules: - dotnet_style_qualification_for_field = true:error - dotnet_style_qualification_for_property = true:error - dotnet_style_qualification_for_method = true:error - dotnet_style_qualification_for_event = true:error Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../autoShell/Handlers/AppCommandHandler.cs | 36 +++++----- .../autoShell/Handlers/AudioCommandHandler.cs | 10 +-- .../autoShell/Handlers/CommandDispatcher.cs | 4 +- .../Handlers/DisplayCommandHandler.cs | 4 +- .../Handlers/NetworkCommandHandler.cs | 10 +-- .../Settings/AccessibilitySettingsHandler.cs | 24 +++---- .../Settings/DisplaySettingsHandler.cs | 22 +++--- .../Settings/FileExplorerSettingsHandler.cs | 12 ++-- .../Handlers/Settings/MouseSettingsHandler.cs | 22 +++--- .../PersonalizationSettingsHandler.cs | 20 +++--- .../Handlers/Settings/PowerSettingsHandler.cs | 10 +-- .../Settings/PrivacySettingsHandler.cs | 6 +- .../Settings/SystemSettingsHandler.cs | 10 +-- .../Settings/TaskbarSettingsHandler.cs | 24 +++---- .../Handlers/SystemCommandHandler.cs | 4 +- .../autoShell/Handlers/ThemeCommandHandler.cs | 72 +++++++++---------- .../Handlers/VirtualDesktopCommandHandler.cs | 72 +++++++++---------- .../Handlers/WindowCommandHandler.cs | 18 ++--- .../autoShell/Services/WindowsAppRegistry.cs | 18 ++--- 19 files changed, 199 insertions(+), 199 deletions(-) diff --git a/dotnet/autoShell/Handlers/AppCommandHandler.cs b/dotnet/autoShell/Handlers/AppCommandHandler.cs index a4fd2d9796..3a4db48d2d 100644 --- a/dotnet/autoShell/Handlers/AppCommandHandler.cs +++ b/dotnet/autoShell/Handlers/AppCommandHandler.cs @@ -20,8 +20,8 @@ internal class AppCommandHandler : ICommandHandler public AppCommandHandler(IAppRegistry appRegistry, IProcessService processService) { - _appRegistry = appRegistry; - _processService = processService; + this._appRegistry = appRegistry; + this._processService = processService; } /// @@ -38,28 +38,28 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "LaunchProgram": - OpenApplication(value); + this.OpenApplication(value); break; case "CloseProgram": - CloseApplication(value); + this.CloseApplication(value); break; case "ListAppNames": - Console.WriteLine(JsonConvert.SerializeObject(_appRegistry.GetAllAppNames())); + Console.WriteLine(JsonConvert.SerializeObject(this._appRegistry.GetAllAppNames())); break; } } private void OpenApplication(string friendlyName) { - string processName = _appRegistry.ResolveProcessName(friendlyName); - Process[] processes = _processService.GetProcessesByName(processName); + string processName = this._appRegistry.ResolveProcessName(friendlyName); + Process[] processes = this._processService.GetProcessesByName(processName); if (processes.Length == 0) { Debug.WriteLine("Starting " + friendlyName); - string path = _appRegistry.GetExecutablePath(friendlyName); + string path = this._appRegistry.GetExecutablePath(friendlyName); if (path != null) { var psi = new ProcessStartInfo @@ -68,13 +68,13 @@ private void OpenApplication(string friendlyName) UseShellExecute = true }; - string workDirEnvVar = _appRegistry.GetWorkingDirectoryEnvVar(friendlyName); + string workDirEnvVar = this._appRegistry.GetWorkingDirectoryEnvVar(friendlyName); if (workDirEnvVar != null) { psi.WorkingDirectory = Environment.ExpandEnvironmentVariables("%" + workDirEnvVar + "%") ?? string.Empty; } - string arguments = _appRegistry.GetArguments(friendlyName); + string arguments = this._appRegistry.GetArguments(friendlyName); if (arguments != null) { psi.Arguments = arguments; @@ -82,22 +82,22 @@ private void OpenApplication(string friendlyName) try { - _processService.Start(psi); + this._processService.Start(psi); } catch (System.ComponentModel.Win32Exception) { psi.FileName = friendlyName; - _processService.Start(psi); + this._processService.Start(psi); } } else { - string appModelUserID = _appRegistry.GetAppUserModelId(friendlyName); - if (appModelUserID != null) + string appModelUserId = this._appRegistry.GetAppUserModelId(friendlyName); + if (appModelUserId != null) { try { - _processService.Start(new ProcessStartInfo("explorer.exe", @" shell:appsFolder\" + appModelUserID)); + this._processService.Start(new ProcessStartInfo("explorer.exe", @" shell:appsFolder\" + appModelUserId)); } catch { } } @@ -106,14 +106,14 @@ private void OpenApplication(string friendlyName) else { Debug.WriteLine("Raising " + friendlyName); - WindowCommandHandler.RaiseWindow(friendlyName, _appRegistry); + WindowCommandHandler.RaiseWindow(friendlyName, this._appRegistry); } } private void CloseApplication(string friendlyName) { - string processName = _appRegistry.ResolveProcessName(friendlyName); - Process[] processes = _processService.GetProcessesByName(processName); + string processName = this._appRegistry.ResolveProcessName(friendlyName); + Process[] processes = this._processService.GetProcessesByName(processName); if (processes.Length != 0) { Debug.WriteLine("Closing " + friendlyName); diff --git a/dotnet/autoShell/Handlers/AudioCommandHandler.cs b/dotnet/autoShell/Handlers/AudioCommandHandler.cs index 92b9f85f40..9d2c1e34d4 100644 --- a/dotnet/autoShell/Handlers/AudioCommandHandler.cs +++ b/dotnet/autoShell/Handlers/AudioCommandHandler.cs @@ -25,7 +25,7 @@ internal class AudioCommandHandler : ICommandHandler public AudioCommandHandler(IAudioService audio) { - _audio = audio; + this._audio = audio; } /// @@ -36,17 +36,17 @@ public void Handle(string key, string value, JToken rawValue) case "Volume": if (int.TryParse(value, out int pct)) { - _savedVolumePct = _audio.GetVolume(); - _audio.SetVolume(pct); + this._savedVolumePct = this._audio.GetVolume(); + this._audio.SetVolume(pct); } break; case "RestoreVolume": - _audio.SetVolume((int)_savedVolumePct); + this._audio.SetVolume((int)this._savedVolumePct); break; case "Mute": if (bool.TryParse(value, out bool mute)) { - _audio.SetMute(mute); + this._audio.SetMute(mute); } break; } diff --git a/dotnet/autoShell/Handlers/CommandDispatcher.cs b/dotnet/autoShell/Handlers/CommandDispatcher.cs index f52428188d..384fc998f6 100644 --- a/dotnet/autoShell/Handlers/CommandDispatcher.cs +++ b/dotnet/autoShell/Handlers/CommandDispatcher.cs @@ -21,7 +21,7 @@ public void Register(params ICommandHandler[] handlers) { foreach (string command in handler.SupportedCommands) { - _handlers[command] = handler; + this._handlers[command] = handler; } } } @@ -45,7 +45,7 @@ public bool Dispatch(JObject root) try { - if (_handlers.TryGetValue(key, out ICommandHandler handler)) + if (this._handlers.TryGetValue(key, out ICommandHandler handler)) { handler.Handle(key, value, kvp.Value); } diff --git a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs index 160a316bdc..f2ece4ccf9 100644 --- a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs +++ b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs @@ -93,12 +93,12 @@ public void Handle(string key, string value, JToken rawValue) case "SetTextSize": if (int.TryParse(value, out int textSizePct)) { - SetTextSize(textSizePct); + this.SetTextSize(textSizePct); } break; case "SetScreenResolution": - SetDisplayResolution(rawValue); + this.SetDisplayResolution(rawValue); break; case "ListResolutions": diff --git a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs index a21e81f71f..a78bf5499d 100644 --- a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs +++ b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs @@ -20,7 +20,7 @@ internal class NetworkCommandHandler : ICommandHandler { #region COM / P/Invoke - private static readonly Guid s_clsidRadioManagementAPI = new Guid(0x581333f6, 0x28db, 0x41be, 0xbc, 0x7a, 0xff, 0x20, 0x1f, 0x12, 0xf3, 0xf6); + private static readonly Guid s_clsidRadioManagementApi = new Guid(0x581333f6, 0x28db, 0x41be, 0xbc, 0x7a, 0xff, 0x20, 0x1f, 0x12, 0xf3, 0xf6); [ComImport] [Guid("db3afbfb-08e6-46c6-aa70-bf9a34c30ab7")] @@ -176,7 +176,7 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "ToggleAirplaneMode": - SetAirplaneMode(bool.Parse(value)); + this.SetAirplaneMode(bool.Parse(value)); break; case "ListWifiNetworks": @@ -187,11 +187,11 @@ public void Handle(string key, string value, JToken rawValue) var netInfo = JObject.Parse(value); string ssid = netInfo.Value("ssid"); string password = netInfo["password"] is not null ? netInfo.Value("password") : ""; - ConnectToWifi(ssid, password); + this.ConnectToWifi(ssid, password); break; case "DisconnectWifi": - DisconnectFromWifi(); + this.DisconnectFromWifi(); break; case "BluetoothToggle": @@ -211,7 +211,7 @@ private void SetAirplaneMode(bool enable) IRadioManager radioManager = null; try { - Type radioManagerType = Type.GetTypeFromCLSID(s_clsidRadioManagementAPI); + Type radioManagerType = Type.GetTypeFromCLSID(s_clsidRadioManagementApi); if (radioManagerType == null) { Debug.WriteLine("Failed to get Radio Management API type"); diff --git a/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs index 3b0ff7713c..902c664d4f 100644 --- a/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs @@ -29,8 +29,8 @@ internal class AccessibilitySettingsHandler : ICommandHandler public AccessibilitySettingsHandler(IRegistryService registry, IProcessService process) { - _registry = registry; - _process = process; + this._registry = registry; + this._process = process; } /// @@ -43,23 +43,23 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "EnableFilterKeysAction": - HandleFilterKeys(param); + this.HandleFilterKeys(param); break; case "EnableMagnifier": - HandleToggleProcess(param, "magnify.exe", "Magnify"); + this.HandleToggleProcess(param, "magnify.exe", "Magnify"); break; case "EnableNarratorAction": - HandleToggleProcess(param, "narrator.exe", "Narrator"); + this.HandleToggleProcess(param, "narrator.exe", "Narrator"); break; case "EnableStickyKeys": - HandleStickyKeys(param); + this.HandleStickyKeys(param); break; case "MonoAudioToggle": - HandleMonoAudio(param); + this.HandleMonoAudio(param); break; } } @@ -72,7 +72,7 @@ public void Handle(string key, string value, JToken rawValue) private void HandleFilterKeys(JObject param) { bool enable = param.Value("enable") ?? true; - _registry.SetValue( + this._registry.SetValue( @"Control Panel\Accessibility\Keyboard Response", "Flags", enable ? "2" : "126", @@ -82,7 +82,7 @@ private void HandleFilterKeys(JObject param) private void HandleStickyKeys(JObject param) { bool enable = param.Value("enable") ?? true; - _registry.SetValue( + this._registry.SetValue( @"Control Panel\Accessibility\StickyKeys", "Flags", enable ? "510" : "506", @@ -92,7 +92,7 @@ private void HandleStickyKeys(JObject param) private void HandleMonoAudio(JObject param) { bool enable = param.Value("enable") ?? true; - _registry.SetValue( + this._registry.SetValue( @"Software\Microsoft\Multimedia\Audio", "AccessibilityMonoMixState", enable ? 1 : 0, @@ -105,11 +105,11 @@ private void HandleToggleProcess(JObject param, string exeName, string processNa if (enable) { - _process.Start(new System.Diagnostics.ProcessStartInfo { FileName = exeName }); + this._process.Start(new System.Diagnostics.ProcessStartInfo { FileName = exeName }); } else { - foreach (var p in _process.GetProcessesByName(processName)) + foreach (var p in this._process.GetProcessesByName(processName)) { p.Kill(); } diff --git a/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs index 082b6acd9d..92089f103a 100644 --- a/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs @@ -34,8 +34,8 @@ internal class DisplaySettingsHandler : ICommandHandler public DisplaySettingsHandler(IRegistryService registry, IProcessService process) { - _registry = registry; - _process = process; + this._registry = registry; + this._process = process; } /// @@ -48,28 +48,28 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "AdjustScreenBrightness": - HandleAdjustScreenBrightness(param); + this.HandleAdjustScreenBrightness(param); break; case "DisplayScaling": - HandleDisplayScaling(param); + this.HandleDisplayScaling(param); break; case "AdjustColorTemperature": - _process.StartShellExecute("ms-settings:nightlight"); + this._process.StartShellExecute("ms-settings:nightlight"); break; case "AdjustScreenOrientation": case "DisplayResolutionAndAspectRatio": - _process.StartShellExecute("ms-settings:display"); + this._process.StartShellExecute("ms-settings:display"); break; case "EnableBlueLightFilterSchedule": - HandleBlueLightFilter(param); + this.HandleBlueLightFilter(param); break; case "RotationLock": - HandleRotationLock(param); + this.HandleRotationLock(param); break; } } @@ -109,7 +109,7 @@ private void HandleDisplayScaling(JObject param) }; // DPI scaling requires opening settings - _process.StartShellExecute("ms-settings:display"); + this._process.StartShellExecute("ms-settings:display"); Debug.WriteLine($"Display scaling target: {percentage}%"); } } @@ -158,7 +158,7 @@ private void HandleBlueLightFilter(JObject param) ? [0x02, 0x00, 0x00, 0x00] : [0x02, 0x00, 0x00, 0x01]; - _registry.SetValue( + this._registry.SetValue( @"Software\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount\Current\default$windows.data.bluelightreduction.settings\windows.data.bluelightreduction.settings", "Data", data, @@ -168,7 +168,7 @@ private void HandleBlueLightFilter(JObject param) private void HandleRotationLock(JObject param) { bool enable = param.Value("enable") ?? true; - _registry.SetValue( + this._registry.SetValue( @"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell", "RotationLockPreference", enable ? 1 : 0, diff --git a/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs index 1282af3271..2ed8442edb 100644 --- a/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs @@ -31,7 +31,7 @@ internal partial class FileExplorerSettingsHandler : ICommandHandler public FileExplorerSettingsHandler(IRegistryService registry) { - _registry = registry; + this._registry = registry; } /// @@ -44,11 +44,11 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "ShowFileExtensions": - HandleShowFileExtensions(param); + this.HandleShowFileExtensions(param); break; case "ShowHiddenAndSystemFiles": - HandleShowHiddenAndSystemFiles(param); + this.HandleShowHiddenAndSystemFiles(param); break; } @@ -64,13 +64,13 @@ private void HandleShowFileExtensions(JObject param) { bool enable = param.Value("enable") ?? true; // Inverted: enable showing extensions = HideFileExt 0 - _registry.SetValue(ExplorerAdvanced, "HideFileExt", enable ? 0 : 1, RegistryValueKind.DWord); + this._registry.SetValue(ExplorerAdvanced, "HideFileExt", enable ? 0 : 1, RegistryValueKind.DWord); } private void HandleShowHiddenAndSystemFiles(JObject param) { bool enable = param.Value("enable") ?? true; - _registry.SetValue(ExplorerAdvanced, "Hidden", enable ? 1 : 2, RegistryValueKind.DWord); - _registry.SetValue(ExplorerAdvanced, "ShowSuperHidden", enable ? 1 : 0, RegistryValueKind.DWord); + this._registry.SetValue(ExplorerAdvanced, "Hidden", enable ? 1 : 2, RegistryValueKind.DWord); + this._registry.SetValue(ExplorerAdvanced, "ShowSuperHidden", enable ? 1 : 0, RegistryValueKind.DWord); } } diff --git a/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs index 35e80729cd..a8edf11618 100644 --- a/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs @@ -43,8 +43,8 @@ internal partial class MouseSettingsHandler : ICommandHandler public MouseSettingsHandler(ISystemParametersService systemParams, IProcessService process) { - _systemParams = systemParams; - _process = process; + this._systemParams = systemParams; + this._process = process; } /// @@ -58,16 +58,16 @@ public void Handle(string key, string value, JToken rawValue) { case "AdjustMousePointerSize": case "MousePointerCustomization": - _process.StartShellExecute("ms-settings:easeofaccess-mouse"); + this._process.StartShellExecute("ms-settings:easeofaccess-mouse"); break; case "EnableTouchPad": case "TouchpadCursorSpeed": - _process.StartShellExecute("ms-settings:devices-touchpad"); + this._process.StartShellExecute("ms-settings:devices-touchpad"); break; case "EnhancePointerPrecision": - HandleEnhancePointerPrecision(param); + this.HandleEnhancePointerPrecision(param); break; case "SetPrimaryMouseButton": @@ -75,11 +75,11 @@ public void Handle(string key, string value, JToken rawValue) break; case "MouseCursorSpeed": - HandleMouseCursorSpeed(param); + this.HandleMouseCursorSpeed(param); break; case "MouseWheelScrollLines": - HandleMouseWheelScrollLines(param); + this.HandleMouseWheelScrollLines(param); break; } } @@ -92,22 +92,22 @@ public void Handle(string key, string value, JToken rawValue) private void HandleMouseCursorSpeed(JObject param) { int speed = param.Value("speedLevel") ?? 10; - _systemParams.SetParameter(SPI_SETMOUSESPEED, 0, (IntPtr)speed, SPIF_UPDATEINIFILE_SENDCHANGE); + this._systemParams.SetParameter(SPI_SETMOUSESPEED, 0, (IntPtr)speed, SPIF_UPDATEINIFILE_SENDCHANGE); } private void HandleMouseWheelScrollLines(JObject param) { int lines = param.Value("scrollLines") ?? 3; - _systemParams.SetParameter(SPI_SETWHEELSCROLLLINES, lines, IntPtr.Zero, SPIF_UPDATEINIFILE_SENDCHANGE); + this._systemParams.SetParameter(SPI_SETWHEELSCROLLLINES, lines, IntPtr.Zero, SPIF_UPDATEINIFILE_SENDCHANGE); } private void HandleEnhancePointerPrecision(JObject param) { bool enable = param.Value("enable") ?? true; int[] mouseParams = new int[3]; - _systemParams.GetParameter(SPI_GETMOUSE, 0, mouseParams, 0); + this._systemParams.GetParameter(SPI_GETMOUSE, 0, mouseParams, 0); mouseParams[2] = enable ? 1 : 0; - _systemParams.SetParameter(SPI_SETMOUSE, 0, mouseParams, SPIF_UPDATEINIFILE_SENDCHANGE); + this._systemParams.SetParameter(SPI_SETMOUSE, 0, mouseParams, SPIF_UPDATEINIFILE_SENDCHANGE); } private static void HandleSetPrimaryMouseButton(JObject param) diff --git a/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs index b3f42f6966..0cbef46319 100644 --- a/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs @@ -28,8 +28,8 @@ internal class PersonalizationSettingsHandler : ICommandHandler public PersonalizationSettingsHandler(IRegistryService registry, IProcessService process) { - _registry = registry; - _process = process; + this._registry = registry; + this._process = process; } /// @@ -42,19 +42,19 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "ApplyColorToTitleBar": - HandleApplyColorToTitleBar(param); + this.HandleApplyColorToTitleBar(param); break; case "EnableTransparency": - HandleEnableTransparency(param); + this.HandleEnableTransparency(param); break; case "HighContrastTheme": - _process.StartShellExecute("ms-settings:easeofaccess-highcontrast"); + this._process.StartShellExecute("ms-settings:easeofaccess-highcontrast"); break; case "SystemThemeMode": - HandleSystemThemeMode(param); + this.HandleSystemThemeMode(param); break; } } @@ -67,7 +67,7 @@ public void Handle(string key, string value, JToken rawValue) private void HandleApplyColorToTitleBar(JObject param) { bool enable = param.Value("enableColor") ?? true; - _registry.SetValue( + this._registry.SetValue( @"Software\Microsoft\Windows\DWM", "ColorPrevalence", enable ? 1 : 0, @@ -77,7 +77,7 @@ private void HandleApplyColorToTitleBar(JObject param) private void HandleEnableTransparency(JObject param) { bool enable = param.Value("enable") ?? true; - _registry.SetValue( + this._registry.SetValue( @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "EnableTransparency", enable ? 1 : 0, @@ -90,7 +90,7 @@ private void HandleSystemThemeMode(JObject param) int value = mode.Equals("light", StringComparison.OrdinalIgnoreCase) ? 1 : 0; const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - _registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); - _registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); + this._registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); + this._registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); } } diff --git a/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs index 43a8fc6f2c..84f6a8e773 100644 --- a/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs @@ -27,8 +27,8 @@ internal class PowerSettingsHandler : ICommandHandler public PowerSettingsHandler(IRegistryService registry, IProcessService process) { - _registry = registry; - _process = process; + this._registry = registry; + this._process = process; } /// @@ -41,12 +41,12 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "BatterySaverActivationLevel": - HandleBatterySaverThreshold(param); + this.HandleBatterySaverThreshold(param); break; case "SetPowerModeOnBattery": case "SetPowerModePluggedIn": - _process.StartShellExecute("ms-settings:powersleep"); + this._process.StartShellExecute("ms-settings:powersleep"); break; } } @@ -59,7 +59,7 @@ public void Handle(string key, string value, JToken rawValue) private void HandleBatterySaverThreshold(JObject param) { int threshold = param.Value("thresholdValue") ?? 20; - _registry.SetValue( + this._registry.SetValue( @"Software\Microsoft\Windows\CurrentVersion\Power\BatterySaver", "ActivationThreshold", threshold, diff --git a/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs index f709f5c46a..741f0b9760 100644 --- a/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs @@ -28,7 +28,7 @@ internal class PrivacySettingsHandler : ICommandHandler public PrivacySettingsHandler(IRegistryService registry) { - _registry = registry; + this._registry = registry; } /// @@ -48,7 +48,7 @@ public void Handle(string key, string value, JToken rawValue) if (subKey != null) { - SetAccessSetting(param, subKey); + this.SetAccessSetting(param, subKey); } } catch (Exception ex) @@ -62,7 +62,7 @@ private void SetAccessSetting(JObject param, string capability) string setting = param.Value("accessSetting") ?? "Allow"; string regValue = setting.Equals("deny", StringComparison.OrdinalIgnoreCase) ? "Deny" : "Allow"; - _registry.SetValue( + this._registry.SetValue( ConsentStoreBase + @"\" + capability, "Value", regValue, diff --git a/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs index d93405aec6..d63f48ec54 100644 --- a/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs @@ -30,7 +30,7 @@ internal class SystemSettingsHandler : ICommandHandler public SystemSettingsHandler(IProcessService process) { - _process = process; + this._process = process; } /// @@ -45,20 +45,20 @@ public void Handle(string key, string value, JToken rawValue) break; case "AutomaticTimeSettingAction": - _process.StartShellExecute("ms-settings:dateandtime"); + this._process.StartShellExecute("ms-settings:dateandtime"); break; case "EnableGameMode": - _process.StartShellExecute("ms-settings:gaming-gamemode"); + this._process.StartShellExecute("ms-settings:gaming-gamemode"); break; case "EnableQuietHours": - _process.StartShellExecute("ms-settings:quiethours"); + this._process.StartShellExecute("ms-settings:quiethours"); break; case "MinimizeWindowsOnMonitorDisconnectAction": case "RememberWindowLocations": - _process.StartShellExecute("ms-settings:display"); + this._process.StartShellExecute("ms-settings:display"); break; } } diff --git a/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs index 7227b85f1e..379eccd622 100644 --- a/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs @@ -38,7 +38,7 @@ internal partial class TaskbarSettingsHandler : ICommandHandler public TaskbarSettingsHandler(IRegistryService registry) { - _registry = registry; + this._registry = registry; } /// @@ -51,25 +51,25 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "AutoHideTaskbar": - HandleAutoHideTaskbar(param); + this.HandleAutoHideTaskbar(param); break; case "TaskbarAlignment": - HandleTaskbarAlignment(param); + this.HandleTaskbarAlignment(param); break; case "TaskViewVisibility": - SetToggle(param, "visibility", "ShowTaskViewButton"); + this.SetToggle(param, "visibility", "ShowTaskViewButton"); break; case "ToggleWidgetsButtonVisibility": - SetToggle(param, "visibility", "TaskbarDa", trueValue: "show"); + this.SetToggle(param, "visibility", "TaskbarDa", trueValue: "show"); break; case "ShowBadgesOnTaskbar": - SetToggle(param, "enableBadging", "TaskbarBadges"); + this.SetToggle(param, "enableBadging", "TaskbarBadges"); break; case "DisplayTaskbarOnAllMonitors": - SetToggle(param, "enable", "MMTaskbarEnabled"); + this.SetToggle(param, "enable", "MMTaskbarEnabled"); break; case "DisplaySecondsInSystrayClock": - SetToggle(param, "enable", "ShowSecondsInSystemClock"); + this.SetToggle(param, "enable", "ShowSecondsInSystemClock"); break; } @@ -99,7 +99,7 @@ private void SetToggle(JObject param, string jsonProperty, string registryValue, regValue = (param.Value(jsonProperty) ?? true) ? 1 : 0; } - _registry.SetValue(ExplorerAdvanced, registryValue, regValue, RegistryValueKind.DWord); + this._registry.SetValue(ExplorerAdvanced, registryValue, regValue, RegistryValueKind.DWord); } private void HandleAutoHideTaskbar(JObject param) @@ -107,7 +107,7 @@ private void HandleAutoHideTaskbar(JObject param) bool hide = param.Value("hideWhenNotUsing"); // Auto-hide uses a binary blob in a different registry path - if (_registry.GetValue(StuckRects3, "Settings", null) is byte[] settings && settings.Length >= 9) + if (this._registry.GetValue(StuckRects3, "Settings", null) is byte[] settings && settings.Length >= 9) { // Bit 0 of byte 8 controls auto-hide if (hide) @@ -119,7 +119,7 @@ private void HandleAutoHideTaskbar(JObject param) settings[8] &= 0xFE; } - _registry.SetValue(StuckRects3, "Settings", settings, RegistryValueKind.Binary); + this._registry.SetValue(StuckRects3, "Settings", settings, RegistryValueKind.Binary); } } @@ -127,6 +127,6 @@ private void HandleTaskbarAlignment(JObject param) { string alignment = param.Value("alignment") ?? "center"; bool useCenter = alignment.Equals("center", StringComparison.OrdinalIgnoreCase); - _registry.SetValue(ExplorerAdvanced, "TaskbarAl", useCenter ? 1 : 0, RegistryValueKind.DWord); + this._registry.SetValue(ExplorerAdvanced, "TaskbarAl", useCenter ? 1 : 0, RegistryValueKind.DWord); } } diff --git a/dotnet/autoShell/Handlers/SystemCommandHandler.cs b/dotnet/autoShell/Handlers/SystemCommandHandler.cs index e604040a6c..3ae032db8b 100644 --- a/dotnet/autoShell/Handlers/SystemCommandHandler.cs +++ b/dotnet/autoShell/Handlers/SystemCommandHandler.cs @@ -24,7 +24,7 @@ internal class SystemCommandHandler : ICommandHandler public SystemCommandHandler(IProcessService process) { - _process = process; + this._process = process; } /// @@ -33,7 +33,7 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "ToggleNotifications": - _process.StartShellExecute("ms-actioncenter:"); + this._process.StartShellExecute("ms-actioncenter:"); break; case "Debug": diff --git a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs index 0a1dd5eaec..1c7ce6969c 100644 --- a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs +++ b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs @@ -64,11 +64,11 @@ private static extern IntPtr SendMessageTimeout( public ThemeCommandHandler(IRegistryService registry, IProcessService process, ISystemParametersService systemParams) { - _registry = registry; - _process = process; - _systemParams = systemParams; + this._registry = registry; + this._process = process; + this._systemParams = systemParams; - LoadThemes(); + this.LoadThemes(); } /// @@ -77,20 +77,20 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "SetWallpaper": - _systemParams.SetParameter(SPI_SETDESKWALLPAPER, 0, value, SPIF_UPDATEINIFILE_SENDCHANGE); + this._systemParams.SetParameter(SPI_SETDESKWALLPAPER, 0, value, SPIF_UPDATEINIFILE_SENDCHANGE); break; case "ApplyTheme": - ApplyTheme(value); + this.ApplyTheme(value); break; case "ListThemes": - var themes = GetInstalledThemes(); + var themes = this.GetInstalledThemes(); Console.WriteLine(JsonConvert.SerializeObject(themes)); break; case "SetThemeMode": - HandleSetThemeMode(value); + this.HandleSetThemeMode(value); break; } } @@ -99,19 +99,19 @@ private void HandleSetThemeMode(string value) { if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase)) { - ToggleLightDarkMode(); + this.ToggleLightDarkMode(); } else if (value.Equals("light", StringComparison.OrdinalIgnoreCase)) { - SetLightDarkMode(true); + this.SetLightDarkMode(true); } else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase)) { - SetLightDarkMode(false); + this.SetLightDarkMode(false); } else if (bool.TryParse(value, out bool useLightMode)) { - SetLightDarkMode(useLightMode); + this.SetLightDarkMode(useLightMode); } } @@ -119,8 +119,8 @@ private void HandleSetThemeMode(string value) private void LoadThemes() { - _themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - _themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + this._themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + this._themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); string[] themePaths = [ @@ -136,22 +136,22 @@ private void LoadThemes() foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme")) { string themeName = Path.GetFileNameWithoutExtension(themeFile); - if (!_themeDictionary.ContainsKey(themeName)) + if (!this._themeDictionary.ContainsKey(themeName)) { - _themeDictionary[themeName] = themeFile; + this._themeDictionary[themeName] = themeFile; // Parse display name from theme file string displayName = GetThemeDisplayName(themeFile); - if (!string.IsNullOrEmpty(displayName) && !_themeDisplayNameDictionary.ContainsKey(displayName)) + if (!string.IsNullOrEmpty(displayName) && !this._themeDisplayNameDictionary.ContainsKey(displayName)) { - _themeDisplayNameDictionary[displayName] = themeName; + this._themeDisplayNameDictionary[displayName] = themeName; } } } } } - _themeDictionary["previous"] = GetCurrentTheme(); + this._themeDictionary["previous"] = this.GetCurrentTheme(); } /// @@ -233,8 +233,8 @@ public List GetInstalledThemes() { HashSet themes = []; - themes.UnionWith(_themeDictionary.Keys); - themes.UnionWith(_themeDisplayNameDictionary.Keys); + themes.UnionWith(this._themeDictionary.Keys); + themes.UnionWith(this._themeDisplayNameDictionary.Keys); return [.. themes]; } @@ -247,7 +247,7 @@ public string GetCurrentTheme() try { const string ThemesPath = @"Software\Microsoft\Windows\CurrentVersion\Themes"; - string currentThemePath = _registry.GetValue(ThemesPath, "CurrentTheme") as string; + string currentThemePath = this._registry.GetValue(ThemesPath, "CurrentTheme") as string; if (!string.IsNullOrEmpty(currentThemePath)) { return Path.GetFileNameWithoutExtension(currentThemePath); @@ -277,8 +277,8 @@ public bool ApplyTheme(string themeName) if (!themeName.Equals("previous", StringComparison.OrdinalIgnoreCase)) { - _process.StartShellExecute(themePath); - _previousTheme = previous; + this._process.StartShellExecute(themePath); + this._previousTheme = previous; return true; } else @@ -287,7 +287,7 @@ public bool ApplyTheme(string themeName) if (success) { - _previousTheme = previous; + this._previousTheme = previous; } return success; @@ -304,12 +304,12 @@ public bool ApplyTheme(string themeName) /// public bool RevertToPreviousTheme() { - if (string.IsNullOrEmpty(_previousTheme)) + if (string.IsNullOrEmpty(this._previousTheme)) { return false; } - string themePath = FindThemePath(_previousTheme); + string themePath = FindThemePath(this._previousTheme); if (string.IsNullOrEmpty(themePath)) { return false; @@ -317,7 +317,7 @@ public bool RevertToPreviousTheme() try { - _process.StartShellExecute(themePath); + this._process.StartShellExecute(themePath); return true; } catch @@ -331,7 +331,7 @@ public bool RevertToPreviousTheme() /// public string GetPreviousTheme() { - return _previousTheme; + return this._previousTheme; } /// @@ -340,15 +340,15 @@ public string GetPreviousTheme() private string FindThemePath(string themeName) { // First check by file name - if (_themeDictionary.TryGetValue(themeName, out string themePath)) + if (this._themeDictionary.TryGetValue(themeName, out string themePath)) { return themePath; } // Then check by display name - if (_themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) + if (this._themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) { - if (_themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) + if (this._themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) { return themePathFromDisplay; } @@ -372,8 +372,8 @@ public bool SetLightDarkMode(bool useLightMode) const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; int value = useLightMode ? 1 : 0; - _registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); - _registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); + this._registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); + this._registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); // Broadcast settings change notification to update UI BroadcastSettingsChange(); @@ -393,7 +393,7 @@ public bool SetLightDarkMode(bool useLightMode) public bool ToggleLightDarkMode() { bool? currentMode = GetCurrentLightMode(); - return currentMode.HasValue && SetLightDarkMode(!currentMode.Value); + return currentMode.HasValue && this.SetLightDarkMode(!currentMode.Value); } /// @@ -423,7 +423,7 @@ private static void BroadcastSettingsChange() try { const string PersonalizePath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - object value = _registry.GetValue(PersonalizePath, "AppsUseLightTheme"); + object value = this._registry.GetValue(PersonalizePath, "AppsUseLightTheme"); return value is int intValue ? intValue == 1 : null; } catch diff --git a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs index 0c8d8201a3..cb9c27570d 100644 --- a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs +++ b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs @@ -28,15 +28,15 @@ internal class VirtualDesktopCommandHandler : ICommandHandler public VirtualDesktopCommandHandler(IAppRegistry appRegistry) { - _appRegistry = appRegistry; + this._appRegistry = appRegistry; // Desktop management COM initialization - _shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(s_clsidImmersiveShell)); - _virtualDesktopManagerInternal = (IVirtualDesktopManagerInternal)_shell.QueryService(s_clsidVirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); - _virtualDesktopManagerInternal_BUGBUG = (IVirtualDesktopManagerInternal_BUGBUG)_shell.QueryService(s_clsidVirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); - _virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(s_clsidVirtualDesktopManager)); - _applicationViewCollection = (IApplicationViewCollection)_shell.QueryService(typeof(IApplicationViewCollection).GUID, typeof(IApplicationViewCollection).GUID); - _virtualDesktopPinnedApps = (IVirtualDesktopPinnedApps)_shell.QueryService(s_clsidVirtualDesktopPinnedApps, typeof(IVirtualDesktopPinnedApps).GUID); + this._shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(s_clsidImmersiveShell)); + this._virtualDesktopManagerInternal = (IVirtualDesktopManagerInternal)this._shell.QueryService(s_clsidVirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); + this._virtualDesktopManagerInternal_BUGBUG = (IVirtualDesktopManagerInternal_BUGBUG)this._shell.QueryService(s_clsidVirtualDesktopManagerInternal, typeof(IVirtualDesktopManagerInternal).GUID); + this._virtualDesktopManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(s_clsidVirtualDesktopManager)); + this._applicationViewCollection = (IApplicationViewCollection)this._shell.QueryService(typeof(IApplicationViewCollection).GUID, typeof(IApplicationViewCollection).GUID); + this._virtualDesktopPinnedApps = (IVirtualDesktopPinnedApps)this._shell.QueryService(s_clsidVirtualDesktopPinnedApps, typeof(IVirtualDesktopPinnedApps).GUID); } /// @@ -56,27 +56,27 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "CreateDesktop": - CreateDesktop(value); + this.CreateDesktop(value); break; case "SwitchDesktop": - SwitchDesktop(value); + this.SwitchDesktop(value); break; case "NextDesktop": - BumpDesktopIndex(1); + this.BumpDesktopIndex(1); break; case "PreviousDesktop": - BumpDesktopIndex(-1); + this.BumpDesktopIndex(-1); break; case "MoveWindowToDesktop": - MoveWindowToDesktop(rawValue); + this.MoveWindowToDesktop(rawValue); break; case "PinWindow": - PinWindow(value); + this.PinWindow(value); break; } } @@ -99,7 +99,7 @@ private void CreateDesktop(string jsonValue) desktopNames = ["desktop X"]; } - if (_virtualDesktopManagerInternal == null) + if (this._virtualDesktopManagerInternal == null) { Debug.WriteLine($"Failed to get Virtual Desktop Manager Internal"); return; @@ -113,7 +113,7 @@ private void CreateDesktop(string jsonValue) try { // Create a new virtual desktop - IVirtualDesktop newDesktop = _virtualDesktopManagerInternal.CreateDesktop(); + IVirtualDesktop newDesktop = this._virtualDesktopManagerInternal.CreateDesktop(); if (newDesktop != null) { @@ -153,17 +153,17 @@ private void SwitchDesktop(string desktopIdentifier) if (!int.TryParse(desktopIdentifier, out int index)) { // Try to find the desktop by name - _virtualDesktopManagerInternal.SwitchDesktop(FindDesktopByName(desktopIdentifier)); + this._virtualDesktopManagerInternal.SwitchDesktop(this.FindDesktopByName(desktopIdentifier)); } else { - SwitchDesktop(index); + this.SwitchDesktop(index); } } private void SwitchDesktop(int index) { - _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + this._virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); desktops.GetAt(index, typeof(IVirtualDesktop).GUID, out object od); // BUGBUG: different windows versions use different COM interfaces @@ -172,17 +172,17 @@ private void SwitchDesktop(int index) if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22621)) { // Use the BUGBUG interface for Windows 11 22H2+ - _virtualDesktopManagerInternal_BUGBUG.SwitchDesktopWithAnimation((IVirtualDesktop)od); + this._virtualDesktopManagerInternal_BUGBUG.SwitchDesktopWithAnimation((IVirtualDesktop)od); } else if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) { // Windows 11 21H2 (build 22000) - _virtualDesktopManagerInternal.SwitchDesktopWithAnimation((IVirtualDesktop)od); + this._virtualDesktopManagerInternal.SwitchDesktopWithAnimation((IVirtualDesktop)od); } else { // Windows 10 - use the original interface - _virtualDesktopManagerInternal.SwitchDesktopAndMoveForegroundView((IVirtualDesktop)od); + this._virtualDesktopManagerInternal.SwitchDesktopAndMoveForegroundView((IVirtualDesktop)od); } Marshal.ReleaseComObject(desktops); @@ -190,9 +190,9 @@ private void SwitchDesktop(int index) private void BumpDesktopIndex(int bump) { - IVirtualDesktop desktop = _virtualDesktopManagerInternal.GetCurrentDesktop(); + IVirtualDesktop desktop = this._virtualDesktopManagerInternal.GetCurrentDesktop(); int index = GetDesktopIndex(desktop); - int count = _virtualDesktopManagerInternal.GetCount(); + int count = this._virtualDesktopManagerInternal.GetCount(); if (index == -1) { @@ -211,14 +211,14 @@ private void BumpDesktopIndex(int bump) index = count - 1; } - SwitchDesktop(index); + this.SwitchDesktop(index); } private IVirtualDesktop FindDesktopByName(string name) { - int count = _virtualDesktopManagerInternal.GetCount(); + int count = this._virtualDesktopManagerInternal.GetCount(); - _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + this._virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); for (int i = 0; i < count; i++) { desktops.GetAt(i, typeof(IVirtualDesktop).GUID, out object od); @@ -237,9 +237,9 @@ private IVirtualDesktop FindDesktopByName(string name) private int GetDesktopIndex(IVirtualDesktop desktop) { - int count = _virtualDesktopManagerInternal.GetCount(); + int count = this._virtualDesktopManagerInternal.GetCount(); - _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + this._virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); for (int i = 0; i < count; i++) { desktops.GetAt(i, typeof(IVirtualDesktop).GUID, out object od); @@ -277,12 +277,12 @@ private void MoveWindowToDesktop(JToken value) return; } - IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(process, _appRegistry); + IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(process, this._appRegistry); if (int.TryParse(desktop, out int desktopIndex)) { - _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); - if (desktopIndex < 1 || desktopIndex > _virtualDesktopManagerInternal.GetCount()) + this._virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + if (desktopIndex < 1 || desktopIndex > this._virtualDesktopManagerInternal.GetCount()) { Debug.WriteLine("Desktop index out of range"); Marshal.ReleaseComObject(desktops); @@ -290,7 +290,7 @@ private void MoveWindowToDesktop(JToken value) } desktops.GetAt(desktopIndex - 1, typeof(IVirtualDesktop).GUID, out object od); Guid g = ((IVirtualDesktop)od).GetId(); - _virtualDesktopManager.MoveWindowToDesktop(hWnd, ref g); + this._virtualDesktopManager.MoveWindowToDesktop(hWnd, ref g); Marshal.ReleaseComObject(desktops); return; } @@ -299,21 +299,21 @@ private void MoveWindowToDesktop(JToken value) if (ivd is not null) { Guid desktopGuid = ivd.GetId(); - _virtualDesktopManager.MoveWindowToDesktop(hWnd, ref desktopGuid); + this._virtualDesktopManager.MoveWindowToDesktop(hWnd, ref desktopGuid); } } private void PinWindow(string processName) { - IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(processName, _appRegistry); + IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(processName, this._appRegistry); if (hWnd != IntPtr.Zero) { - _applicationViewCollection.GetViewForHwnd(hWnd, out IApplicationView view); + this._applicationViewCollection.GetViewForHwnd(hWnd, out IApplicationView view); if (view is not null) { - _virtualDesktopPinnedApps.PinView((IApplicationView)view); + this._virtualDesktopPinnedApps.PinView((IApplicationView)view); } } else diff --git a/dotnet/autoShell/Handlers/WindowCommandHandler.cs b/dotnet/autoShell/Handlers/WindowCommandHandler.cs index 1a70b145ab..b8680fec2d 100644 --- a/dotnet/autoShell/Handlers/WindowCommandHandler.cs +++ b/dotnet/autoShell/Handlers/WindowCommandHandler.cs @@ -21,7 +21,7 @@ internal class WindowCommandHandler : ICommandHandler public WindowCommandHandler(IAppRegistry appRegistry) { - _appRegistry = appRegistry; + this._appRegistry = appRegistry; } /// @@ -39,22 +39,22 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "Maximize": - MaximizeWindow(value); + this.MaximizeWindow(value); break; case "Minimize": - MinimizeWindow(value); + this.MinimizeWindow(value); break; case "SwitchTo": - RaiseWindow(value, _appRegistry); + RaiseWindow(value, this._appRegistry); break; case "Tile": string[] apps = value.Split(','); if (apps.Length == 2) { - TileWindowPair(apps[0], apps[1]); + this.TileWindowPair(apps[0], apps[1]); } break; } @@ -94,7 +94,7 @@ internal static void RaiseWindow(string friendlyName, IAppRegistry appRegistry) private void MaximizeWindow(string friendlyName) { - string processName = _appRegistry.ResolveProcessName(friendlyName); + string processName = this._appRegistry.ResolveProcessName(friendlyName); Process[] processes = Process.GetProcessesByName(processName); foreach (Process p in processes) { @@ -118,7 +118,7 @@ private void MaximizeWindow(string friendlyName) private void MinimizeWindow(string friendlyName) { - string processName = _appRegistry.ResolveProcessName(friendlyName); + string processName = this._appRegistry.ResolveProcessName(friendlyName); Process[] processes = Process.GetProcessesByName(processName); foreach (Process p in processes) { @@ -141,7 +141,7 @@ private void MinimizeWindow(string friendlyName) private void TileWindowPair(string name1, string name2) { // TODO: Update this to account for UWP apps (e.g. calculator). UWPs are hosted by ApplicationFrameHost.exe - string processName1 = _appRegistry.ResolveProcessName(name1); + string processName1 = this._appRegistry.ResolveProcessName(name1); Process[] processes1 = Process.GetProcessesByName(processName1); IntPtr hWnd1 = IntPtr.Zero; IntPtr hWnd2 = IntPtr.Zero; @@ -163,7 +163,7 @@ private void TileWindowPair(string name1, string name2) (hWnd1, pid1) = FindWindowByTitle(processName1); } - string processName2 = _appRegistry.ResolveProcessName(name2); + string processName2 = this._appRegistry.ResolveProcessName(name2); Process[] processes2 = Process.GetProcessesByName(processName2); foreach (Process p in processes2) { diff --git a/dotnet/autoShell/Services/WindowsAppRegistry.cs b/dotnet/autoShell/Services/WindowsAppRegistry.cs index d96b45c4b5..395186e3b6 100644 --- a/dotnet/autoShell/Services/WindowsAppRegistry.cs +++ b/dotnet/autoShell/Services/WindowsAppRegistry.cs @@ -26,7 +26,7 @@ public WindowsAppRegistry() { string userName = Environment.UserName; - _appMetadata = new SortedList + this._appMetadata = new SortedList { { "chrome", ["chrome.exe"] }, { "power point", ["C:\\Program Files\\Microsoft Office\\root\\Office16\\POWERPNT.EXE"] }, @@ -56,9 +56,9 @@ public WindowsAppRegistry() { "github copilot", [$"{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"] }, }; - foreach (var kvp in _appMetadata) + foreach (var kvp in this._appMetadata) { - _friendlyNameToPath.Add(kvp.Key, kvp.Value[0]); + this._friendlyNameToPath.Add(kvp.Key, kvp.Value[0]); } try @@ -66,7 +66,7 @@ public WindowsAppRegistry() var installedApps = GetAllInstalledAppIds(); foreach (var kvp in installedApps) { - _friendlyNameToId.Add(kvp.Key, kvp.Value); + this._friendlyNameToId.Add(kvp.Key, kvp.Value); } } catch (Exception ex) @@ -77,12 +77,12 @@ public WindowsAppRegistry() public string GetExecutablePath(string friendlyName) { - return (string)_friendlyNameToPath[friendlyName.ToLowerInvariant()]; + return (string)this._friendlyNameToPath[friendlyName.ToLowerInvariant()]; } public string GetAppUserModelId(string friendlyName) { - return (string)_friendlyNameToId[friendlyName.ToLowerInvariant()]; + return (string)this._friendlyNameToId[friendlyName.ToLowerInvariant()]; } public string ResolveProcessName(string friendlyName) @@ -93,21 +93,21 @@ public string ResolveProcessName(string friendlyName) public string GetWorkingDirectoryEnvVar(string friendlyName) { - return _appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 1 + return this._appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 1 ? value[1] : null; } public string GetArguments(string friendlyName) { - return _appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 2 + return this._appMetadata.TryGetValue(friendlyName.ToLowerInvariant(), out string[] value) && value.Length > 2 ? string.Join(" ", value.Skip(2)) : null; } public IEnumerable GetAllAppNames() { - return _friendlyNameToId.Keys.Cast(); + return this._friendlyNameToId.Keys.Cast(); } private static SortedList GetAllInstalledAppIds() From cd7fe5bc5b80cfc506db175b0ccb1e71ee79c807 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 2 Apr 2026 14:11:31 -0700 Subject: [PATCH 13/27] more cleanup --- dotnet/.editorconfig | 15 +- .../autoShell.Tests/SettingsHandlerTests.cs | 2 - .../ThemeCommandHandlerTests.cs | 1 - dotnet/autoShell.Tests/autoShell.Tests.csproj | 3 +- dotnet/autoShell/AutoShell.cs | 53 +- .../autoShell/Handlers/AppCommandHandler.cs | 42 +- .../autoShell/Handlers/AudioCommandHandler.cs | 30 +- .../Handlers/DisplayCommandHandler.cs | 90 +-- .../Handlers/NetworkCommandHandler.cs | 312 ++++----- .../Settings/AccessibilitySettingsHandler.cs | 18 +- .../Settings/DisplaySettingsHandler.cs | 82 +-- .../Settings/FileExplorerSettingsHandler.cs | 16 +- .../Handlers/Settings/MouseSettingsHandler.cs | 46 +- .../PersonalizationSettingsHandler.cs | 18 +- .../Handlers/Settings/PowerSettingsHandler.cs | 16 +- .../Settings/PrivacySettingsHandler.cs | 14 +- .../Settings/SystemSettingsHandler.cs | 14 +- .../Settings/TaskbarSettingsHandler.cs | 77 +-- .../Handlers/SystemCommandHandler.cs | 22 +- .../autoShell/Handlers/ThemeCommandHandler.cs | 404 ++++++------ .../Handlers/VirtualDesktopCommandHandler.cs | 618 +++++++++--------- .../Handlers/WindowCommandHandler.cs | 166 ++--- dotnet/autoShell/UIAutomation.cs | 48 +- 23 files changed, 1058 insertions(+), 1049 deletions(-) diff --git a/dotnet/.editorconfig b/dotnet/.editorconfig index 0c2fe634e8..c68815bd76 100644 --- a/dotnet/.editorconfig +++ b/dotnet/.editorconfig @@ -63,10 +63,10 @@ file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the # Organize usings dotnet_sort_system_directives_first = true # this. preferences -dotnet_style_qualification_for_field = true:error -dotnet_style_qualification_for_property = true:error -dotnet_style_qualification_for_method = true:error -dotnet_style_qualification_for_event = true:error +dotnet_style_qualification_for_field = false:none +dotnet_style_qualification_for_property = false:none +dotnet_style_qualification_for_method = false:none +dotnet_style_qualification_for_event = false:none # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion @@ -231,8 +231,9 @@ dotnet_diagnostic.RCS1241.severity = none # Implement IComparable when implement dotnet_diagnostic.IDE0001.severity = none # Simplify name dotnet_diagnostic.IDE0002.severity = none # Simplify member access dotnet_diagnostic.IDE0004.severity = none # Remove unnecessary cast -dotnet_diagnostic.IDE0005.severity = none # Remove unnecessary cast +dotnet_diagnostic.IDE0005.severity = warning # Remove unnecessary using directives dotnet_diagnostic.IDE0009.severity = none # Add this or Me qualification +dotnet_diagnostic.IDE1006.severity = none # Naming rule violation (allow Win32 conventions) dotnet_diagnostic.IDE0010.severity = none # Populate switch dotnet_diagnostic.IDE0017.severity = none # Object initializers dotnet_diagnostic.IDE0022.severity = none # Use block body for method @@ -307,11 +308,11 @@ dotnet_naming_symbols.any_async_methods.required_modifiers = async dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.local_constant_should_be_pascal_case.symbols = local_constant dotnet_naming_rule.local_constant_should_be_pascal_case.style = pascal_case_style -dotnet_naming_rule.local_constant_should_be_pascal_case.severity = error +dotnet_naming_rule.local_constant_should_be_pascal_case.severity = suggestion dotnet_naming_rule.private_static_fields_underscored.symbols = private_static_fields dotnet_naming_rule.private_static_fields_underscored.style = static_underscored diff --git a/dotnet/autoShell.Tests/SettingsHandlerTests.cs b/dotnet/autoShell.Tests/SettingsHandlerTests.cs index 4864abd457..a380a09537 100644 --- a/dotnet/autoShell.Tests/SettingsHandlerTests.cs +++ b/dotnet/autoShell.Tests/SettingsHandlerTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Diagnostics; -using autoShell.Handlers; using autoShell.Handlers.Settings; using autoShell.Services; using Microsoft.Win32; diff --git a/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs b/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs index afa511d8f2..95f882163c 100644 --- a/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs +++ b/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using autoShell.Handlers; using autoShell.Services; using Microsoft.Win32; diff --git a/dotnet/autoShell.Tests/autoShell.Tests.csproj b/dotnet/autoShell.Tests/autoShell.Tests.csproj index 28b99ca8c7..1c6050d5a2 100644 --- a/dotnet/autoShell.Tests/autoShell.Tests.csproj +++ b/dotnet/autoShell.Tests/autoShell.Tests.csproj @@ -7,7 +7,8 @@ false true - $(NoWarn);JSON002 + + $(NoWarn);JSON002;IDE1006 diff --git a/dotnet/autoShell/AutoShell.cs b/dotnet/autoShell/AutoShell.cs index e6f8ab6b5b..99a5245332 100644 --- a/dotnet/autoShell/AutoShell.cs +++ b/dotnet/autoShell/AutoShell.cs @@ -16,11 +16,14 @@ namespace autoShell; internal class AutoShell { + #region P/Invoke + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetCommandLineW(); - private static CommandDispatcher s_dispatcher; + #endregion P/Invoke + private static CommandDispatcher s_dispatcher; /// /// Constructor used to get system wide information required for specific commands. @@ -56,6 +59,25 @@ static AutoShell() ); } + internal static void LogError(Exception ex) + { + Debug.WriteLine(ex); + ConsoleColor previousColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Error: " + ex.Message); + Console.ForegroundColor = previousColor; + } + + internal static void LogWarning(string message) + { + Debug.WriteLine(message); + ConsoleColor previousColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Warning: " + message); + Console.ForegroundColor = previousColor; + } + + /// /// Program entry point /// @@ -89,12 +111,12 @@ private static void Main(string[] args) JArray commands = JArray.Parse(cmdLine); foreach (JObject jo in commands.Children()) { - execLine(jo); + ExecLine(jo); } } catch (JsonReaderException) { - execLine(JObject.Parse(cmdLine)); + ExecLine(JObject.Parse(cmdLine)); } // exit @@ -120,7 +142,7 @@ private static void Main(string[] args) JObject root = JObject.Parse(line); // execute the line - quit = execLine(root); + quit = ExecLine(root); } catch (Exception ex) { @@ -129,26 +151,7 @@ private static void Main(string[] args) } } - internal static void LogError(Exception ex) - { - Debug.WriteLine(ex); - ConsoleColor previousColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Error: " + ex.Message); - Console.ForegroundColor = previousColor; - } - - internal static void LogWarning(string message) - { - Debug.WriteLine(message); - ConsoleColor previousColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Warning: " + message); - Console.ForegroundColor = previousColor; - } - - - private static bool execLine(JObject root) + private static bool ExecLine(JObject root) => s_dispatcher.Dispatch(root); -} +} \ No newline at end of file diff --git a/dotnet/autoShell/Handlers/AppCommandHandler.cs b/dotnet/autoShell/Handlers/AppCommandHandler.cs index 3a4db48d2d..18e85498e9 100644 --- a/dotnet/autoShell/Handlers/AppCommandHandler.cs +++ b/dotnet/autoShell/Handlers/AppCommandHandler.cs @@ -37,20 +37,37 @@ public void Handle(string key, string value, JToken rawValue) { switch (key) { - case "LaunchProgram": - this.OpenApplication(value); - break; - case "CloseProgram": this.CloseApplication(value); break; + case "LaunchProgram": + this.OpenApplication(value); + break; + case "ListAppNames": Console.WriteLine(JsonConvert.SerializeObject(this._appRegistry.GetAllAppNames())); break; } } + private void CloseApplication(string friendlyName) + { + string processName = this._appRegistry.ResolveProcessName(friendlyName); + Process[] processes = this._processService.GetProcessesByName(processName); + if (processes.Length != 0) + { + Debug.WriteLine("Closing " + friendlyName); + foreach (Process p in processes) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + p.CloseMainWindow(); + } + } + } + } + private void OpenApplication(string friendlyName) { string processName = this._appRegistry.ResolveProcessName(friendlyName); @@ -109,21 +126,4 @@ private void OpenApplication(string friendlyName) WindowCommandHandler.RaiseWindow(friendlyName, this._appRegistry); } } - - private void CloseApplication(string friendlyName) - { - string processName = this._appRegistry.ResolveProcessName(friendlyName); - Process[] processes = this._processService.GetProcessesByName(processName); - if (processes.Length != 0) - { - Debug.WriteLine("Closing " + friendlyName); - foreach (Process p in processes) - { - if (p.MainWindowHandle != IntPtr.Zero) - { - p.CloseMainWindow(); - } - } - } - } } diff --git a/dotnet/autoShell/Handlers/AudioCommandHandler.cs b/dotnet/autoShell/Handlers/AudioCommandHandler.cs index 9d2c1e34d4..565f2e126e 100644 --- a/dotnet/autoShell/Handlers/AudioCommandHandler.cs +++ b/dotnet/autoShell/Handlers/AudioCommandHandler.cs @@ -12,14 +12,6 @@ namespace autoShell.Handlers; /// internal class AudioCommandHandler : ICommandHandler { - /// - public IEnumerable SupportedCommands { get; } = - [ - "Mute", - "RestoreVolume", - "Volume", - ]; - private readonly IAudioService _audio; private double _savedVolumePct; @@ -28,25 +20,33 @@ public AudioCommandHandler(IAudioService audio) this._audio = audio; } + /// + public IEnumerable SupportedCommands { get; } = + [ + "Mute", + "RestoreVolume", + "Volume", + ]; + /// public void Handle(string key, string value, JToken rawValue) { switch (key) { - case "Volume": - if (int.TryParse(value, out int pct)) + case "Mute": + if (bool.TryParse(value, out bool mute)) { - this._savedVolumePct = this._audio.GetVolume(); - this._audio.SetVolume(pct); + this._audio.SetMute(mute); } break; case "RestoreVolume": this._audio.SetVolume((int)this._savedVolumePct); break; - case "Mute": - if (bool.TryParse(value, out bool mute)) + case "Volume": + if (int.TryParse(value, out int pct)) { - this._audio.SetMute(mute); + this._savedVolumePct = this._audio.GetVolume(); + this._audio.SetVolume(pct); } break; } diff --git a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs index f2ece4ccf9..3919d063fe 100644 --- a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs +++ b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs @@ -90,60 +90,23 @@ public void Handle(string key, string value, JToken rawValue) { switch (key) { - case "SetTextSize": - if (int.TryParse(value, out int textSizePct)) - { - this.SetTextSize(textSizePct); - } + case "ListResolutions": + ListDisplayResolutions(); break; case "SetScreenResolution": - this.SetDisplayResolution(rawValue); + SetDisplayResolution(rawValue); break; - case "ListResolutions": - ListDisplayResolutions(); + case "SetTextSize": + if (int.TryParse(value, out int textSizePct)) + { + SetTextSize(textSizePct); + } break; } } - /// - /// Sets the system text scaling factor (percentage). - /// - private void SetTextSize(int percentage) - { - try - { - if (percentage == -1) - { - percentage = new Random().Next(100, 225 + 1); - } - - if (percentage < 100) - { - percentage = 100; - } - else if (percentage > 225) - { - percentage = 225; - } - - Process.Start(new ProcessStartInfo - { - FileName = "ms-settings:easeofaccess", - UseShellExecute = true - }); - -#pragma warning disable CS0618 // UIAutomation is intentionally marked obsolete as a last-resort approach - UIAutomation.SetTextSizeViaUIAutomation(percentage); -#pragma warning restore CS0618 - } - catch (Exception ex) - { - AutoShell.LogError(ex); - } - } - /// /// Lists all available display resolutions for the primary monitor. /// @@ -302,4 +265,41 @@ private void SetDisplayResolution(JToken value) AutoShell.LogError(ex); } } + + /// + /// Sets the system text scaling factor (percentage). + /// + private void SetTextSize(int percentage) + { + try + { + if (percentage == -1) + { + percentage = new Random().Next(100, 225 + 1); + } + + if (percentage < 100) + { + percentage = 100; + } + else if (percentage > 225) + { + percentage = 225; + } + + Process.Start(new ProcessStartInfo + { + FileName = "ms-settings:easeofaccess", + UseShellExecute = true + }); + +#pragma warning disable CS0618 // UIAutomation is intentionally marked obsolete as a last-resort approach + UIAutomation.SetTextSizeViaUIAutomation(percentage); +#pragma warning restore CS0618 + } + catch (Exception ex) + { + AutoShell.LogError(ex); + } + } } diff --git a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs index a78bf5499d..946d648a9d 100644 --- a/dotnet/autoShell/Handlers/NetworkCommandHandler.cs +++ b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs @@ -59,7 +59,7 @@ private interface IRadioManager private static extern int WlanGetAvailableNetworkList(IntPtr hClientHandle, ref Guid pInterfaceGuid, uint dwFlags, IntPtr pReserved, out IntPtr ppAvailableNetworkList); [DllImport("wlanapi.dll")] - private static extern int WlanScan(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pDot11Ssid, IntPtr pIeData, IntPtr pReserved); + private static extern int WlanScan(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pDOT11_SSID, IntPtr pIeData, IntPtr pReserved); [DllImport("wlanapi.dll")] private static extern void WlanFreeMemory(IntPtr pMemory); @@ -134,7 +134,7 @@ private struct WLAN_CONNECTION_PARAMETERS public WLAN_CONNECTION_MODE wlanConnectionMode; [MarshalAs(UnmanagedType.LPWStr)] public string strProfile; - public IntPtr pDot11Ssid; + public IntPtr pDOT11_SSID; public IntPtr pDesiredBssidList; public DOT11_BSS_TYPE dot11BssType; public uint dwFlags; @@ -175,12 +175,11 @@ public void Handle(string key, string value, JToken rawValue) { switch (key) { - case "ToggleAirplaneMode": - this.SetAirplaneMode(bool.Parse(value)); - break; - - case "ListWifiNetworks": - ListWifiNetworks(); + case "BluetoothToggle": + case "EnableMeteredConnections": + case "EnableWifi": + // Not yet implemented — requires additional infrastructure + Debug.WriteLine($"Command not yet implemented: {key}"); break; case "ConnectWifi": @@ -194,72 +193,162 @@ public void Handle(string key, string value, JToken rawValue) this.DisconnectFromWifi(); break; - case "BluetoothToggle": - case "EnableWifi": - case "EnableMeteredConnections": - // Not yet implemented — requires additional infrastructure - Debug.WriteLine($"Command not yet implemented: {key}"); + case "ListWifiNetworks": + ListWifiNetworks(); + break; + + case "ToggleAirplaneMode": + this.SetAirplaneMode(bool.Parse(value)); break; } } /// - /// Sets the airplane mode state using the Radio Management API. + /// Connects to a WiFi network by name (SSID). /// - private void SetAirplaneMode(bool enable) + private void ConnectToWifi(string ssid, string password = null) { - IRadioManager radioManager = null; + IntPtr clientHandle = IntPtr.Zero; + IntPtr wlanInterfaceList = IntPtr.Zero; + try { - Type radioManagerType = Type.GetTypeFromCLSID(s_clsidRadioManagementApi); - if (radioManagerType == null) + int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); + if (result != 0) { - Debug.WriteLine("Failed to get Radio Management API type"); + AutoShell.LogWarning($"Failed to open WLAN handle: {result}"); return; } - object obj = Activator.CreateInstance(radioManagerType); - radioManager = (IRadioManager)obj; - - if (radioManager == null) + result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); + if (result != 0) { - Debug.WriteLine("Failed to create Radio Manager instance"); + AutoShell.LogWarning($"Failed to enumerate WLAN interfaces: {result}"); return; } - int hr = radioManager.GetSystemRadioState(out int currentState, out int _, out int _); - if (hr < 0) + WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + + if (interfaceList.dwNumberOfItems == 0) { - Debug.WriteLine($"Failed to get system radio state: HRESULT 0x{hr:X8}"); + AutoShell.LogWarning("No wireless interfaces found."); return; } - bool airplaneModeCurrentlyOn = currentState == 0; - Debug.WriteLine($"Current airplane mode state: {(airplaneModeCurrentlyOn ? "on" : "off")}"); + WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[0]; - int newState = enable ? 0 : 1; - hr = radioManager.SetSystemRadioState(newState); - if (hr < 0) + if (!string.IsNullOrEmpty(password)) { - Debug.WriteLine($"Failed to set system radio state: HRESULT 0x{hr:X8}"); + string profileXml = GenerateWifiProfileXml(ssid, password); + + result = WlanSetProfile(clientHandle, ref interfaceInfo.InterfaceGuid, 0, profileXml, null, true, IntPtr.Zero, out uint reasonCode); + if (result != 0) + { + AutoShell.LogWarning($"Failed to set WiFi profile: {result}, reason: {reasonCode}"); + return; + } + } + + WLAN_CONNECTION_PARAMETERS connectionParams = new WLAN_CONNECTION_PARAMETERS + { + wlanConnectionMode = WLAN_CONNECTION_MODE.wlan_connection_mode_profile, + strProfile = ssid, + pDOT11_SSID = IntPtr.Zero, + pDesiredBssidList = IntPtr.Zero, + dot11BssType = DOT11_BSS_TYPE.dot11_BSS_type_any, + dwFlags = 0 + }; + + result = WlanConnect(clientHandle, ref interfaceInfo.InterfaceGuid, ref connectionParams, IntPtr.Zero); + if (result != 0) + { + AutoShell.LogWarning($"Failed to connect to WiFi network '{ssid}': {result}"); return; } - Debug.WriteLine($"Airplane mode set to: {(enable ? "on" : "off")}"); + Debug.WriteLine($"Successfully initiated connection to WiFi network: {ssid}"); + Console.WriteLine($"Connecting to WiFi network: {ssid}"); } - catch (COMException ex) + catch (Exception ex) { - Debug.WriteLine($"COM Exception setting airplane mode: {ex.Message} (HRESULT: 0x{ex.HResult:X8})"); + AutoShell.LogError(ex); + } + finally + { + if (wlanInterfaceList != IntPtr.Zero) + { + WlanFreeMemory(wlanInterfaceList); + } + + if (clientHandle != IntPtr.Zero) + { + _ = WlanCloseHandle(clientHandle, IntPtr.Zero); + } + } + } + + /// + /// Disconnects from the currently connected WiFi network. + /// + private void DisconnectFromWifi() + { + IntPtr clientHandle = IntPtr.Zero; + IntPtr wlanInterfaceList = IntPtr.Zero; + + try + { + int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); + if (result != 0) + { + AutoShell.LogWarning($"Failed to open WLAN handle: {result}"); + return; + } + + result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); + if (result != 0) + { + AutoShell.LogWarning($"Failed to enumerate WLAN interfaces: {result}"); + return; + } + + WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + + if (interfaceList.dwNumberOfItems == 0) + { + AutoShell.LogWarning("No wireless interfaces found."); + return; + } + + for (int i = 0; i < interfaceList.dwNumberOfItems; i++) + { + WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i]; + + result = WlanDisconnect(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero); + if (result != 0) + { + AutoShell.LogWarning($"Failed to disconnect from WiFi on interface {i}: {result}"); + } + else + { + Debug.WriteLine($"Successfully disconnected from WiFi on interface: {interfaceInfo.strInterfaceDescription}"); + Console.WriteLine("Disconnected from WiFi"); + } + } } catch (Exception ex) { - Debug.WriteLine($"Failed to set airplane mode: {ex.Message}"); + AutoShell.LogError(ex); } finally { - if (radioManager != null) + if (wlanInterfaceList != IntPtr.Zero) { - Marshal.ReleaseComObject(radioManager); + WlanFreeMemory(wlanInterfaceList); + } + + if (clientHandle != IntPtr.Zero) + { + _ = WlanCloseHandle(clientHandle, IntPtr.Zero); } } } @@ -378,85 +467,62 @@ private void ListWifiNetworks() } /// - /// Connects to a WiFi network by name (SSID). + /// Sets the airplane mode state using the Radio Management API. /// - private void ConnectToWifi(string ssid, string password = null) + private void SetAirplaneMode(bool enable) { - IntPtr clientHandle = IntPtr.Zero; - IntPtr wlanInterfaceList = IntPtr.Zero; - + IRadioManager radioManager = null; try { - int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); - if (result != 0) - { - AutoShell.LogWarning($"Failed to open WLAN handle: {result}"); - return; - } - - result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); - if (result != 0) + Type radioManagerType = Type.GetTypeFromCLSID(s_clsidRadioManagementApi); + if (radioManagerType == null) { - AutoShell.LogWarning($"Failed to enumerate WLAN interfaces: {result}"); + Debug.WriteLine("Failed to get Radio Management API type"); return; } - WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + object obj = Activator.CreateInstance(radioManagerType); + radioManager = (IRadioManager)obj; - if (interfaceList.dwNumberOfItems == 0) + if (radioManager == null) { - AutoShell.LogWarning("No wireless interfaces found."); + Debug.WriteLine("Failed to create Radio Manager instance"); return; } - WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[0]; - - if (!string.IsNullOrEmpty(password)) + int hr = radioManager.GetSystemRadioState(out int currentState, out int _, out int _); + if (hr < 0) { - string profileXml = GenerateWifiProfileXml(ssid, password); - - result = WlanSetProfile(clientHandle, ref interfaceInfo.InterfaceGuid, 0, profileXml, null, true, IntPtr.Zero, out uint reasonCode); - if (result != 0) - { - AutoShell.LogWarning($"Failed to set WiFi profile: {result}, reason: {reasonCode}"); - return; - } + Debug.WriteLine($"Failed to get system radio state: HRESULT 0x{hr:X8}"); + return; } - WLAN_CONNECTION_PARAMETERS connectionParams = new WLAN_CONNECTION_PARAMETERS - { - wlanConnectionMode = WLAN_CONNECTION_MODE.wlan_connection_mode_profile, - strProfile = ssid, - pDot11Ssid = IntPtr.Zero, - pDesiredBssidList = IntPtr.Zero, - dot11BssType = DOT11_BSS_TYPE.dot11_BSS_type_any, - dwFlags = 0 - }; + bool airplaneModeCurrentlyOn = currentState == 0; + Debug.WriteLine($"Current airplane mode state: {(airplaneModeCurrentlyOn ? "on" : "off")}"); - result = WlanConnect(clientHandle, ref interfaceInfo.InterfaceGuid, ref connectionParams, IntPtr.Zero); - if (result != 0) + int newState = enable ? 0 : 1; + hr = radioManager.SetSystemRadioState(newState); + if (hr < 0) { - AutoShell.LogWarning($"Failed to connect to WiFi network '{ssid}': {result}"); + Debug.WriteLine($"Failed to set system radio state: HRESULT 0x{hr:X8}"); return; } - Debug.WriteLine($"Successfully initiated connection to WiFi network: {ssid}"); - Console.WriteLine($"Connecting to WiFi network: {ssid}"); + Debug.WriteLine($"Airplane mode set to: {(enable ? "on" : "off")}"); + } + catch (COMException ex) + { + Debug.WriteLine($"COM Exception setting airplane mode: {ex.Message} (HRESULT: 0x{ex.HResult:X8})"); } catch (Exception ex) { - AutoShell.LogError(ex); + Debug.WriteLine($"Failed to set airplane mode: {ex.Message}"); } finally { - if (wlanInterfaceList != IntPtr.Zero) - { - WlanFreeMemory(wlanInterfaceList); - } - - if (clientHandle != IntPtr.Zero) + if (radioManager != null) { - _ = WlanCloseHandle(clientHandle, IntPtr.Zero); + Marshal.ReleaseComObject(radioManager); } } } @@ -495,70 +561,4 @@ private static string GenerateWifiProfileXml(string ssid, string password) "; } - - /// - /// Disconnects from the currently connected WiFi network. - /// - private void DisconnectFromWifi() - { - IntPtr clientHandle = IntPtr.Zero; - IntPtr wlanInterfaceList = IntPtr.Zero; - - try - { - int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); - if (result != 0) - { - AutoShell.LogWarning($"Failed to open WLAN handle: {result}"); - return; - } - - result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); - if (result != 0) - { - AutoShell.LogWarning($"Failed to enumerate WLAN interfaces: {result}"); - return; - } - - WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); - - if (interfaceList.dwNumberOfItems == 0) - { - AutoShell.LogWarning("No wireless interfaces found."); - return; - } - - for (int i = 0; i < interfaceList.dwNumberOfItems; i++) - { - WLAN_INTERFACE_INFO interfaceInfo = interfaceList.InterfaceInfo[i]; - - result = WlanDisconnect(clientHandle, ref interfaceInfo.InterfaceGuid, IntPtr.Zero); - if (result != 0) - { - AutoShell.LogWarning($"Failed to disconnect from WiFi on interface {i}: {result}"); - } - else - { - Debug.WriteLine($"Successfully disconnected from WiFi on interface: {interfaceInfo.strInterfaceDescription}"); - Console.WriteLine("Disconnected from WiFi"); - } - } - } - catch (Exception ex) - { - AutoShell.LogError(ex); - } - finally - { - if (wlanInterfaceList != IntPtr.Zero) - { - WlanFreeMemory(wlanInterfaceList); - } - - if (clientHandle != IntPtr.Zero) - { - _ = WlanCloseHandle(clientHandle, IntPtr.Zero); - } - } - } } diff --git a/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs index 902c664d4f..4d48768ff3 100644 --- a/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs @@ -14,6 +14,15 @@ namespace autoShell.Handlers.Settings; /// internal class AccessibilitySettingsHandler : ICommandHandler { + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public AccessibilitySettingsHandler(IRegistryService registry, IProcessService process) + { + this._registry = registry; + this._process = process; + } + /// public IEnumerable SupportedCommands { get; } = [ @@ -24,15 +33,6 @@ internal class AccessibilitySettingsHandler : ICommandHandler "MonoAudioToggle", ]; - private readonly IRegistryService _registry; - private readonly IProcessService _process; - - public AccessibilitySettingsHandler(IRegistryService registry, IProcessService process) - { - this._registry = registry; - this._process = process; - } - /// public void Handle(string key, string value, JToken rawValue) { diff --git a/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs index 92089f103a..067b808aed 100644 --- a/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs @@ -17,6 +17,15 @@ namespace autoShell.Handlers.Settings; /// internal class DisplaySettingsHandler : ICommandHandler { + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public DisplaySettingsHandler(IRegistryService registry, IProcessService process) + { + this._registry = registry; + this._process = process; + } + /// public IEnumerable SupportedCommands { get; } = [ @@ -29,15 +38,6 @@ internal class DisplaySettingsHandler : ICommandHandler "RotationLock", ]; - private readonly IRegistryService _registry; - private readonly IProcessService _process; - - public DisplaySettingsHandler(IRegistryService registry, IProcessService process) - { - this._registry = registry; - this._process = process; - } - /// public void Handle(string key, string value, JToken rawValue) { @@ -47,23 +47,23 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { - case "AdjustScreenBrightness": - this.HandleAdjustScreenBrightness(param); - break; - - case "DisplayScaling": - this.HandleDisplayScaling(param); - break; - case "AdjustColorTemperature": this._process.StartShellExecute("ms-settings:nightlight"); break; + case "AdjustScreenBrightness": + this.HandleAdjustScreenBrightness(param); + break; + case "AdjustScreenOrientation": case "DisplayResolutionAndAspectRatio": this._process.StartShellExecute("ms-settings:display"); break; + case "DisplayScaling": + this.HandleDisplayScaling(param); + break; + case "EnableBlueLightFilterSchedule": this.HandleBlueLightFilter(param); break; @@ -114,6 +114,30 @@ private void HandleDisplayScaling(JObject param) } } + private void HandleBlueLightFilter(JObject param) + { + bool disabled = param.Value("nightLightScheduleDisabled") ?? false; + byte[] data = disabled + ? [0x02, 0x00, 0x00, 0x00] + : [0x02, 0x00, 0x00, 0x01]; + + this._registry.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount\Current\default$windows.data.bluelightreduction.settings\windows.data.bluelightreduction.settings", + "Data", + data, + RegistryValueKind.Binary); + } + + private void HandleRotationLock(JObject param) + { + bool enable = param.Value("enable") ?? true; + this._registry.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell", + "RotationLockPreference", + enable ? 1 : 0, + RegistryValueKind.DWord); + } + private static byte GetCurrentBrightness() { try @@ -150,28 +174,4 @@ private static void SetBrightness(byte brightness) Debug.WriteLine($"Failed to set brightness: {ex.Message}"); } } - - private void HandleBlueLightFilter(JObject param) - { - bool disabled = param.Value("nightLightScheduleDisabled") ?? false; - byte[] data = disabled - ? [0x02, 0x00, 0x00, 0x00] - : [0x02, 0x00, 0x00, 0x01]; - - this._registry.SetValue( - @"Software\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount\Current\default$windows.data.bluelightreduction.settings\windows.data.bluelightreduction.settings", - "Data", - data, - RegistryValueKind.Binary); - } - - private void HandleRotationLock(JObject param) - { - bool enable = param.Value("enable") ?? true; - this._registry.SetValue( - @"Software\Microsoft\Windows\CurrentVersion\ImmersiveShell", - "RotationLockPreference", - enable ? 1 : 0, - RegistryValueKind.DWord); - } } diff --git a/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs index 2ed8442edb..36f747e69b 100644 --- a/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs @@ -15,17 +15,12 @@ namespace autoShell.Handlers.Settings; /// internal partial class FileExplorerSettingsHandler : ICommandHandler { + #region P/Invoke private const string ExplorerAdvanced = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; [LibraryImport("user32.dll")] private static partial IntPtr SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - /// - public IEnumerable SupportedCommands { get; } = - [ - "ShowFileExtensions", - "ShowHiddenAndSystemFiles", - ]; + #endregion P/Invoke private readonly IRegistryService _registry; @@ -34,6 +29,13 @@ public FileExplorerSettingsHandler(IRegistryService registry) this._registry = registry; } + /// + public IEnumerable SupportedCommands { get; } = + [ + "ShowFileExtensions", + "ShowHiddenAndSystemFiles", + ]; + /// public void Handle(string key, string value, JToken rawValue) { diff --git a/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs index a8edf11618..7cc50876ac 100644 --- a/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs @@ -15,6 +15,7 @@ namespace autoShell.Handlers.Settings; /// internal partial class MouseSettingsHandler : ICommandHandler { + #region P/Invoke private const int SPI_GETMOUSE = 3; private const int SPI_SETMOUSE = 4; private const int SPI_SETMOUSESPEED = 0x0071; @@ -24,6 +25,16 @@ internal partial class MouseSettingsHandler : ICommandHandler [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool SwapMouseButton(int fSwap); + #endregion P/Invoke + + private readonly ISystemParametersService _systemParams; + private readonly IProcessService _process; + + public MouseSettingsHandler(ISystemParametersService systemParams, IProcessService process) + { + this._systemParams = systemParams; + this._process = process; + } /// public IEnumerable SupportedCommands { get; } = @@ -38,15 +49,6 @@ internal partial class MouseSettingsHandler : ICommandHandler "TouchpadCursorSpeed", ]; - private readonly ISystemParametersService _systemParams; - private readonly IProcessService _process; - - public MouseSettingsHandler(ISystemParametersService systemParams, IProcessService process) - { - this._systemParams = systemParams; - this._process = process; - } - /// public void Handle(string key, string value, JToken rawValue) { @@ -70,10 +72,6 @@ public void Handle(string key, string value, JToken rawValue) this.HandleEnhancePointerPrecision(param); break; - case "SetPrimaryMouseButton": - HandleSetPrimaryMouseButton(param); - break; - case "MouseCursorSpeed": this.HandleMouseCursorSpeed(param); break; @@ -81,6 +79,10 @@ public void Handle(string key, string value, JToken rawValue) case "MouseWheelScrollLines": this.HandleMouseWheelScrollLines(param); break; + + case "SetPrimaryMouseButton": + HandleSetPrimaryMouseButton(param); + break; } } catch (Exception ex) @@ -89,6 +91,15 @@ public void Handle(string key, string value, JToken rawValue) } } + private void HandleEnhancePointerPrecision(JObject param) + { + bool enable = param.Value("enable") ?? true; + int[] mouseParams = new int[3]; + this._systemParams.GetParameter(SPI_GETMOUSE, 0, mouseParams, 0); + mouseParams[2] = enable ? 1 : 0; + this._systemParams.SetParameter(SPI_SETMOUSE, 0, mouseParams, SPIF_UPDATEINIFILE_SENDCHANGE); + } + private void HandleMouseCursorSpeed(JObject param) { int speed = param.Value("speedLevel") ?? 10; @@ -101,15 +112,6 @@ private void HandleMouseWheelScrollLines(JObject param) this._systemParams.SetParameter(SPI_SETWHEELSCROLLLINES, lines, IntPtr.Zero, SPIF_UPDATEINIFILE_SENDCHANGE); } - private void HandleEnhancePointerPrecision(JObject param) - { - bool enable = param.Value("enable") ?? true; - int[] mouseParams = new int[3]; - this._systemParams.GetParameter(SPI_GETMOUSE, 0, mouseParams, 0); - mouseParams[2] = enable ? 1 : 0; - this._systemParams.SetParameter(SPI_SETMOUSE, 0, mouseParams, SPIF_UPDATEINIFILE_SENDCHANGE); - } - private static void HandleSetPrimaryMouseButton(JObject param) { string button = param.Value("primaryButton") ?? "left"; diff --git a/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs index 0cbef46319..c8acaaf473 100644 --- a/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs @@ -14,15 +14,6 @@ namespace autoShell.Handlers.Settings; /// internal class PersonalizationSettingsHandler : ICommandHandler { - /// - public IEnumerable SupportedCommands { get; } = - [ - "ApplyColorToTitleBar", - "EnableTransparency", - "HighContrastTheme", - "SystemThemeMode", - ]; - private readonly IRegistryService _registry; private readonly IProcessService _process; @@ -32,6 +23,15 @@ public PersonalizationSettingsHandler(IRegistryService registry, IProcessService this._process = process; } + /// + public IEnumerable SupportedCommands { get; } = + [ + "ApplyColorToTitleBar", + "EnableTransparency", + "HighContrastTheme", + "SystemThemeMode", + ]; + /// public void Handle(string key, string value, JToken rawValue) { diff --git a/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs index 84f6a8e773..b7b485f668 100644 --- a/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs @@ -14,14 +14,6 @@ namespace autoShell.Handlers.Settings; /// internal class PowerSettingsHandler : ICommandHandler { - /// - public IEnumerable SupportedCommands { get; } = - [ - "BatterySaverActivationLevel", - "SetPowerModeOnBattery", - "SetPowerModePluggedIn", - ]; - private readonly IRegistryService _registry; private readonly IProcessService _process; @@ -31,6 +23,14 @@ public PowerSettingsHandler(IRegistryService registry, IProcessService process) this._process = process; } + /// + public IEnumerable SupportedCommands { get; } = + [ + "BatterySaverActivationLevel", + "SetPowerModeOnBattery", + "SetPowerModePluggedIn", + ]; + /// public void Handle(string key, string value, JToken rawValue) { diff --git a/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs index 741f0b9760..75d9de26ff 100644 --- a/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs @@ -16,6 +16,13 @@ internal class PrivacySettingsHandler : ICommandHandler { private const string ConsentStoreBase = @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore"; + private readonly IRegistryService _registry; + + public PrivacySettingsHandler(IRegistryService registry) + { + this._registry = registry; + } + /// public IEnumerable SupportedCommands { get; } = [ @@ -24,13 +31,6 @@ internal class PrivacySettingsHandler : ICommandHandler "ManageMicrophoneAccess", ]; - private readonly IRegistryService _registry; - - public PrivacySettingsHandler(IRegistryService registry) - { - this._registry = registry; - } - /// public void Handle(string key, string value, JToken rawValue) { diff --git a/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs index d63f48ec54..a178c3cdb9 100644 --- a/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs @@ -15,6 +15,13 @@ namespace autoShell.Handlers.Settings; /// internal class SystemSettingsHandler : ICommandHandler { + private readonly IProcessService _process; + + public SystemSettingsHandler(IProcessService process) + { + this._process = process; + } + /// public IEnumerable SupportedCommands { get; } = [ @@ -26,13 +33,6 @@ internal class SystemSettingsHandler : ICommandHandler "RememberWindowLocations", ]; - private readonly IProcessService _process; - - public SystemSettingsHandler(IProcessService process) - { - this._process = process; - } - /// public void Handle(string key, string value, JToken rawValue) { diff --git a/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs index 379eccd622..a65deb1de3 100644 --- a/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Runtime.InteropServices; using autoShell.Services; using Microsoft.Win32; @@ -16,11 +15,20 @@ namespace autoShell.Handlers.Settings; /// internal partial class TaskbarSettingsHandler : ICommandHandler { + #region P/Invoke private const string ExplorerAdvanced = @"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"; private const string StuckRects3 = @"Software\Microsoft\Windows\CurrentVersion\Explorer\StuckRects3"; [LibraryImport("user32.dll")] private static partial IntPtr SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + #endregion P/Invoke + + private readonly IRegistryService _registry; + + public TaskbarSettingsHandler(IRegistryService registry) + { + this._registry = registry; + } /// public IEnumerable SupportedCommands { get; } = @@ -34,13 +42,6 @@ internal partial class TaskbarSettingsHandler : ICommandHandler "ToggleWidgetsButtonVisibility", ]; - private readonly IRegistryService _registry; - - public TaskbarSettingsHandler(IRegistryService registry) - { - this._registry = registry; - } - /// public void Handle(string key, string value, JToken rawValue) { @@ -53,6 +54,15 @@ public void Handle(string key, string value, JToken rawValue) case "AutoHideTaskbar": this.HandleAutoHideTaskbar(param); break; + case "DisplaySecondsInSystrayClock": + this.SetToggle(param, "enable", "ShowSecondsInSystemClock"); + break; + case "DisplayTaskbarOnAllMonitors": + this.SetToggle(param, "enable", "MMTaskbarEnabled"); + break; + case "ShowBadgesOnTaskbar": + this.SetToggle(param, "enableBadging", "TaskbarBadges"); + break; case "TaskbarAlignment": this.HandleTaskbarAlignment(param); break; @@ -62,15 +72,6 @@ public void Handle(string key, string value, JToken rawValue) case "ToggleWidgetsButtonVisibility": this.SetToggle(param, "visibility", "TaskbarDa", trueValue: "show"); break; - case "ShowBadgesOnTaskbar": - this.SetToggle(param, "enableBadging", "TaskbarBadges"); - break; - case "DisplayTaskbarOnAllMonitors": - this.SetToggle(param, "enable", "MMTaskbarEnabled"); - break; - case "DisplaySecondsInSystrayClock": - this.SetToggle(param, "enable", "ShowSecondsInSystemClock"); - break; } SendNotifyMessage((IntPtr)0xffff, 0x001A, IntPtr.Zero, IntPtr.Zero); @@ -81,27 +82,6 @@ public void Handle(string key, string value, JToken rawValue) } } - /// - /// Sets a DWord toggle in Explorer\Advanced. - /// For bool JSON values, true=1 false=0. - /// For string JSON values, compares against . - /// - private void SetToggle(JObject param, string jsonProperty, string registryValue, string trueValue = null) - { - int regValue; - if (trueValue != null) - { - string val = param.Value(jsonProperty) ?? ""; - regValue = val.Equals(trueValue, StringComparison.OrdinalIgnoreCase) ? 1 : 0; - } - else - { - regValue = (param.Value(jsonProperty) ?? true) ? 1 : 0; - } - - this._registry.SetValue(ExplorerAdvanced, registryValue, regValue, RegistryValueKind.DWord); - } - private void HandleAutoHideTaskbar(JObject param) { bool hide = param.Value("hideWhenNotUsing"); @@ -129,4 +109,25 @@ private void HandleTaskbarAlignment(JObject param) bool useCenter = alignment.Equals("center", StringComparison.OrdinalIgnoreCase); this._registry.SetValue(ExplorerAdvanced, "TaskbarAl", useCenter ? 1 : 0, RegistryValueKind.DWord); } + + /// + /// Sets a DWord toggle in Explorer\Advanced. + /// For bool JSON values, true=1 false=0. + /// For string JSON values, compares against . + /// + private void SetToggle(JObject param, string jsonProperty, string registryValue, string trueValue = null) + { + int regValue; + if (trueValue != null) + { + string val = param.Value(jsonProperty) ?? ""; + regValue = val.Equals(trueValue, StringComparison.OrdinalIgnoreCase) ? 1 : 0; + } + else + { + regValue = (param.Value(jsonProperty) ?? true) ? 1 : 0; + } + + this._registry.SetValue(ExplorerAdvanced, registryValue, regValue, RegistryValueKind.DWord); + } } diff --git a/dotnet/autoShell/Handlers/SystemCommandHandler.cs b/dotnet/autoShell/Handlers/SystemCommandHandler.cs index 3ae032db8b..3ae95ad093 100644 --- a/dotnet/autoShell/Handlers/SystemCommandHandler.cs +++ b/dotnet/autoShell/Handlers/SystemCommandHandler.cs @@ -13,13 +13,6 @@ namespace autoShell.Handlers; /// internal class SystemCommandHandler : ICommandHandler { - /// - public IEnumerable SupportedCommands { get; } = - [ - "Debug", - "ToggleNotifications", - ]; - private readonly IProcessService _process; public SystemCommandHandler(IProcessService process) @@ -27,18 +20,25 @@ public SystemCommandHandler(IProcessService process) this._process = process; } + /// + public IEnumerable SupportedCommands { get; } = + [ + "Debug", + "ToggleNotifications", + ]; + /// public void Handle(string key, string value, JToken rawValue) { switch (key) { - case "ToggleNotifications": - this._process.StartShellExecute("ms-actioncenter:"); - break; - case "Debug": Debugger.Launch(); break; + + case "ToggleNotifications": + this._process.StartShellExecute("ms-actioncenter:"); + break; } } } diff --git a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs index 1c7ce6969c..234a1fc1f2 100644 --- a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs +++ b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; -using System.Text; using autoShell.Services; using Microsoft.Win32; using Newtonsoft.Json; @@ -21,6 +19,8 @@ namespace autoShell.Handlers; /// internal partial class ThemeCommandHandler : ICommandHandler { + #region P/Invoke + private const int SPI_SETDESKWALLPAPER = 0x0014; private const int SPIF_UPDATEINIFILE_SENDCHANGE = 3; private const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002; @@ -38,21 +38,14 @@ internal partial class ThemeCommandHandler : ICommandHandler [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SendMessageTimeout( IntPtr hWnd, - uint Msg, + uint msg, IntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult); - /// - public IEnumerable SupportedCommands { get; } = - [ - "ApplyTheme", - "ListThemes", - "SetThemeMode", - "SetWallpaper", - ]; + #endregion P/Invoke private readonly IRegistryService _registry; private readonly IProcessService _process; @@ -71,15 +64,20 @@ public ThemeCommandHandler(IRegistryService registry, IProcessService process, I this.LoadThemes(); } + /// + public IEnumerable SupportedCommands { get; } = + [ + "ApplyTheme", + "ListThemes", + "SetThemeMode", + "SetWallpaper", + ]; + /// public void Handle(string key, string value, JToken rawValue) { switch (key) { - case "SetWallpaper": - this._systemParams.SetParameter(SPI_SETDESKWALLPAPER, 0, value, SPIF_UPDATEINIFILE_SENDCHANGE); - break; - case "ApplyTheme": this.ApplyTheme(value); break; @@ -92,151 +90,52 @@ public void Handle(string key, string value, JToken rawValue) case "SetThemeMode": this.HandleSetThemeMode(value); break; - } - } - private void HandleSetThemeMode(string value) - { - if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase)) - { - this.ToggleLightDarkMode(); - } - else if (value.Equals("light", StringComparison.OrdinalIgnoreCase)) - { - this.SetLightDarkMode(true); - } - else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase)) - { - this.SetLightDarkMode(false); - } - else if (bool.TryParse(value, out bool useLightMode)) - { - this.SetLightDarkMode(useLightMode); + case "SetWallpaper": + this._systemParams.SetParameter(SPI_SETDESKWALLPAPER, 0, value, SPIF_UPDATEINIFILE_SENDCHANGE); + break; } } #region Theme Management - private void LoadThemes() - { - this._themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - this._themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - - string[] themePaths = - [ - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Themes"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Ease of Access Themes"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Themes") - ]; - - foreach (string themesFolder in themePaths) - { - if (Directory.Exists(themesFolder)) - { - foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme")) - { - string themeName = Path.GetFileNameWithoutExtension(themeFile); - if (!this._themeDictionary.ContainsKey(themeName)) - { - this._themeDictionary[themeName] = themeFile; - - // Parse display name from theme file - string displayName = GetThemeDisplayName(themeFile); - if (!string.IsNullOrEmpty(displayName) && !this._themeDisplayNameDictionary.ContainsKey(displayName)) - { - this._themeDisplayNameDictionary[displayName] = themeName; - } - } - } - } - } - - this._themeDictionary["previous"] = this.GetCurrentTheme(); - } - /// - /// Parses the display name from a .theme file. + /// Applies a Windows theme by name. /// - private static string GetThemeDisplayName(string themeFilePath) + public bool ApplyTheme(string themeName) { - try - { - foreach (string line in File.ReadLines(themeFilePath)) - { - if (line.StartsWith("DisplayName=", StringComparison.OrdinalIgnoreCase)) - { - string displayName = line["DisplayName=".Length..].Trim(); - // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013) - if (displayName.StartsWith('@')) - { - displayName = ResolveLocalizedString(displayName); - } - return displayName; - } - } - } - catch + string themePath = FindThemePath(themeName); + if (string.IsNullOrEmpty(themePath)) { - // Ignore errors reading theme file + return false; } - return null; - } - /// - /// Resolves a localized string resource reference. - /// - private static string ResolveLocalizedString(string localizedString) - { try { - // Remove the @ prefix - string resourcePath = localizedString[1..]; - // Expand environment variables - int commaIndex = resourcePath.LastIndexOf(','); - if (commaIndex > 0) + string previous = GetCurrentTheme(); + + if (!themeName.Equals("previous", StringComparison.OrdinalIgnoreCase)) { - string dllPath = Environment.ExpandEnvironmentVariables(resourcePath[..commaIndex]); - string resourceIdStr = resourcePath[(commaIndex + 1)..]; - if (int.TryParse(resourceIdStr, out int resourceId)) + this._process.StartShellExecute(themePath); + this._previousTheme = previous; + return true; + } + else + { + bool success = RevertToPreviousTheme(); + + if (success) { - char[] buffer = new char[256]; - IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); - if (hModule != IntPtr.Zero) - { - try - { - int result = LoadString(hModule, (uint)Math.Abs(resourceId), buffer, buffer.Length); - if (result > 0) - { - return new string(buffer, 0, result); - } - } - finally - { - FreeLibrary(hModule); - } - } + this._previousTheme = previous; } + + return success; } } catch { - // Ignore errors resolving localized string + return false; } - return localizedString; - } - - /// - /// Returns a list of all installed Windows themes. - /// - public List GetInstalledThemes() - { - HashSet themes = []; - - themes.UnionWith(this._themeDictionary.Keys); - themes.UnionWith(this._themeDisplayNameDictionary.Keys); - - return [.. themes]; } /// @@ -261,42 +160,24 @@ public string GetCurrentTheme() } /// - /// Applies a Windows theme by name. + /// Returns a list of all installed Windows themes. /// - public bool ApplyTheme(string themeName) + public List GetInstalledThemes() { - string themePath = FindThemePath(themeName); - if (string.IsNullOrEmpty(themePath)) - { - return false; - } - - try - { - string previous = GetCurrentTheme(); + HashSet themes = []; - if (!themeName.Equals("previous", StringComparison.OrdinalIgnoreCase)) - { - this._process.StartShellExecute(themePath); - this._previousTheme = previous; - return true; - } - else - { - bool success = RevertToPreviousTheme(); + themes.UnionWith(this._themeDictionary.Keys); + themes.UnionWith(this._themeDisplayNameDictionary.Keys); - if (success) - { - this._previousTheme = previous; - } + return [.. themes]; + } - return success; - } - } - catch - { - return false; - } + /// + /// Gets the name of the previous theme. + /// + public string GetPreviousTheme() + { + return this._previousTheme; } /// @@ -326,37 +207,6 @@ public bool RevertToPreviousTheme() } } - /// - /// Gets the name of the previous theme. - /// - public string GetPreviousTheme() - { - return this._previousTheme; - } - - /// - /// Finds the full path to a theme file by name or display name. - /// - private string FindThemePath(string themeName) - { - // First check by file name - if (this._themeDictionary.TryGetValue(themeName, out string themePath)) - { - return themePath; - } - - // Then check by display name - if (this._themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) - { - if (this._themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) - { - return themePathFromDisplay; - } - } - - return null; - } - #endregion #region Light/Dark Mode @@ -396,6 +246,28 @@ public bool ToggleLightDarkMode() return currentMode.HasValue && this.SetLightDarkMode(!currentMode.Value); } + #endregion + + private void HandleSetThemeMode(string value) + { + if (value.Equals("toggle", StringComparison.OrdinalIgnoreCase)) + { + this.ToggleLightDarkMode(); + } + else if (value.Equals("light", StringComparison.OrdinalIgnoreCase)) + { + this.SetLightDarkMode(true); + } + else if (value.Equals("dark", StringComparison.OrdinalIgnoreCase)) + { + this.SetLightDarkMode(false); + } + else if (bool.TryParse(value, out bool useLightMode)) + { + this.SetLightDarkMode(useLightMode); + } + } + /// /// Broadcasts a WM_SETTINGCHANGE message to notify the system of theme changes. /// @@ -414,6 +286,29 @@ private static void BroadcastSettingsChange() out nint result); } + /// + /// Finds the full path to a theme file by name or display name. + /// + private string FindThemePath(string themeName) + { + // First check by file name + if (this._themeDictionary.TryGetValue(themeName, out string themePath)) + { + return themePath; + } + + // Then check by display name + if (this._themeDisplayNameDictionary.TryGetValue(themeName, out string fileNameFromDisplay)) + { + if (this._themeDictionary.TryGetValue(fileNameFromDisplay, out string themePathFromDisplay)) + { + return themePathFromDisplay; + } + } + + return null; + } + /// /// Gets the current light/dark mode setting from the registry. /// @@ -432,5 +327,112 @@ private static void BroadcastSettingsChange() } } - #endregion + /// + /// Parses the display name from a .theme file. + /// + private static string GetThemeDisplayName(string themeFilePath) + { + try + { + foreach (string line in File.ReadLines(themeFilePath)) + { + if (line.StartsWith("DisplayName=", StringComparison.OrdinalIgnoreCase)) + { + string displayName = line["DisplayName=".Length..].Trim(); + // Handle localized strings (e.g., @%SystemRoot%\System32\themeui.dll,-2013) + if (displayName.StartsWith('@')) + { + displayName = ResolveLocalizedString(displayName); + } + return displayName; + } + } + } + catch + { + // Ignore errors reading theme file + } + return null; + } + + private void LoadThemes() + { + this._themeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + this._themeDisplayNameDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string[] themePaths = + [ + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Themes"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Resources", "Ease of Access Themes"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Themes") + ]; + + foreach (string themesFolder in themePaths) + { + if (Directory.Exists(themesFolder)) + { + foreach (string themeFile in Directory.GetFiles(themesFolder, "*.theme")) + { + string themeName = Path.GetFileNameWithoutExtension(themeFile); + if (!this._themeDictionary.ContainsKey(themeName)) + { + this._themeDictionary[themeName] = themeFile; + + // Parse display name from theme file + string displayName = GetThemeDisplayName(themeFile); + if (!string.IsNullOrEmpty(displayName) && !this._themeDisplayNameDictionary.ContainsKey(displayName)) + { + this._themeDisplayNameDictionary[displayName] = themeName; + } + } + } + } + } + + this._themeDictionary["previous"] = this.GetCurrentTheme(); + } + + /// + /// Resolves a localized string resource reference. + /// + private static string ResolveLocalizedString(string localizedString) + { + try + { + // Remove the @ prefix + string resourcePath = localizedString[1..]; + // Expand environment variables + int commaIndex = resourcePath.LastIndexOf(','); + if (commaIndex > 0) + { + string dllPath = Environment.ExpandEnvironmentVariables(resourcePath[..commaIndex]); + string resourceIdStr = resourcePath[(commaIndex + 1)..]; + if (int.TryParse(resourceIdStr, out int resourceId)) + { + char[] buffer = new char[256]; + IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); + if (hModule != IntPtr.Zero) + { + try + { + int result = LoadString(hModule, (uint)Math.Abs(resourceId), buffer, buffer.Length); + if (result > 0) + { + return new string(buffer, 0, result); + } + } + finally + { + FreeLibrary(hModule); + } + } + } + } + } + catch + { + // Ignore errors resolving localized string + } + return localizedString; + } } diff --git a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs index cb9c27570d..c52c345f8a 100644 --- a/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs +++ b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs @@ -18,6 +18,239 @@ namespace autoShell.Handlers; /// internal class VirtualDesktopCommandHandler : ICommandHandler { + #region Virtual Desktop COM Interfaces + + private enum APPLICATION_VIEW_CLOAK_TYPE : int + { + AVCT_NONE = 0, + AVCT_DEFAULT = 1, + AVCT_VIRTUAL_DESKTOP = 2 + } + + private enum APPLICATION_VIEW_COMPATIBILITY_POLICY : int + { + AVCP_NONE = 0, + AVCP_SMALL_SCREEN = 1, + AVCP_TABLET_SMALL_SCREEN = 2, + AVCP_VERY_SMALL_SCREEN = 3, + AVCP_HIGH_SCALE_FACTOR = 4 + } + + // Virtual Desktop COM Interface GUIDs + private static readonly Guid s_clsidImmersiveShell = new Guid("C2F03A33-21F5-47FA-B4BB-156362A2F239"); + private static readonly Guid s_clsidVirtualDesktopManagerInternal = new Guid("C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B"); + private static readonly Guid s_clsidVirtualDesktopManager = new Guid("AA509086-5CA9-4C25-8F95-589D3C07B48A"); + private static readonly Guid s_clsidVirtualDesktopPinnedApps = new Guid("B5A399E7-1C87-46B8-88E9-FC5747B171BD"); + + // IServiceProvider COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] + private interface IServiceProvider + { + [return: MarshalAs(UnmanagedType.IUnknown)] + void QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject); + } + + // IVirtualDesktopManager COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("A5CD92FF-29BE-454C-8D04-D82879FB3F1B")] + private interface IVirtualDesktopManager + { + bool IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow); + Guid GetWindowDesktopId(IntPtr topLevelWindow); + void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId); + } + + // IVirtualDesktop COM Interface (Windows 10/11) + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] + private interface IVirtualDesktop + { + bool IsViewVisible(IApplicationView view); + Guid GetId(); + // TODO: proper HSTRING custom marshaling + [return: MarshalAs(UnmanagedType.HString)] + string GetName(); + [return: MarshalAs(UnmanagedType.HString)] + string GetWallpaperPath(); + bool IsRemote(); + } + + // IVirtualDesktopManagerInternal COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("53F5CA0B-158F-4124-900C-057158060B27")] + private interface IVirtualDesktopManagerInternal_BUGBUG + { + int GetCount(); + void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); + bool CanViewMoveDesktops(IApplicationView view); + IVirtualDesktop GetCurrentDesktop(); + void GetDesktops(out IObjectArray desktops); + [PreserveSig] + int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); + void SwitchDesktop(IVirtualDesktop desktop); + IVirtualDesktop CreateDesktop(); + void MoveDesktop(IVirtualDesktop desktop, int nIndex); + void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); + IVirtualDesktop FindDesktop(ref Guid desktopid); + void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); + void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); + void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); + void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); + void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); + void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); + void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); + void SwitchDesktopWithAnimation(IVirtualDesktop desktop); + void GetLastActiveDesktop(out IVirtualDesktop desktop); + void WaitForAnimationToComplete(); + } + + // IVirtualDesktopManagerInternal COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("53F5CA0B-158F-4124-900C-057158060B27")] + private interface IVirtualDesktopManagerInternal + { + int GetCount(); + void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); + bool CanViewMoveDesktops(IApplicationView view); + IVirtualDesktop GetCurrentDesktop(); + void GetDesktops(out IObjectArray desktops); + [PreserveSig] + int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); + void SwitchDesktop(IVirtualDesktop desktop); + void SwitchDesktopAndMoveForegroundView(IVirtualDesktop desktop); + IVirtualDesktop CreateDesktop(); + void MoveDesktop(IVirtualDesktop desktop, int nIndex); + void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); + IVirtualDesktop FindDesktop(ref Guid desktopid); + void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); + void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); + void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); + void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); + void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); + void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); + void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); + void SwitchDesktopWithAnimation(IVirtualDesktop desktop); + void GetLastActiveDesktop(out IVirtualDesktop desktop); + void WaitForAnimationToComplete(); + } + + // IObjectArray COM Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("92CA9DCD-5622-4BBA-A805-5E9F541BD8C9")] + private interface IObjectArray + { + void GetCount(out int pcObjects); + void GetAt(int uiIndex, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("372E1D3B-38D3-42E4-A15B-8AB2B178F513")] + private interface IApplicationView + { + int SetFocus(); + int SwitchTo(); + int TryInvokeBack(IntPtr /* IAsyncCallback* */ callback); + int GetThumbnailWindow(out IntPtr hwnd); + int GetMonitor(out IntPtr /* IImmersiveMonitor */ immersiveMonitor); + int GetVisibility(out int visibility); + int SetCloak(APPLICATION_VIEW_CLOAK_TYPE cloakType, int unknown); + int GetPosition(ref Guid guid /* GUID for IApplicationViewPosition */, out IntPtr /* IApplicationViewPosition** */ position); + int SetPosition(ref IntPtr /* IApplicationViewPosition* */ position); + int InsertAfterWindow(IntPtr hwnd); + int GetExtendedFramePosition(out Rect rect); + int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string id); + int SetAppUserModelId(string id); + int IsEqualByAppUserModelId(string id, out int result); + int GetViewState(out uint state); + int SetViewState(uint state); + int GetNeediness(out int neediness); + int GetLastActivationTimestamp(out ulong timestamp); + int SetLastActivationTimestamp(ulong timestamp); + int GetVirtualDesktopId(out Guid guid); + int SetVirtualDesktopId(ref Guid guid); + int GetShowInSwitchers(out int flag); + int SetShowInSwitchers(int flag); + int GetScaleFactor(out int factor); + int CanReceiveInput(out bool canReceiveInput); + int GetCompatibilityPolicyType(out APPLICATION_VIEW_COMPATIBILITY_POLICY flags); + int SetCompatibilityPolicyType(APPLICATION_VIEW_COMPATIBILITY_POLICY flags); + int GetSizeConstraints(IntPtr /* IImmersiveMonitor* */ monitor, out Size size1, out Size size2); + int GetSizeConstraintsForDpi(uint uint1, out Size size1, out Size size2); + int SetSizeConstraintsForDpi(ref uint uint1, ref Size size1, ref Size size2); + int OnMinSizePreferencesUpdated(IntPtr hwnd); + int ApplyOperation(IntPtr /* IApplicationViewOperation* */ operation); + int IsTray(out bool isTray); + int IsInHighZOrderBand(out bool isInHighZOrderBand); + int IsSplashScreenPresented(out bool isSplashScreenPresented); + int Flash(); + int GetRootSwitchableOwner(out IApplicationView rootSwitchableOwner); + int EnumerateOwnershipTree(out IObjectArray ownershipTree); + int GetEnterpriseId([MarshalAs(UnmanagedType.LPWStr)] out string enterpriseId); + int IsMirrored(out bool isMirrored); + int Unknown1(out int unknown); + int Unknown2(out int unknown); + int Unknown3(out int unknown); + int Unknown4(out int unknown); + int Unknown5(out int unknown); + int Unknown6(int unknown); + int Unknown7(); + int Unknown8(out int unknown); + int Unknown9(int unknown); + int Unknown10(int unknownX, int unknownY); + int Unknown11(int unknown); + int Unknown12(out Size size1); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("1841C6D7-4F9D-42C0-AF41-8747538F10E5")] + private interface IApplicationViewCollection + { + int GetViews(out IObjectArray array); + int GetViewsByZOrder(out IObjectArray array); + int GetViewsByAppUserModelId(string id, out IObjectArray array); + int GetViewForHwnd(IntPtr hwnd, out IApplicationView view); + int GetViewForApplication(object application, out IApplicationView view); + int GetViewForAppUserModelId(string id, out IApplicationView view); + int GetViewInFocus(out IntPtr view); + int Unknown1(out IntPtr view); + void RefreshCollection(); + int RegisterForApplicationViewChanges(object listener, out int cookie); + int UnregisterForApplicationViewChanges(int cookie); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("4CE81583-1E4C-4632-A621-07A53543148F")] + private interface IVirtualDesktopPinnedApps + { + bool IsAppIdPinned(string appId); + void PinAppID(string appId); + void UnpinAppID(string appId); + bool IsViewPinned(IApplicationView applicationView); + void PinView(IApplicationView applicationView); + void UnpinView(IApplicationView applicationView); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] + private interface IServiceProvider10 + { + [return: MarshalAs(UnmanagedType.IUnknown)] + object QueryService(ref Guid service, ref Guid riid); + } + + #endregion + private readonly IAppRegistry _appRegistry; private readonly IServiceProvider10 _shell; private readonly IVirtualDesktopManagerInternal _virtualDesktopManagerInternal; @@ -59,24 +292,24 @@ public void Handle(string key, string value, JToken rawValue) this.CreateDesktop(value); break; - case "SwitchDesktop": - this.SwitchDesktop(value); + case "MoveWindowToDesktop": + this.MoveWindowToDesktop(rawValue); break; case "NextDesktop": this.BumpDesktopIndex(1); break; - case "PreviousDesktop": - this.BumpDesktopIndex(-1); + case "PinWindow": + this.PinWindow(value); break; - case "MoveWindowToDesktop": - this.MoveWindowToDesktop(rawValue); + case "PreviousDesktop": + this.BumpDesktopIndex(-1); break; - case "PinWindow": - this.PinWindow(value); + case "SwitchDesktop": + this.SwitchDesktop(value); break; } } @@ -148,7 +381,73 @@ private void CreateDesktop(string jsonValue) } } - private void SwitchDesktop(string desktopIdentifier) + /// + /// + /// + /// + /// Currently not working correction, returns ACCESS_DENIED // TODO: investigate + private void MoveWindowToDesktop(JToken value) + { + string process = value.SelectToken("process").ToString(); + string desktop = value.SelectToken("desktop").ToString(); + if (string.IsNullOrEmpty(process)) + { + Debug.WriteLine("No process name supplied"); + return; + } + + if (string.IsNullOrEmpty(desktop)) + { + Debug.WriteLine("No desktop id supplied"); + return; + } + + IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(process, this._appRegistry); + + if (int.TryParse(desktop, out int desktopIndex)) + { + this._virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + if (desktopIndex < 1 || desktopIndex > this._virtualDesktopManagerInternal.GetCount()) + { + Debug.WriteLine("Desktop index out of range"); + Marshal.ReleaseComObject(desktops); + return; + } + desktops.GetAt(desktopIndex - 1, typeof(IVirtualDesktop).GUID, out object od); + Guid g = ((IVirtualDesktop)od).GetId(); + this._virtualDesktopManager.MoveWindowToDesktop(hWnd, ref g); + Marshal.ReleaseComObject(desktops); + return; + } + + IVirtualDesktop ivd = FindDesktopByName(desktop); + if (ivd is not null) + { + Guid desktopGuid = ivd.GetId(); + this._virtualDesktopManager.MoveWindowToDesktop(hWnd, ref desktopGuid); + } + } + + private void PinWindow(string processName) + { + IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(processName, this._appRegistry); + + if (hWnd != IntPtr.Zero) + { + this._applicationViewCollection.GetViewForHwnd(hWnd, out IApplicationView view); + + if (view is not null) + { + this._virtualDesktopPinnedApps.PinView((IApplicationView)view); + } + } + else + { + Console.WriteLine($"The window handle for '{processName}' could not be found"); + } + } + + private void SwitchDesktop(string desktopIdentifier) { if (!int.TryParse(desktopIdentifier, out int index)) { @@ -256,72 +555,6 @@ private int GetDesktopIndex(IVirtualDesktop desktop) return -1; } - /// - /// - /// - /// - /// Currently not working correction, returns ACCESS_DENIED // TODO: investigate - private void MoveWindowToDesktop(JToken value) - { - string process = value.SelectToken("process").ToString(); - string desktop = value.SelectToken("desktop").ToString(); - if (string.IsNullOrEmpty(process)) - { - Debug.WriteLine("No process name supplied"); - return; - } - - if (string.IsNullOrEmpty(desktop)) - { - Debug.WriteLine("No desktop id supplied"); - return; - } - - IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(process, this._appRegistry); - - if (int.TryParse(desktop, out int desktopIndex)) - { - this._virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); - if (desktopIndex < 1 || desktopIndex > this._virtualDesktopManagerInternal.GetCount()) - { - Debug.WriteLine("Desktop index out of range"); - Marshal.ReleaseComObject(desktops); - return; - } - desktops.GetAt(desktopIndex - 1, typeof(IVirtualDesktop).GUID, out object od); - Guid g = ((IVirtualDesktop)od).GetId(); - this._virtualDesktopManager.MoveWindowToDesktop(hWnd, ref g); - Marshal.ReleaseComObject(desktops); - return; - } - - IVirtualDesktop ivd = FindDesktopByName(desktop); - if (ivd is not null) - { - Guid desktopGuid = ivd.GetId(); - this._virtualDesktopManager.MoveWindowToDesktop(hWnd, ref desktopGuid); - } - } - - private void PinWindow(string processName) - { - IntPtr hWnd = WindowCommandHandler.FindProcessWindowHandle(processName, this._appRegistry); - - if (hWnd != IntPtr.Zero) - { - this._applicationViewCollection.GetViewForHwnd(hWnd, out IApplicationView view); - - if (view is not null) - { - this._virtualDesktopPinnedApps.PinView((IApplicationView)view); - } - } - else - { - Console.WriteLine($"The window handle for '{processName}' could not be found"); - } - } - private IVirtualDesktopManagerInternal GetVirtualDesktopManagerInternal() { try @@ -345,237 +578,4 @@ private IVirtualDesktopManagerInternal GetVirtualDesktopManagerInternal() } #endregion - - #region Virtual Desktop COM Interfaces - - private enum APPLICATION_VIEW_CLOAK_TYPE : int - { - AVCT_NONE = 0, - AVCT_DEFAULT = 1, - AVCT_VIRTUAL_DESKTOP = 2 - } - - private enum APPLICATION_VIEW_COMPATIBILITY_POLICY : int - { - AVCP_NONE = 0, - AVCP_SMALL_SCREEN = 1, - AVCP_TABLET_SMALL_SCREEN = 2, - AVCP_VERY_SMALL_SCREEN = 3, - AVCP_HIGH_SCALE_FACTOR = 4 - } - - // Virtual Desktop COM Interface GUIDs - private static readonly Guid s_clsidImmersiveShell = new Guid("C2F03A33-21F5-47FA-B4BB-156362A2F239"); - private static readonly Guid s_clsidVirtualDesktopManagerInternal = new Guid("C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B"); - private static readonly Guid s_clsidVirtualDesktopManager = new Guid("AA509086-5CA9-4C25-8F95-589D3C07B48A"); - private static readonly Guid s_clsidVirtualDesktopPinnedApps = new Guid("B5A399E7-1C87-46B8-88E9-FC5747B171BD"); - - // IServiceProvider COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] - private interface IServiceProvider - { - [return: MarshalAs(UnmanagedType.IUnknown)] - void QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject); - } - - // IVirtualDesktopManager COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("A5CD92FF-29BE-454C-8D04-D82879FB3F1B")] - private interface IVirtualDesktopManager - { - bool IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow); - Guid GetWindowDesktopId(IntPtr topLevelWindow); - void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId); - } - - // IVirtualDesktop COM Interface (Windows 10/11) - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] - private interface IVirtualDesktop - { - bool IsViewVisible(IApplicationView view); - Guid GetId(); - // TODO: proper HSTRING custom marshaling - [return: MarshalAs(UnmanagedType.HString)] - string GetName(); - [return: MarshalAs(UnmanagedType.HString)] - string GetWallpaperPath(); - bool IsRemote(); - } - - // IVirtualDesktopManagerInternal COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("53F5CA0B-158F-4124-900C-057158060B27")] - private interface IVirtualDesktopManagerInternal_BUGBUG - { - int GetCount(); - void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); - bool CanViewMoveDesktops(IApplicationView view); - IVirtualDesktop GetCurrentDesktop(); - void GetDesktops(out IObjectArray desktops); - [PreserveSig] - int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); - void SwitchDesktop(IVirtualDesktop desktop); - IVirtualDesktop CreateDesktop(); - void MoveDesktop(IVirtualDesktop desktop, int nIndex); - void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); - IVirtualDesktop FindDesktop(ref Guid desktopid); - void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); - void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); - void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); - void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); - void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); - void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); - void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); - void SwitchDesktopWithAnimation(IVirtualDesktop desktop); - void GetLastActiveDesktop(out IVirtualDesktop desktop); - void WaitForAnimationToComplete(); - } - - // IVirtualDesktopManagerInternal COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("53F5CA0B-158F-4124-900C-057158060B27")] - private interface IVirtualDesktopManagerInternal - { - int GetCount(); - void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); - bool CanViewMoveDesktops(IApplicationView view); - IVirtualDesktop GetCurrentDesktop(); - void GetDesktops(out IObjectArray desktops); - [PreserveSig] - int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); - void SwitchDesktop(IVirtualDesktop desktop); - void SwitchDesktopAndMoveForegroundView(IVirtualDesktop desktop); - IVirtualDesktop CreateDesktop(); - void MoveDesktop(IVirtualDesktop desktop, int nIndex); - void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); - IVirtualDesktop FindDesktop(ref Guid desktopid); - void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); - void SetDesktopName(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string name); - void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); - void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); - void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); - void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); - void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); - void SwitchDesktopWithAnimation(IVirtualDesktop desktop); - void GetLastActiveDesktop(out IVirtualDesktop desktop); - void WaitForAnimationToComplete(); - } - - // IObjectArray COM Interface - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("92CA9DCD-5622-4BBA-A805-5E9F541BD8C9")] - private interface IObjectArray - { - void GetCount(out int pcObjects); - void GetAt(int uiIndex, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("372E1D3B-38D3-42E4-A15B-8AB2B178F513")] - private interface IApplicationView - { - int SetFocus(); - int SwitchTo(); - int TryInvokeBack(IntPtr /* IAsyncCallback* */ callback); - int GetThumbnailWindow(out IntPtr hwnd); - int GetMonitor(out IntPtr /* IImmersiveMonitor */ immersiveMonitor); - int GetVisibility(out int visibility); - int SetCloak(APPLICATION_VIEW_CLOAK_TYPE cloakType, int unknown); - int GetPosition(ref Guid guid /* GUID for IApplicationViewPosition */, out IntPtr /* IApplicationViewPosition** */ position); - int SetPosition(ref IntPtr /* IApplicationViewPosition* */ position); - int InsertAfterWindow(IntPtr hwnd); - int GetExtendedFramePosition(out Rect rect); - int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string id); - int SetAppUserModelId(string id); - int IsEqualByAppUserModelId(string id, out int result); - int GetViewState(out uint state); - int SetViewState(uint state); - int GetNeediness(out int neediness); - int GetLastActivationTimestamp(out ulong timestamp); - int SetLastActivationTimestamp(ulong timestamp); - int GetVirtualDesktopId(out Guid guid); - int SetVirtualDesktopId(ref Guid guid); - int GetShowInSwitchers(out int flag); - int SetShowInSwitchers(int flag); - int GetScaleFactor(out int factor); - int CanReceiveInput(out bool canReceiveInput); - int GetCompatibilityPolicyType(out APPLICATION_VIEW_COMPATIBILITY_POLICY flags); - int SetCompatibilityPolicyType(APPLICATION_VIEW_COMPATIBILITY_POLICY flags); - int GetSizeConstraints(IntPtr /* IImmersiveMonitor* */ monitor, out Size size1, out Size size2); - int GetSizeConstraintsForDpi(uint uint1, out Size size1, out Size size2); - int SetSizeConstraintsForDpi(ref uint uint1, ref Size size1, ref Size size2); - int OnMinSizePreferencesUpdated(IntPtr hwnd); - int ApplyOperation(IntPtr /* IApplicationViewOperation* */ operation); - int IsTray(out bool isTray); - int IsInHighZOrderBand(out bool isInHighZOrderBand); - int IsSplashScreenPresented(out bool isSplashScreenPresented); - int Flash(); - int GetRootSwitchableOwner(out IApplicationView rootSwitchableOwner); - int EnumerateOwnershipTree(out IObjectArray ownershipTree); - int GetEnterpriseId([MarshalAs(UnmanagedType.LPWStr)] out string enterpriseId); - int IsMirrored(out bool isMirrored); - int Unknown1(out int unknown); - int Unknown2(out int unknown); - int Unknown3(out int unknown); - int Unknown4(out int unknown); - int Unknown5(out int unknown); - int Unknown6(int unknown); - int Unknown7(); - int Unknown8(out int unknown); - int Unknown9(int unknown); - int Unknown10(int unknownX, int unknownY); - int Unknown11(int unknown); - int Unknown12(out Size size1); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("1841C6D7-4F9D-42C0-AF41-8747538F10E5")] - private interface IApplicationViewCollection - { - int GetViews(out IObjectArray array); - int GetViewsByZOrder(out IObjectArray array); - int GetViewsByAppUserModelId(string id, out IObjectArray array); - int GetViewForHwnd(IntPtr hwnd, out IApplicationView view); - int GetViewForApplication(object application, out IApplicationView view); - int GetViewForAppUserModelId(string id, out IApplicationView view); - int GetViewInFocus(out IntPtr view); - int Unknown1(out IntPtr view); - void RefreshCollection(); - int RegisterForApplicationViewChanges(object listener, out int cookie); - int UnregisterForApplicationViewChanges(int cookie); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("4CE81583-1E4C-4632-A621-07A53543148F")] - private interface IVirtualDesktopPinnedApps - { - bool IsAppIdPinned(string appId); - void PinAppID(string appId); - void UnpinAppID(string appId); - bool IsViewPinned(IApplicationView applicationView); - void PinView(IApplicationView applicationView); - void UnpinView(IApplicationView applicationView); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] - private interface IServiceProvider10 - { - [return: MarshalAs(UnmanagedType.IUnknown)] - object QueryService(ref Guid service, ref Guid riid); - } - - #endregion -} +} \ No newline at end of file diff --git a/dotnet/autoShell/Handlers/WindowCommandHandler.cs b/dotnet/autoShell/Handlers/WindowCommandHandler.cs index b8680fec2d..7872fc4ead 100644 --- a/dotnet/autoShell/Handlers/WindowCommandHandler.cs +++ b/dotnet/autoShell/Handlers/WindowCommandHandler.cs @@ -17,6 +17,57 @@ namespace autoShell.Handlers; /// internal class WindowCommandHandler : ICommandHandler { + #region P/Invoke + + private const uint WM_SYSCOMMAND = 0x112; + private const uint SC_MAXIMIZE = 0xF030; + private const uint SC_MINIMIZE = 0xF020; + + internal struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hWnd, ref RECT rect); + + [DllImport("user32.dll")] + private static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll", EntryPoint = "SendMessage", SetLastError = true)] + private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, uint wParam, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow); + + [DllImport("user32.dll")] + internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName); + + internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + #endregion + private readonly IAppRegistry _appRegistry; public WindowCommandHandler(IAppRegistry appRegistry) @@ -60,39 +111,31 @@ public void Handle(string key, string value, JToken rawValue) } } - internal static void RaiseWindow(string friendlyName, IAppRegistry appRegistry) + private void MaximizeWindow(string friendlyName) { - string processName = appRegistry.ResolveProcessName(friendlyName); + string processName = this._appRegistry.ResolveProcessName(friendlyName); Process[] processes = Process.GetProcessesByName(processName); foreach (Process p in processes) { if (p.MainWindowHandle != IntPtr.Zero) { + SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero); SetForegroundWindow(p.MainWindowHandle); Interaction.AppActivate(p.Id); return; } } - // All processes are background-only (e.g. Edge, Chrome). Try launching by path. - string path = appRegistry.GetExecutablePath(friendlyName); - if (path != null) - { - Process.Start(path); - } - else + (nint hWnd, int pid) = FindWindowByTitle(processName); + if (hWnd != nint.Zero) { - // Try to find by window title - (nint hWnd1, int pid) = FindWindowByTitle(processName); - if (hWnd1 != nint.Zero) - { - SetForegroundWindow(hWnd1); - Interaction.AppActivate(pid); - } + SendMessage(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero); + SetForegroundWindow(hWnd); + Interaction.AppActivate(pid); } } - private void MaximizeWindow(string friendlyName) + private void MinimizeWindow(string friendlyName) { string processName = this._appRegistry.ResolveProcessName(friendlyName); Process[] processes = Process.GetProcessesByName(processName); @@ -100,41 +143,49 @@ private void MaximizeWindow(string friendlyName) { if (p.MainWindowHandle != IntPtr.Zero) { - SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero); - SetForegroundWindow(p.MainWindowHandle); - Interaction.AppActivate(p.Id); - return; + SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero); + break; } } (nint hWnd, int pid) = FindWindowByTitle(processName); if (hWnd != nint.Zero) { - SendMessage(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, IntPtr.Zero); + SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero); SetForegroundWindow(hWnd); Interaction.AppActivate(pid); } } - private void MinimizeWindow(string friendlyName) + internal static void RaiseWindow(string friendlyName, IAppRegistry appRegistry) { - string processName = this._appRegistry.ResolveProcessName(friendlyName); + string processName = appRegistry.ResolveProcessName(friendlyName); Process[] processes = Process.GetProcessesByName(processName); foreach (Process p in processes) { if (p.MainWindowHandle != IntPtr.Zero) { - SendMessage(p.MainWindowHandle, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero); - break; + SetForegroundWindow(p.MainWindowHandle); + Interaction.AppActivate(p.Id); + return; } } - (nint hWnd, int pid) = FindWindowByTitle(processName); - if (hWnd != nint.Zero) + // All processes are background-only (e.g. Edge, Chrome). Try launching by path. + string path = appRegistry.GetExecutablePath(friendlyName); + if (path != null) { - SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, IntPtr.Zero); - SetForegroundWindow(hWnd); - Interaction.AppActivate(pid); + Process.Start(path); + } + else + { + // Try to find by window title + (nint hWnd1, int pid) = FindWindowByTitle(processName); + if (hWnd1 != nint.Zero) + { + SetForegroundWindow(hWnd1); + Interaction.AppActivate(pid); + } } } @@ -288,55 +339,4 @@ internal static IntPtr FindProcessWindowHandle(string processName, IAppRegistry } #endregion - - #region P/Invoke - - private const uint WM_SYSCOMMAND = 0x112; - private const uint SC_MAXIMIZE = 0xF030; - private const uint SC_MINIMIZE = 0xF020; - - internal struct RECT - { - public int Left; - public int Top; - public int Right; - public int Bottom; - } - - [DllImport("user32.dll")] - private static extern bool GetWindowRect(IntPtr hWnd, ref RECT rect); - - [DllImport("user32.dll")] - private static extern IntPtr GetDesktopWindow(); - - [DllImport("user32.dll")] - private static extern bool SetForegroundWindow(IntPtr hWnd); - - [DllImport("user32.dll", EntryPoint = "SendMessage", SetLastError = true)] - private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, uint wParam, IntPtr lParam); - - [DllImport("user32.dll")] - private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); - - [DllImport("user32.dll")] - private static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow); - - [DllImport("user32.dll")] - internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName); - - internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - - [DllImport("user32.dll")] - private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); - - [DllImport("user32.dll")] - internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - private static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - #endregion -} +} \ No newline at end of file diff --git a/dotnet/autoShell/UIAutomation.cs b/dotnet/autoShell/UIAutomation.cs index fa9fbfab8a..6806ddc33e 100644 --- a/dotnet/autoShell/UIAutomation.cs +++ b/dotnet/autoShell/UIAutomation.cs @@ -2,12 +2,9 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; using UIAutomationClient = Interop.UIAutomationClient; namespace autoShell; @@ -19,6 +16,27 @@ namespace autoShell; [Obsolete("UIAutomation is a last-resort method and should be avoided in production code.")] internal sealed class UIAutomation { + #region P/Invoke + + // Mouse event constants for simulated clicks + private const uint MOUSEEVENTF_LEFTDOWN = 0x0002; + private const uint MOUSEEVENTF_LEFTUP = 0x0004; + + // Keyboard event constants + private const uint KEYEVENTF_KEYUP = 0x0002; + private const byte VK_DELETE = 0x2E; + + [DllImport("user32.dll")] + private static extern bool SetCursorPos(int X, int Y); + + [DllImport("user32.dll")] + private static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo); + + [DllImport("user32.dll")] + private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, IntPtr dwExtraInfo); + + #endregion P/Invoke + /// /// Uses UI Automation to navigate the Settings app and set the text size. /// @@ -200,7 +218,7 @@ internal static void SetTextSizeViaUIAutomation(int percentage) /// /// Finds the "Text Size" navigation item in the Settings window. /// - static UIAutomationClient.IUIAutomationElement FindTextSizeNavigationItem( + private static UIAutomationClient.IUIAutomationElement FindTextSizeNavigationItem( UIAutomationClient.CUIAutomation uiAutomation, UIAutomationClient.IUIAutomationElement settingsWindow) { @@ -255,7 +273,7 @@ static UIAutomationClient.IUIAutomationElement FindTextSizeNavigationItem( /// /// Clicks a UI Automation element using the Invoke pattern or simulated click. /// - static void ClickElement(UIAutomationClient.IUIAutomationElement element) + private static void ClickElement(UIAutomationClient.IUIAutomationElement element) { // UI Automation Pattern IDs const int UIA_InvokePatternId = 10000; @@ -298,22 +316,4 @@ static void ClickElement(UIAutomationClient.IUIAutomationElement element) Debug.WriteLine($"Error clicking element: {ex.Message}"); } } - - // Mouse event constants for simulated clicks - private const uint MOUSEEVENTF_LEFTDOWN = 0x0002; - private const uint MOUSEEVENTF_LEFTUP = 0x0004; - - // Keyboard event constants - private const uint KEYEVENTF_KEYUP = 0x0002; - private const byte VK_DELETE = 0x2E; - - [DllImport("user32.dll")] - private static extern bool SetCursorPos(int X, int Y); - - [DllImport("user32.dll")] - private static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo); - - [DllImport("user32.dll")] - private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, IntPtr dwExtraInfo); - -} +} \ No newline at end of file From 71f80f7b3c10b1f1e1657b37afb2db07fcf6234c Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 2 Apr 2026 14:31:23 -0700 Subject: [PATCH 14/27] merging with latest main + adding more tests? --- .repolicy.json | 3 +- .../HandlerRegistrationTests.cs | 16 +- .../autoShell.Tests/SettingsHandlerTests.cs | 42 +- .../SystemCommandHandlerTests.cs | 11 +- dotnet/autoShell/AutoShell.cs | 8 +- .../Settings/DisplaySettingsHandler.cs | 50 +- .../Handlers/Settings/MouseSettingsHandler.cs | 48 +- .../Settings/SystemSettingsHandler.cs | 16 +- .../Handlers/SystemCommandHandler.cs | 11 +- dotnet/autoShell/README.md | 11 + .../autoShell/Services/IBrightnessService.cs | 20 + dotnet/autoShell/Services/IDebuggerService.cs | 15 + dotnet/autoShell/Services/IRegistryService.cs | 9 + .../Services/ISystemParametersService.cs | 6 + .../Services/WindowsBrightnessService.cs | 54 + .../Services/WindowsDebuggerService.cs | 18 + .../Services/WindowsRegistryService.cs | 7 + .../WindowsSystemParametersService.cs | 13 +- ts/docs/architecture/actionGrammar.md | 2 +- ts/extensions/agr-language/sample.agr | 2 +- ts/package.json | 2 +- .../actionGrammar/src/grammarCompletion.ts | 67 +- .../actionGrammar/src/grammarLoader.ts | 6 +- .../actionGrammar/src/grammarMatcher.ts | 22 +- .../actionGrammar/src/grammarRuleParser.ts | 14 +- .../src/grammarValueExprParser.ts | 2 +- ts/packages/actionGrammar/src/nfaCompiler.ts | 8 +- .../actionGrammar/src/nfaInterpreter.ts | 9 +- .../grammarCompletionSpacingNested.spec.ts | 545 ++++++++ .../test/grammarMatcherGroups.spec.ts | 140 ++ .../test/grammarValueExprTypeChecking.spec.ts | 16 +- .../test/grammarValueExpressions.spec.ts | 15 +- .../test/grammarValueStringUnion.spec.ts | 6 +- ts/packages/agents/browser/package.json | 2 +- ts/packages/agents/desktop/src/connector.ts | 24 + .../desktop/src/windows/inputActionsSchema.ts | 10 + .../desktop/src/windows/inputSchema.agr | 11 + .../agents/desktop/test-grammar-matching.mjs | 61 +- .../cache/src/grammar/exportGrammar.ts | 931 ++++++++++++- ts/packages/cache/src/indexGrammar.ts | 1 + ts/packages/cache/test/exportGrammar.spec.ts | 1176 +++++++++++++++++ .../data/config.test.json | 33 + .../dispatcher/src/utils/fsUtils.ts | 3 + ts/packages/shell/package.json | 2 +- ts/packages/shell/playwright.config.ts | 2 +- ts/packages/shell/src/main/instance.ts | 18 +- .../shell/src/preload/shellSettingsType.ts | 2 +- .../shell/src/renderer/src/chat/chatView.ts | 2 +- ts/packages/shell/src/renderer/src/main.ts | 21 + ts/packages/shell/test/hostWindow.spec.ts | 6 + ts/packages/shell/test/testHelper.ts | 19 +- ts/pnpm-lock.yaml | 239 +++- ts/tools/scripts/getKeys.mjs | 25 +- .../scripts/policyChecks/invisibleUnicode.mjs | 128 ++ ts/tools/scripts/repo-policy-check.mjs | 185 ++- 55 files changed, 3857 insertions(+), 258 deletions(-) create mode 100644 dotnet/autoShell/Services/IBrightnessService.cs create mode 100644 dotnet/autoShell/Services/IDebuggerService.cs create mode 100644 dotnet/autoShell/Services/WindowsBrightnessService.cs create mode 100644 dotnet/autoShell/Services/WindowsDebuggerService.cs create mode 100644 ts/packages/actionGrammar/test/grammarCompletionSpacingNested.spec.ts create mode 100644 ts/packages/cache/test/exportGrammar.spec.ts create mode 100644 ts/packages/defaultAgentProvider/data/config.test.json create mode 100644 ts/tools/scripts/policyChecks/invisibleUnicode.mjs diff --git a/.repolicy.json b/.repolicy.json index 8e7fa81899..09adbd728c 100644 --- a/.repolicy.json +++ b/.repolicy.json @@ -3,6 +3,7 @@ "ts/packages/typechat/", "ts/pnpm-lock.yaml", "docs/pnpm-lock.yaml", - "ts/pnpm-workspace.yaml" + "ts/pnpm-workspace.yaml", + "ts/examples/websiteAliases/cache/" ] } diff --git a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs index 3c8b9d583c..5154d14122 100644 --- a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs +++ b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs @@ -21,6 +21,8 @@ public HandlerRegistrationTests() var systemParamsMock = new Moq.Mock(); var processMock = new Moq.Mock(); var appRegistryMock = new Moq.Mock(); + var debuggerMock = new Moq.Mock(); + var brightnessMock = new Moq.Mock(); _handlers = [ @@ -32,15 +34,15 @@ public HandlerRegistrationTests() new NetworkCommandHandler(), new DisplayCommandHandler(), new TaskbarSettingsHandler(registryMock.Object), - new DisplaySettingsHandler(registryMock.Object, processMock.Object), + new DisplaySettingsHandler(registryMock.Object, processMock.Object, brightnessMock.Object), new PersonalizationSettingsHandler(registryMock.Object, processMock.Object), new MouseSettingsHandler(systemParamsMock.Object, processMock.Object), new AccessibilitySettingsHandler(registryMock.Object, processMock.Object), new PrivacySettingsHandler(registryMock.Object), new PowerSettingsHandler(registryMock.Object, processMock.Object), new FileExplorerSettingsHandler(registryMock.Object), - new SystemSettingsHandler(processMock.Object), - new SystemCommandHandler(processMock.Object), + new SystemSettingsHandler(registryMock.Object, processMock.Object), + new SystemCommandHandler(processMock.Object, debuggerMock.Object), ]; } @@ -113,14 +115,6 @@ public void AllCommands_HaveAtLeastOneUnitTest() "EnableMeteredConnections", "EnableWifi", "ListWifiNetworks", "ToggleAirplaneMode", // DisplayCommandHandler — direct P/Invoke "ListResolutions", "SetScreenResolution", "SetTextSize", - // SystemCommandHandler.Debug — Debugger.Launch() - "Debug", - // MouseSettingsHandler.SetPrimaryMouseButton — native SwapMouseButton - "SetPrimaryMouseButton", - // DisplaySettingsHandler.AdjustScreenBrightness — WMI + direct Registry - "AdjustScreenBrightness", - // SystemSettingsHandler.AutomaticDSTAdjustment — direct Registry.LocalMachine - "AutomaticDSTAdjustment", }; // Discover all test classes in this assembly diff --git a/dotnet/autoShell.Tests/SettingsHandlerTests.cs b/dotnet/autoShell.Tests/SettingsHandlerTests.cs index a380a09537..30d7117600 100644 --- a/dotnet/autoShell.Tests/SettingsHandlerTests.cs +++ b/dotnet/autoShell.Tests/SettingsHandlerTests.cs @@ -446,6 +446,23 @@ public void TouchpadCursorSpeed_OpensTouchpadSettings() _processMock.Verify(p => p.StartShellExecute("ms-settings:devices-touchpad"), Times.Once); } + [Fact] + public void SetPrimaryMouseButton_Right_SwapsButtons() + { + Handle("SetPrimaryMouseButton", """{"primaryButton":"right"}"""); + + _systemParamsMock.Verify(s => s.SwapMouseButton(true), Times.Once); + } + + [Fact] + public void CursorTrail_Enable_SetsTrailLength() + { + Handle("CursorTrail", """{"enable":true,"length":7}"""); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x005D, 7, IntPtr.Zero, 3), Times.Once); + } + private void Handle(string key, string jsonValue) { _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); @@ -616,11 +633,12 @@ public class DisplaySettingsHandlerTests { private readonly Mock _registryMock = new(); private readonly Mock _processMock = new(); + private readonly Mock _brightnessMock = new(); private readonly DisplaySettingsHandler _handler; public DisplaySettingsHandlerTests() { - _handler = new DisplaySettingsHandler(_registryMock.Object, _processMock.Object); + _handler = new DisplaySettingsHandler(_registryMock.Object, _processMock.Object, _brightnessMock.Object); } [Fact] @@ -699,6 +717,15 @@ public void RotationLock_Disable_SetsPreference0() "RotationLockPreference", 0, RegistryValueKind.DWord), Times.Once); } + [Fact] + public void AdjustScreenBrightness_Increase_SetsBrightnessPlus10() + { + _brightnessMock.Setup(b => b.GetCurrentBrightness()).Returns(50); + Handle("AdjustScreenBrightness", """{"brightnessLevel":"increase"}"""); + + _brightnessMock.Verify(b => b.SetBrightness(60), Times.Once); + } + private void Handle(string key, string jsonValue) { _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); @@ -711,12 +738,13 @@ private void Handle(string key, string jsonValue) public class SystemSettingsHandlerTests { + private readonly Mock _registryMock = new(); private readonly Mock _processMock = new(); private readonly SystemSettingsHandler _handler; public SystemSettingsHandlerTests() { - _handler = new SystemSettingsHandler(_processMock.Object); + _handler = new SystemSettingsHandler(_registryMock.Object, _processMock.Object); } [Fact] @@ -759,6 +787,16 @@ public void RememberWindowLocations_OpensDisplaySettings() _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); } + [Fact] + public void AutomaticDSTAdjustment_Enable_SetsRegistryValue() + { + Handle("AutomaticDSTAdjustment", """{"enable":true}"""); + + _registryMock.Verify(r => r.SetValueLocalMachine( + It.IsAny(), "DynamicDaylightTimeDisabled", + 0, Microsoft.Win32.RegistryValueKind.DWord), Times.Once); + } + private void Handle(string key, string jsonValue) { _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); diff --git a/dotnet/autoShell.Tests/SystemCommandHandlerTests.cs b/dotnet/autoShell.Tests/SystemCommandHandlerTests.cs index ef45e70b2b..7a22ce96e0 100644 --- a/dotnet/autoShell.Tests/SystemCommandHandlerTests.cs +++ b/dotnet/autoShell.Tests/SystemCommandHandlerTests.cs @@ -11,11 +11,20 @@ namespace autoShell.Tests; public class SystemCommandHandlerTests { private readonly Mock _processMock = new(); + private readonly Mock _debuggerMock = new(); private readonly SystemCommandHandler _handler; public SystemCommandHandlerTests() { - _handler = new SystemCommandHandler(_processMock.Object); + _handler = new SystemCommandHandler(_processMock.Object, _debuggerMock.Object); + } + + [Fact] + public void Debug_LaunchesDebugger() + { + Handle("Debug", ""); + + _debuggerMock.Verify(d => d.Launch(), Times.Once); } [Fact] diff --git a/dotnet/autoShell/AutoShell.cs b/dotnet/autoShell/AutoShell.cs index 99a5245332..96248804e0 100644 --- a/dotnet/autoShell/AutoShell.cs +++ b/dotnet/autoShell/AutoShell.cs @@ -36,6 +36,8 @@ static AutoShell() var process = new WindowsProcessService(); var audio = new WindowsAudioService(); var appRegistry = new WindowsAppRegistry(); + var debugger = new WindowsDebuggerService(); + var brightness = new WindowsBrightnessService(); s_dispatcher = new CommandDispatcher(); s_dispatcher.Register( @@ -47,15 +49,15 @@ static AutoShell() new NetworkCommandHandler(), new DisplayCommandHandler(), new TaskbarSettingsHandler(registry), - new DisplaySettingsHandler(registry, process), + new DisplaySettingsHandler(registry, process, brightness), new PersonalizationSettingsHandler(registry, process), new MouseSettingsHandler(systemParams, process), new AccessibilitySettingsHandler(registry, process), new PrivacySettingsHandler(registry), new PowerSettingsHandler(registry, process), new FileExplorerSettingsHandler(registry), - new SystemSettingsHandler(process), - new SystemCommandHandler(process) + new SystemSettingsHandler(registry, process), + new SystemCommandHandler(process, debugger) ); } diff --git a/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs index 067b808aed..af2115ebf8 100644 --- a/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using autoShell.Services; using Microsoft.Win32; using Newtonsoft.Json.Linq; @@ -19,11 +18,13 @@ internal class DisplaySettingsHandler : ICommandHandler { private readonly IRegistryService _registry; private readonly IProcessService _process; + private readonly IBrightnessService _brightness; - public DisplaySettingsHandler(IRegistryService registry, IProcessService process) + public DisplaySettingsHandler(IRegistryService registry, IProcessService process, IBrightnessService brightness) { - this._registry = registry; - this._process = process; + _registry = registry; + _process = process; + _brightness = brightness; } /// @@ -84,12 +85,12 @@ private void HandleAdjustScreenBrightness(JObject param) string level = param.Value("brightnessLevel"); bool increase = level == "increase"; - byte currentBrightness = GetCurrentBrightness(); + byte currentBrightness = _brightness.GetCurrentBrightness(); byte newBrightness = increase ? (byte)Math.Min(100, currentBrightness + 10) : (byte)Math.Max(0, currentBrightness - 10); - SetBrightness(newBrightness); + _brightness.SetBrightness(newBrightness); Debug.WriteLine($"Brightness adjusted to: {newBrightness}%"); } @@ -137,41 +138,4 @@ private void HandleRotationLock(JObject param) enable ? 1 : 0, RegistryValueKind.DWord); } - - private static byte GetCurrentBrightness() - { - try - { - using var key = Registry.CurrentUser.OpenSubKey( - @"Software\Microsoft\Windows\CurrentVersion\SettingSync\Settings\SystemSettings\Brightness"); - if (key != null) - { - object value = key.GetValue("Data"); - if (value is byte[] data && data.Length > 0) - { - return data[0]; - } - } - } - catch { } - return 50; - } - - private static void SetBrightness(byte brightness) - { - try - { - using var searcher = new System.Management.ManagementObjectSearcher( - "root\\WMI", "SELECT * FROM WmiMonitorBrightnessMethods"); - using var objectCollection = searcher.Get(); - foreach (System.Management.ManagementObject obj in objectCollection.Cast()) - { - obj.InvokeMethod("WmiSetBrightness", [1, brightness]); - } - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to set brightness: {ex.Message}"); - } - } } diff --git a/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs index 7cc50876ac..e66aa97d14 100644 --- a/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; +using System.Diagnostics; using autoShell.Services; using Newtonsoft.Json.Linq; @@ -13,33 +13,31 @@ namespace autoShell.Handlers.Settings; /// Handles mouse and touchpad settings: pointer size, precision, cursor speed, scroll lines, /// primary button, customization, and touchpad. /// -internal partial class MouseSettingsHandler : ICommandHandler +internal class MouseSettingsHandler : ICommandHandler { - #region P/Invoke private const int SPI_GETMOUSE = 3; private const int SPI_SETMOUSE = 4; private const int SPI_SETMOUSESPEED = 0x0071; + private const int SPI_SETMOUSETRAILS = 0x005D; private const int SPI_SETWHEELSCROLLLINES = 0x0069; + private const int SPIF_UPDATEINIFILE = 0x01; + private const int SPIF_SENDCHANGE = 0x02; private const int SPIF_UPDATEINIFILE_SENDCHANGE = 3; - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool SwapMouseButton(int fSwap); - #endregion P/Invoke - private readonly ISystemParametersService _systemParams; private readonly IProcessService _process; public MouseSettingsHandler(ISystemParametersService systemParams, IProcessService process) { - this._systemParams = systemParams; - this._process = process; + _systemParams = systemParams; + _process = process; } /// public IEnumerable SupportedCommands { get; } = [ "AdjustMousePointerSize", + "CursorTrail", "EnableTouchPad", "EnhancePointerPrecision", "MouseCursorSpeed", @@ -63,6 +61,10 @@ public void Handle(string key, string value, JToken rawValue) this._process.StartShellExecute("ms-settings:easeofaccess-mouse"); break; + case "CursorTrail": + this.HandleMouseCursorTrail(value); + break; + case "EnableTouchPad": case "TouchpadCursorSpeed": this._process.StartShellExecute("ms-settings:devices-touchpad"); @@ -106,16 +108,38 @@ private void HandleMouseCursorSpeed(JObject param) this._systemParams.SetParameter(SPI_SETMOUSESPEED, 0, (IntPtr)speed, SPIF_UPDATEINIFILE_SENDCHANGE); } + /// + /// Enables or disables the mouse cursor trail and sets its length. + /// Command: {"CursorTrail": "{\"enable\":true,\"length\":7}"} + /// SPI_SETMOUSETRAILS: 0 or 1 = off, >= 2 = trail length + /// + private void HandleMouseCursorTrail(string jsonParams) + { + var param = JObject.Parse(jsonParams); + var enable = param.Value("enable") ?? true; + var length = param.Value("length") ?? 7; + + // Clamp trail length to valid range + length = Math.Max(2, Math.Min(12, length)); + + int trailValue = enable ? length : 0; + + this._systemParams.SetParameter(SPI_SETMOUSETRAILS, trailValue, IntPtr.Zero, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + Debug.WriteLine(enable + ? $"Cursor trail enabled with length {length}" + : "Cursor trail disabled"); + } + private void HandleMouseWheelScrollLines(JObject param) { int lines = param.Value("scrollLines") ?? 3; this._systemParams.SetParameter(SPI_SETWHEELSCROLLLINES, lines, IntPtr.Zero, SPIF_UPDATEINIFILE_SENDCHANGE); } - private static void HandleSetPrimaryMouseButton(JObject param) + private void HandleSetPrimaryMouseButton(JObject param) { string button = param.Value("primaryButton") ?? "left"; bool leftPrimary = button.Equals("left", StringComparison.OrdinalIgnoreCase); - SwapMouseButton(leftPrimary ? 0 : 1); + _systemParams.SwapMouseButton(!leftPrimary); } } diff --git a/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs index a178c3cdb9..8e78e89529 100644 --- a/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs +++ b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using autoShell.Services; -using Microsoft.Win32; using Newtonsoft.Json.Linq; namespace autoShell.Handlers.Settings; @@ -15,11 +14,13 @@ namespace autoShell.Handlers.Settings; /// internal class SystemSettingsHandler : ICommandHandler { + private readonly IRegistryService _registry; private readonly IProcessService _process; - public SystemSettingsHandler(IProcessService process) + public SystemSettingsHandler(IRegistryService registry, IProcessService process) { - this._process = process; + _registry = registry; + _process = process; } /// @@ -68,13 +69,16 @@ public void Handle(string key, string value, JToken rawValue) } } - private static void HandleAutomaticDSTAdjustment(string jsonParams) + private void HandleAutomaticDSTAdjustment(string jsonParams) { var param = JObject.Parse(jsonParams); bool enable = param.Value("enable") ?? true; - using var key = Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"); - key?.SetValue("DynamicDaylightTimeDisabled", enable ? 0 : 1, RegistryValueKind.DWord); + _registry.SetValueLocalMachine( + @"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", + "DynamicDaylightTimeDisabled", + enable ? 0 : 1, + Microsoft.Win32.RegistryValueKind.DWord); Debug.WriteLine($"Automatic DST adjustment {(enable ? "enabled" : "disabled")}"); } diff --git a/dotnet/autoShell/Handlers/SystemCommandHandler.cs b/dotnet/autoShell/Handlers/SystemCommandHandler.cs index 3ae95ad093..53c409f9fd 100644 --- a/dotnet/autoShell/Handlers/SystemCommandHandler.cs +++ b/dotnet/autoShell/Handlers/SystemCommandHandler.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Collections.Generic; -using System.Diagnostics; using autoShell.Services; using Newtonsoft.Json.Linq; @@ -14,10 +13,12 @@ namespace autoShell.Handlers; internal class SystemCommandHandler : ICommandHandler { private readonly IProcessService _process; + private readonly IDebuggerService _debugger; - public SystemCommandHandler(IProcessService process) + public SystemCommandHandler(IProcessService process, IDebuggerService debugger) { - this._process = process; + _process = process; + _debugger = debugger; } /// @@ -33,11 +34,11 @@ public void Handle(string key, string value, JToken rawValue) switch (key) { case "Debug": - Debugger.Launch(); + _debugger.Launch(); break; case "ToggleNotifications": - this._process.StartShellExecute("ms-actioncenter:"); + _process.StartShellExecute("ms-actioncenter:"); break; } } diff --git a/dotnet/autoShell/README.md b/dotnet/autoShell/README.md index 2ea09edf0f..94dd50baae 100644 --- a/dotnet/autoShell/README.md +++ b/dotnet/autoShell/README.md @@ -123,6 +123,7 @@ Run the application and send JSON commands via stdin: | `EnhancePointerPrecision` | `true`/`false` | Enables or disables pointer precision | | `AdjustMousePointerSize` | value | Adjusts mouse pointer size | | `mousePointerCustomization` | value | Customizes mouse pointer | +| `CursorTrail` | `{"enable": true/false, "length": 2-12}` | Enables/disables cursor trail (length: 2–12) | ##### Touchpad Settings @@ -287,6 +288,16 @@ Set display resolution using JSON object: {"setScreenResolution": {"width": 2560, "height": 1440, "refreshRate": 60}} ``` +Enable cursor trail with length 7: +```json +{"CursorTrail": "{\"enable\":true,\"length\":7}"} +``` + +Disable cursor trail: +```json +{"CursorTrail": "{\"enable\":false}"} +``` + ### Supported Application Friendly Names AutoShell recognizes these friendly names (case-insensitive): diff --git a/dotnet/autoShell/Services/IBrightnessService.cs b/dotnet/autoShell/Services/IBrightnessService.cs new file mode 100644 index 0000000000..e6de6f56b6 --- /dev/null +++ b/dotnet/autoShell/Services/IBrightnessService.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace autoShell.Services; + +/// +/// Abstracts display brightness operations for testability. +/// +internal interface IBrightnessService +{ + /// + /// Gets the current display brightness (0–100). + /// + byte GetCurrentBrightness(); + + /// + /// Sets the display brightness (0–100). + /// + void SetBrightness(byte brightness); +} diff --git a/dotnet/autoShell/Services/IDebuggerService.cs b/dotnet/autoShell/Services/IDebuggerService.cs new file mode 100644 index 0000000000..c392ec0544 --- /dev/null +++ b/dotnet/autoShell/Services/IDebuggerService.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace autoShell.Services; + +/// +/// Abstracts debugger operations for testability. +/// +internal interface IDebuggerService +{ + /// + /// Launches and attaches a debugger to the process. + /// + void Launch(); +} diff --git a/dotnet/autoShell/Services/IRegistryService.cs b/dotnet/autoShell/Services/IRegistryService.cs index d126d827b4..266c32e857 100644 --- a/dotnet/autoShell/Services/IRegistryService.cs +++ b/dotnet/autoShell/Services/IRegistryService.cs @@ -24,4 +24,13 @@ internal interface IRegistryService /// The data to store. /// The registry data type. void SetValue(string keyPath, string valueName, object value, Microsoft.Win32.RegistryValueKind valueKind); + + /// + /// Sets a value in the registry under HKEY_LOCAL_MACHINE. + /// + /// The registry subkey path (created if it does not exist). + /// The name of the value to set. + /// The data to store. + /// The registry data type. + void SetValueLocalMachine(string keyPath, string valueName, object value, Microsoft.Win32.RegistryValueKind valueKind); } diff --git a/dotnet/autoShell/Services/ISystemParametersService.cs b/dotnet/autoShell/Services/ISystemParametersService.cs index f414204792..d0bf38193f 100644 --- a/dotnet/autoShell/Services/ISystemParametersService.cs +++ b/dotnet/autoShell/Services/ISystemParametersService.cs @@ -45,4 +45,10 @@ internal interface ISystemParametersService /// Array to receive the value. /// Flags (typically 0 for get operations). bool GetParameter(int action, int param, int[] vparam, int flags); + + /// + /// Swaps the primary and secondary mouse buttons. + /// + /// If true, swaps the buttons; if false, restores default. + bool SwapMouseButton(bool swap); } diff --git a/dotnet/autoShell/Services/WindowsBrightnessService.cs b/dotnet/autoShell/Services/WindowsBrightnessService.cs new file mode 100644 index 0000000000..66d1e70990 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsBrightnessService.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Linq; +using Microsoft.Win32; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IBrightnessService using WMI and Windows Registry. +/// +internal class WindowsBrightnessService : IBrightnessService +{ + /// + public byte GetCurrentBrightness() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey( + @"Software\Microsoft\Windows\CurrentVersion\SettingSync\Settings\SystemSettings\Brightness"); + if (key != null) + { + object value = key.GetValue("Data"); + if (value is byte[] data && data.Length > 0) + { + return data[0]; + } + } + } + catch { } + return 50; + } + + /// + public void SetBrightness(byte brightness) + { + try + { + using var searcher = new System.Management.ManagementObjectSearcher( + "root\\WMI", "SELECT * FROM WmiMonitorBrightnessMethods"); + using var objectCollection = searcher.Get(); + foreach (System.Management.ManagementObject obj in objectCollection.Cast()) + { + obj.InvokeMethod("WmiSetBrightness", [1, brightness]); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to set brightness: {ex.Message}"); + } + } +} diff --git a/dotnet/autoShell/Services/WindowsDebuggerService.cs b/dotnet/autoShell/Services/WindowsDebuggerService.cs new file mode 100644 index 0000000000..fc3d93c2c6 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsDebuggerService.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IDebuggerService using System.Diagnostics.Debugger. +/// +internal class WindowsDebuggerService : IDebuggerService +{ + /// + public void Launch() + { + Debugger.Launch(); + } +} diff --git a/dotnet/autoShell/Services/WindowsRegistryService.cs b/dotnet/autoShell/Services/WindowsRegistryService.cs index bafce5de41..e35837fa9b 100644 --- a/dotnet/autoShell/Services/WindowsRegistryService.cs +++ b/dotnet/autoShell/Services/WindowsRegistryService.cs @@ -23,4 +23,11 @@ public void SetValue(string keyPath, string valueName, object value, RegistryVal using var key = Registry.CurrentUser.CreateSubKey(keyPath); key?.SetValue(valueName, value, valueKind); } + + /// + public void SetValueLocalMachine(string keyPath, string valueName, object value, RegistryValueKind valueKind) + { + using var key = Registry.LocalMachine.CreateSubKey(keyPath); + key?.SetValue(valueName, value, valueKind); + } } diff --git a/dotnet/autoShell/Services/WindowsSystemParametersService.cs b/dotnet/autoShell/Services/WindowsSystemParametersService.cs index bf039d4a13..d2a2e09586 100644 --- a/dotnet/autoShell/Services/WindowsSystemParametersService.cs +++ b/dotnet/autoShell/Services/WindowsSystemParametersService.cs @@ -11,9 +11,6 @@ namespace autoShell.Services; /// internal partial class WindowsSystemParametersService : ISystemParametersService { - private const int SPIF_UPDATEINIFILE = 0x01; - private const int SPIF_SENDCHANGE = 0x02; - [LibraryImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool SystemParametersInfo(int uiAction, int uiParam, IntPtr pvParam, int fWinIni); @@ -48,4 +45,14 @@ public bool GetParameter(int action, int param, int[] vparam, int flags) { return SystemParametersInfo(action, param, vparam, flags); } + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SwapMouseButtonNative(int fSwap); + + /// + public bool SwapMouseButton(bool swap) + { + return SwapMouseButtonNative(swap ? 1 : 0); + } } diff --git a/ts/docs/architecture/actionGrammar.md b/ts/docs/architecture/actionGrammar.md index d62a3f817f..1bbdf840cb 100644 --- a/ts/docs/architecture/actionGrammar.md +++ b/ts/docs/architecture/actionGrammar.md @@ -909,7 +909,7 @@ type-checked at compile time. Every expression node has a statically-known type — there is no `any` escape hatch. This section documents the design principles and restrictions. -Enable expressions via `enableExpressions: true` in +Enable expressions via `enableValueExpressions: true` in `LoadGrammarRulesOptions` when your grammar rules need computed values (arithmetic, conditionals, method calls) in the `->` position. diff --git a/ts/extensions/agr-language/sample.agr b/ts/extensions/agr-language/sample.agr index b12c56ff18..b559b5d640 100644 --- a/ts/extensions/agr-language/sample.agr +++ b/ts/extensions/agr-language/sample.agr @@ -173,7 +173,7 @@ export = hello | goodbye; parameters: { items: [item], listName: list } }; -// ─── Value expressions (enableExpressions mode) ────────────────────────────── +// ─── Value expressions (enableValueExpressions mode) ───────────────────────── // Ternary operator = diff --git a/ts/package.json b/ts/package.json index 184a5fc7bc..93bf678be8 100644 --- a/ts/package.json +++ b/ts/package.json @@ -61,7 +61,7 @@ "prettier": "^3.5.3", "shx": "^0.4.0" }, - "packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017", + "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be", "engines": { "node": ">=20", "pnpm": ">=10" diff --git a/ts/packages/actionGrammar/src/grammarCompletion.ts b/ts/packages/actionGrammar/src/grammarCompletion.ts index 1e90faf30b..8ebf75fa62 100644 --- a/ts/packages/actionGrammar/src/grammarCompletion.ts +++ b/ts/packages/actionGrammar/src/grammarCompletion.ts @@ -20,6 +20,7 @@ import { finalizeNestedRule, matchState, initialMatchState, + leadingSpacingMode, } from "./grammarMatcher.js"; const debugCompletion = registerDebug("typeagent:grammar:completion"); @@ -64,6 +65,7 @@ function matchWordsGreedily( text: string, startIndex: number, spacingMode: CompiledSpacingMode, + suppressLeadingSeparator: boolean = false, ): { matchedWords: number; endIndex: number; prevEndIndex: number } { let index = startIndex; let prevIndex = startIndex; @@ -73,11 +75,37 @@ function matchWordsGreedily( const word = words[k]; const escaped = escapeMatch(word); + // Separator logic has two independent dimensions: + // + // k === 0 (first word): + // Governed by the *caller*, not spacingMode. The caller + // decides whether the leading separator (between the match + // start position and the first keyword word) is allowed: + // suppressLeadingSeparator=true → bare word, no prefix + // (used when leadingSpacingMode() returns "none") + // suppressLeadingSeparator=false → [sep]*? lazy optional + // prefix that skips any leading whitespace/punctuation. + // The lazy quantifier matches zero-width when the + // keyword starts immediately, so this is "allow but + // don't require" leading whitespace. + // Note: spacingMode is intentionally NOT consulted for + // k=0. Even when spacingMode is "none", callers like + // matchKeywordWordsFrom (wildcard scanning) need the + // optional leading separator to find keyword candidates + // at separator-delimited positions within wildcard text. + // + // k > 0 (subsequent words): + // Governed by the rule's spacingMode: + // "none" → bare word, no inter-word separator + // other → separator required or optional per + // requiresSeparator() for the character pair let regExpStr: string; - if (spacingMode === "none") { + if (k === 0) { + regExpStr = suppressLeadingSeparator + ? escaped + : `[${separatorRegExpStr}]*?${escaped}`; + } else if (spacingMode === "none") { regExpStr = escaped; - } else if (k === 0) { - regExpStr = `[${separatorRegExpStr}]*?${escaped}`; } else { const sep = requiresSeparator( words[k - 1].at(-1)!, @@ -388,6 +416,7 @@ function tryPartialStringMatch( spacingMode: CompiledSpacingMode, direction?: "forward" | "backward", effectivePrefixEnd?: number, + suppressLeadingSeparator: boolean = false, ): | { consumedLength: number; @@ -403,6 +432,7 @@ function tryPartialStringMatch( input, startIndex, spacingMode, + suppressLeadingSeparator, ); // Direction matters when at least one word fully matched and no @@ -679,11 +709,15 @@ function collectPropertyCandidate( ): void { updateMaxPrefixLength(ctx, prefixPosition); if (prefixPosition !== ctx.maxPrefixLength) return; + // At the leading edge of a nested rule, the separator between + // the matched prefix and the property slot is governed by the + // parent's spacing mode, not the nested rule's own mode. + const effectiveMode = leadingSpacingMode(state); ctx.fixedCandidates.push({ kind: "property", valueId, state: { ...state }, - spacingMode: state.spacingMode, + spacingMode: effectiveMode, isAfterWildcard, partialKeywordBackup: false, }); @@ -696,7 +730,8 @@ function collectPropertyCandidate( // candidate may still be discarded by the maxPrefixLength filter). function tryCollectStringCandidate( ctx: CompletionContext, - state: MatchState, + leadingMode: CompiledSpacingMode, + interWordMode: CompiledSpacingMode, part: StringPart, isAfterWildcard: boolean, startIndex: number, @@ -707,17 +742,26 @@ function tryCollectStringCandidate( part, ctx.input, startIndex, - state.spacingMode, + interWordMode, dir, effectivePrefixEnd, + leadingMode === "none", ); if (partial !== undefined) { updateMaxPrefixLength(ctx, partial.consumedLength); if (partial.consumedLength === ctx.maxPrefixLength) { + // When no words were consumed (completion is the first + // word of the part), the separator between the matched + // prefix and the completion is governed by the leading + // mode, not the rule's inter-word spacingMode. + const candidateSpacingMode = + partial.consumedLength === startIndex + ? leadingMode + : interWordMode; ctx.fixedCandidates.push({ kind: "string", completionText: partial.remainingText, - spacingMode: state.spacingMode, + spacingMode: candidateSpacingMode, isAfterWildcard, partialKeywordBackup: false, }); @@ -763,7 +807,8 @@ function tryCollectBackwardCandidate( if ( tryCollectStringCandidate( ctx, - state, + leadingSpacingMode(state), + info.matchedSpacingMode, info.part, info.afterWildcard, info.start, @@ -917,7 +962,8 @@ function processCleanPartial( if (nextPart.type === "string" && !deferredToEoi) { tryCollectStringCandidate( ctx, - state, + leadingSpacingMode(state), + state.spacingMode, nextPart, savedPendingWildcard?.valueId !== undefined, state.index, @@ -1017,7 +1063,8 @@ function processDirtyPartial( if (currentPart !== undefined && currentPart.type === "string") { tryCollectStringCandidate( ctx, - state, + leadingSpacingMode(state), + state.spacingMode, currentPart, false, state.index, diff --git a/ts/packages/actionGrammar/src/grammarLoader.ts b/ts/packages/actionGrammar/src/grammarLoader.ts index abcac4278b..5f4c26b094 100644 --- a/ts/packages/actionGrammar/src/grammarLoader.ts +++ b/ts/packages/actionGrammar/src/grammarLoader.ts @@ -10,7 +10,7 @@ export type LoadGrammarRulesOptions = { start?: string; // Optional start symbol (default: "Start") startValueRequired?: boolean; // Whether the start rule must produce a value (default: true) schemaLoader?: SchemaLoader; // Optional loader for resolving .ts type imports - enableExpressions?: boolean; // Enable JavaScript-like value expressions (default: false) + enableValueExpressions?: boolean; // Enable JavaScript-like value expressions (default: false) }; function parseAndCompileGrammar( @@ -35,12 +35,12 @@ function parseAndCompileGrammar( const start = options?.start ?? "Start"; const startValueRequired = options?.startValueRequired ?? true; - const enableExpressions = options?.enableExpressions ?? false; + const enableValueExpressions = options?.enableValueExpressions ?? false; const parseResult = parseGrammarRules( displayPath, content, undefined, - enableExpressions, + enableValueExpressions, ); const grammar = compileGrammar( displayPath, diff --git a/ts/packages/actionGrammar/src/grammarMatcher.ts b/ts/packages/actionGrammar/src/grammarMatcher.ts index 1fad6c9698..0d4092c5aa 100644 --- a/ts/packages/actionGrammar/src/grammarMatcher.ts +++ b/ts/packages/actionGrammar/src/grammarMatcher.ts @@ -260,12 +260,14 @@ export type MatchState = { readonly start: number; readonly part: StringPart; readonly afterWildcard: boolean; + readonly matchedSpacingMode: CompiledSpacingMode; } | { readonly type: "number"; readonly start: number; readonly valueId: number; readonly afterWildcard: boolean; + readonly matchedSpacingMode: CompiledSpacingMode; } | undefined; }; @@ -835,11 +837,26 @@ function matchStringPartWithWildcard( } if (captureWildcard(state, request, wildcardEnd, newIndex, pending)) { + // Assign default string value for single-part rules without + // an explicit value expression — same logic as the non-wildcard + // path in matchStringPartWithoutWildcard. Without this, a + // pending wildcard from a parent rule that leaks into a + // single-part child rule would bypass the default value + // assignment and cause "No value assign to variable" at + // finalizeNestedRule time. + if ( + state.value === undefined && + state.parts.length === 1 && + state.valueIds !== null + ) { + addValue(state, undefined, part.value.join(" ")); + } state.lastMatchedPartInfo = { type: "string", start: wildcardEnd, part, afterWildcard: true, + matchedSpacingMode: state.spacingMode, }; debugMatch( state, @@ -891,6 +908,7 @@ function matchStringPartWithoutWildcard( start: curr, part, afterWildcard: false, + matchedSpacingMode: state.spacingMode, }; state.index = newIndex; return true; @@ -911,7 +929,7 @@ function matchStringPartWithoutWildcard( // flex-space, we've reached the top-level rule — its spacing mode // determines the leading/trailing behavior (all modes except "none" // allow leading whitespace at the top level). -function leadingSpacingMode(state: MatchState): CompiledSpacingMode { +export function leadingSpacingMode(state: MatchState): CompiledSpacingMode { if (state.partIndex !== 0 || state.parent === undefined) { return state.spacingMode; } @@ -1035,6 +1053,7 @@ function matchVarNumberPartWithWildcard( start: wildcardEnd, valueId, afterWildcard: true, + matchedSpacingMode: state.spacingMode, }; } return true; @@ -1088,6 +1107,7 @@ function matchVarNumberPartWithoutWildcard( start: curr, valueId, afterWildcard: false, + matchedSpacingMode: state.spacingMode, }; } state.index = newIndex; diff --git a/ts/packages/actionGrammar/src/grammarRuleParser.ts b/ts/packages/actionGrammar/src/grammarRuleParser.ts index b393663968..cfeb7d99f0 100644 --- a/ts/packages/actionGrammar/src/grammarRuleParser.ts +++ b/ts/packages/actionGrammar/src/grammarRuleParser.ts @@ -87,7 +87,7 @@ const debugParse = registerDebug("typeagent:grammar:parse"); * ::= * ::= "(" ( ")" | ")?" | ")*" | ")+" ) * - * // ── Value (basic mode: enableExpressions=false) ────────────────────────── + * // ── Value (basic mode: enableValueExpressions=false) ────────────────────────── * = | | * | | | * = "[" ( ("," )* ","?)? "]" @@ -102,8 +102,8 @@ const debugParse = registerDebug("typeagent:grammar:parse"); * = * = * - * // ── Value expressions (enableExpressions=true) ─────────────────────────── - * // When enableExpressions is true, the position (after "->") + * // ── Value expressions (enableValueExpressions=true) ─────────────────────────── + * // When enableValueExpressions is true, the position (after "->") * // supports full JavaScript-like expressions with operator precedence. * // In this mode is replaced by . * // @@ -181,13 +181,13 @@ export function parseGrammarRules( /** Whether to track source positions on value nodes (default: true). */ position?: boolean, /** Enable JavaScript-like value expressions in the `->` position (default: false). */ - enableExpressions: boolean = false, + enableValueExpressions: boolean = false, ): GrammarParseResult { const parser = new GrammarRuleParser( fileName, content, position ?? true, - enableExpressions, + enableValueExpressions, ); const result = parser.parse(); debugParse(JSON.stringify(result, undefined, 2)); @@ -430,7 +430,7 @@ class GrammarRuleParser implements ValueExprParserContext { private readonly fileName: string, readonly content: string, private readonly position: boolean = true, - private readonly enableExpressions: boolean = false, + private readonly enableValueExpressions: boolean = false, ) {} private get pos(): number | undefined { @@ -905,7 +905,7 @@ class GrammarRuleParser implements ValueExprParserContext { } private parseValue(): ValueNode { - if (this.enableExpressions) { + if (this.enableValueExpressions) { return parseValueExpr(this); } return this.parseSimpleValue(); diff --git a/ts/packages/actionGrammar/src/grammarValueExprParser.ts b/ts/packages/actionGrammar/src/grammarValueExprParser.ts index e73d8c0b84..25cbf709d9 100644 --- a/ts/packages/actionGrammar/src/grammarValueExprParser.ts +++ b/ts/packages/actionGrammar/src/grammarValueExprParser.ts @@ -130,7 +130,7 @@ export interface ValueExprParserContext { /** * Parse a value expression. - * Entry point called from GrammarRuleParser.parseValue() when enableExpressions is true. + * Entry point called from GrammarRuleParser.parseValue() when enableValueExpressions is true. */ export function parseValueExpr(ctx: ValueExprParserContext): ValueNode { return parseTernary(ctx); diff --git a/ts/packages/actionGrammar/src/nfaCompiler.ts b/ts/packages/actionGrammar/src/nfaCompiler.ts index 1600d3f43a..180faa9b02 100644 --- a/ts/packages/actionGrammar/src/nfaCompiler.ts +++ b/ts/packages/actionGrammar/src/nfaCompiler.ts @@ -479,9 +479,11 @@ export function compileGrammarToNFA(grammar: Grammar, name?: string): NFA { // Create slot map for this rule FIRST (needed to compile value expression) const slotMap = createRuleSlotMap(rule); - if (slotMap.size > 0) { - builder.setStateSlotInfo(ruleEntry, slotMap.size, slotMap); - } + // Always set slot info for Start rules — even rules with no variables + // need an environment so that nested sub-rules (e.g. optional groups + // normalized into passthrough wrappers) can correctly pop back to the + // Start rule's environment/actionValue on exit. + builder.setStateSlotInfo(ruleEntry, slotMap.size, slotMap); // Create type map for type conversion during evaluation const typeMap = createRuleTypeMap(rule); diff --git a/ts/packages/actionGrammar/src/nfaInterpreter.ts b/ts/packages/actionGrammar/src/nfaInterpreter.ts index d8c8848f62..d1095798d4 100644 --- a/ts/packages/actionGrammar/src/nfaInterpreter.ts +++ b/ts/packages/actionGrammar/src/nfaInterpreter.ts @@ -306,11 +306,14 @@ function seedQueue( ` Environment slots: ${JSON.stringify(state.environment.slots)}`, ); } - if (state.actionValue && state.environment) { + if (state.actionValue) { + // Use the existing environment, or create an empty one for + // literal-only values that don't reference any variables. + const env = state.environment ?? createEnvironment(0); try { evaluatedActionValue = evaluateExpression( state.actionValue, - state.environment, + env, ); debugNFA( ` Evaluated actionValue: ${JSON.stringify(evaluatedActionValue)}`, @@ -788,7 +791,7 @@ function epsilonClosure( // This is a rule entry with an action value - use it currentActionValue = nfaState.actionValue; } else if (nfaState.actionValue !== undefined && !state.environment) { - // Legacy: top-level rule entry without environment (backwards compatibility) + // Top-level rule entry without environment (backwards compatibility) currentActionValue = nfaState.actionValue; } // Otherwise keep the current actionValue (we're inside the same rule) diff --git a/ts/packages/actionGrammar/test/grammarCompletionSpacingNested.spec.ts b/ts/packages/actionGrammar/test/grammarCompletionSpacingNested.spec.ts new file mode 100644 index 0000000000..c3ddf766a4 --- /dev/null +++ b/ts/packages/actionGrammar/test/grammarCompletionSpacingNested.spec.ts @@ -0,0 +1,545 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Completion tests for grammars that mix spacing modes across nested rules. + * + * The matching path (grammarMatcherSpacingNested.spec.ts) covers how the + * grammar matches full inputs with mixed spacing modes. This file covers + * the COMPLETION path: what completions are offered, what separatorMode + * values are reported, and how the effective leading spacing mode at + * nested-rule boundaries affects completion behavior. + * + * Key invariant: the separator between a matched prefix and the first + * completion word should be governed by `leadingSpacingMode` (which walks + * the parent chain), not the nested rule's own `spacingMode`. This + * mirrors how `matchStringPart` in grammarMatcher.ts uses + * `leadingSpacingMode` for its leading separator regex. + */ + +import { loadGrammarRules } from "../src/grammarLoader.js"; +import { describeForEachCompletion, expectMetadata } from "./testUtils.js"; + +describeForEachCompletion( + "Grammar Completion - Mixed Spacing Modes (Nested Rules)", + (matchGrammarCompletion) => { + // ================================================================ + // Section 1: Parent spacing=none, nested rule auto (default) + // + // The parent rule uses spacing=none, so the flex-space between + // the parent's first part ("hello") and the nested rule reference + // allows no separator. The nested rule's auto mode would + // normally require a separator between Latin words, but at the + // *leading edge* of the nested rule the parent's mode governs. + // ================================================================ + + describe("parent spacing=none, nested auto", () => { + const g = ` + = world -> "w"; + [spacing=none] = hello $(x:) -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("offers nested rule's keyword with separatorMode reflecting parent's none mode", () => { + const result = matchGrammarCompletion(grammar, "hello"); + // "hello" fully matched. Next part is the nested rule + // whose first part is "world". The separator + // between "hello" (last char 'o') and "world" (first + // char 'w') is governed by the parent's spacing=none → + // separatorMode should be "none". + expectMetadata(result, { + completions: ["world"], + matchedPrefixLength: 5, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + + it("matches partial keyword without separator", () => { + const result = matchGrammarCompletion(grammar, "hellow"); + // "hellow" → "hello" matched, "w" partially matches "world" + // in the nested rule. With spacing=none, no separator is + // needed — the partial match succeeds. + expectMetadata(result, { + completions: ["world"], + matchedPrefixLength: 5, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + + it("still offers completion despite spurious separator (matchedPrefixLength tells caller where valid input ends)", () => { + // "hello world" — the space violates spacing=none, but the + // completion system still reports "world" because it reports + // completions from the grammar structure. matchedPrefixLength=5 + // tells the caller that only "hello" was validly consumed. + const result = matchGrammarCompletion(grammar, "hello world"); + expectMetadata(result, { + completions: ["world"], + matchedPrefixLength: 5, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + }); + + // ================================================================ + // Section 2: Parent auto (default), nested rule spacing=none + // + // The parent uses auto mode, so a separator is required between + // Latin words. The nested rule uses spacing=none for its own + // internal parts, but the leading separator into the nested rule + // is governed by the parent's auto mode. + // ================================================================ + + describe("parent auto, nested spacing=none", () => { + const g = ` + [spacing=none] = ab cd -> "trackname"; + = play $(x:) -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("offers nested rule's first keyword after separator", () => { + const result = matchGrammarCompletion(grammar, "play "); + // "play " → "play" matched, trailing separator consumed. + // Next is whose first part is "ab". Leading + // separator mode = parent's auto mode (compiles to + // spacePunctuation; Latin→Latin requires space). + // But we already have a trailing separator. + expectMetadata(result, { + completions: ["ab"], + matchedPrefixLength: 4, + separatorMode: "spacePunctuation", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + + it("matches 'play ab' and offers second keyword in none mode", () => { + // "play ab" → "play" matched with auto mode, " ab" + // consumed via leading separator (parent auto allows + // [\s\p{P}]*? before "ab"). Inside , "ab" matched. + // Next part "cd" is an inter-word separator within the + // none-mode rule → separatorMode = "none". + const result = matchGrammarCompletion(grammar, "play ab"); + expectMetadata(result, { + completions: ["cd"], + matchedPrefixLength: 7, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + + it("exact match 'play abcd' backs up to last word (Category 1)", () => { + // "play abcd" → full match. Category 1 exact match backs + // up to the last matched part. The backup uses the nested + // rule's spacingMode (saved as matchedSpacingMode on + // lastMatchedPartInfo) so that the inter-word "none" mode is preserved across + // finalizeNestedRule's parent-state restoration. + const result = matchGrammarCompletion(grammar, "play abcd"); + expectMetadata(result, { + completions: ["cd"], + matchedPrefixLength: 7, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + }); + + // ================================================================ + // Section 3: Parent spacing=optional, nested spacing=required + // + // The nested rule requires spaces between its own parts, but the + // leading separator into the nested rule is governed by the + // parent's optional mode. Start has a leading keyword "do" so + // that backward completion always matches the full prefix (avoiding + // a pre-existing invariant #3 path-dependence when backward backs + // up across the nested rule boundary). + // ================================================================ + + describe("parent spacing=optional, nested spacing=required", () => { + // Inner has a single keyword so that backward completion + // always matches the full prefix (no mid-Inner word boundary + // to back up across, avoiding a pre-existing invariant #3 + // path-dependence at nested rule boundaries). + const g = ` + [spacing=required] = hello -> "x"; + [spacing=optional] = do $(x:) end -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("offers nested rule's first keyword with optional separator", () => { + // "do" matched in Start. Next part is at + // partIndex 1; the leading separator into Inner uses + // the parent's optional mode. + const result = matchGrammarCompletion(grammar, "do"); + expectMetadata(result, { + completions: ["hello"], + matchedPrefixLength: 2, + separatorMode: "optional", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + + it("after nested rule, separator to parent's next part uses parent's optional mode", () => { + // "do hello" completes . Next part "end" + // is in with spacing=optional → separator + // between "hello" (Latin) and "end" (Latin): optional + // mode says separator not required. + const result = matchGrammarCompletion(grammar, "do hello"); + expectMetadata(result, { + completions: ["end"], + matchedPrefixLength: 8, + separatorMode: "optional", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + }); + + // ================================================================ + // Section 4: Deep pass-through chain + // + // Mirrors the patterns from grammarMatcherSpacingNested.spec.ts: + // grandparent optional, parent required (pass-through), grandchild optional. + // The separator after the pass-through chain should be governed + // by the grandparent's optional mode. + // ================================================================ + + describe("deep pass-through: grandparent optional, parent required, grandchild optional", () => { + const g = ` + [spacing=optional] = bar -> true; + [spacing=required] = $(x:) -> x; + [spacing=optional] = $(x:) baz -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("completion after pass-through uses grandparent's optional mode", () => { + // "bar" → matched by . is a pass-through. + // Next part "baz" is in (spacing=optional). + // The separator between "bar" and "baz" should use + // the grandparent's optional mode (not Middle's required). + const result = matchGrammarCompletion(grammar, "bar"); + expectMetadata(result, { + completions: ["baz"], + matchedPrefixLength: 3, + separatorMode: "optional", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + + it("matches without separator (grandparent optional allows it)", () => { + const result = matchGrammarCompletion(grammar, "barbaz"); + // "barbaz" → exact match (optional mode allows no separator). + expectMetadata(result, { + completions: ["baz"], + matchedPrefixLength: 3, + separatorMode: "optional", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + }); + + describe("deep pass-through: grandparent required, parent optional, grandchild optional", () => { + const g = ` + [spacing=optional] = bar -> true; + [spacing=optional] = $(x:) -> x; + [spacing=required] = $(x:) baz -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("completion after pass-through uses grandparent's required mode", () => { + const result = matchGrammarCompletion(grammar, "bar"); + expectMetadata(result, { + completions: ["baz"], + matchedPrefixLength: 3, + separatorMode: "spacePunctuation", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + }); + + // ================================================================ + // Section 5: 4 levels deep + // ================================================================ + + describe("4 levels: great-grandparent optional, two required pass-through, leaf optional", () => { + const g = ` + [spacing=optional] = bar -> true; + [spacing=required] = $(x:) -> x; + [spacing=required] = $(x:) -> x; + [spacing=optional] = $(x:) baz -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("completion uses great-grandparent's optional mode past two pass-through levels", () => { + const result = matchGrammarCompletion(grammar, "bar"); + expectMetadata(result, { + completions: ["baz"], + matchedPrefixLength: 3, + separatorMode: "optional", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + }); + + // ================================================================ + // Section 6: Property completion at nested rule boundary + // + // When a nested rule's first part is a wildcard, the property + // completion's separatorMode should reflect the parent's mode. + // ================================================================ + + describe("property completion uses parent's spacing mode at nested boundary", () => { + // Use matching spacing modes (both none) so backward's + // path-dependent spacingMode matches forward's, avoiding + // a pre-existing invariant #3 violation. + const g = ` + [spacing=none] = $(name:wildcard) done -> { name }; + [spacing=none] = go $(x:) -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("property completion uses parent's none mode", () => { + const result = matchGrammarCompletion(grammar, "go"); + // "go" matched. Next part is whose first part + // is a wildcard. The separator is governed by parent's + // spacing=none → separatorMode should be "none". + expectMetadata(result, { + completions: [], + matchedPrefixLength: 2, + separatorMode: "none", + closedSet: false, + directionSensitive: true, + afterWildcard: "none", + properties: [ + { + match: {}, + propertyNames: ["name"], + }, + ], + }); + }); + }); + + // ================================================================ + // Section 7: Backward completion at nested boundary + // ================================================================ + + describe("backward completion at nested boundary respects parent mode", () => { + const g = ` + = world -> "w"; + [spacing=none] = hello $(x:) -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("backward from 'hellow' uses parent's none mode", () => { + const result = matchGrammarCompletion( + grammar, + "hellow", + undefined, + "backward", + ); + expectMetadata(result, { + completions: ["world"], + matchedPrefixLength: 5, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + }); + + // ================================================================ + // Section 8: Mixed modes with alternation + // + // Two alternatives with different spacing modes should each + // produce completions with their own correct separatorMode. + // ================================================================ + + describe("alternation with different spacing modes", () => { + const g = ` + [spacing=required] = hello world -> "required"; + [spacing=optional] = hello world -> "optional"; + = $(x:) -> x | $(x:) -> x; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("offers 'world' with merged separator mode from both alternatives", () => { + const result = matchGrammarCompletion(grammar, "hello"); + // Both alternatives match "hello" and offer "world". + // Required mode produces spacePunctuation; optional + // produces optional. Merge: spacePunctuation wins. + expectMetadata(result, { + completions: ["world"], + matchedPrefixLength: 5, + separatorMode: "spacePunctuation", + closedSet: true, + directionSensitive: true, + afterWildcard: "none", + properties: [], + }); + }); + }); + + // ================================================================ + // Section 9: Wildcard + keyword in spacing=none + // + // matchKeywordWordsFrom calls matchWordsGreedily without + // suppressLeadingSeparator. When the rule uses spacing=none, + // the k=0 branch falls through to the optional leading + // separator regex ([\\s\\p{P}]*?). Because the quantifier is + // lazy, it matches zero-width when the keyword immediately + // follows — so for callers that don't set + // suppressLeadingSeparator, behavior is equivalent to no + // separator in practice. + // ================================================================ + + describe("wildcard + keyword in spacing=none", () => { + const g = ` + [spacing=none] = $(x:wildcard) done -> { x }; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("offers 'done' after wildcard input", () => { + const result = matchGrammarCompletion(grammar, "abc"); + // "abc" is consumed by the wildcard. "done" is the + // next keyword. In spacing=none, the keyword must + // abut the wildcard content with no separator. + expectMetadata(result, { + completions: ["done"], + matchedPrefixLength: 3, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "all", + properties: [], + }); + }); + + it("partial keyword abutting wildcard matches in none mode", () => { + // "abcdo" → wildcard absorbs "abc", "do" partially + // matches "done" with no separator (spacing=none). + const result = matchGrammarCompletion(grammar, "abcdo"); + expectMetadata(result, { + completions: ["done"], + matchedPrefixLength: 3, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "all", + properties: [], + }); + }); + + it("wildcard absorbs spurious space in none mode (space not a separator)", () => { + // "abc do" — in spacing=none, the space is part of the + // wildcard content. The wildcard scanning still finds + // "do" as a partial match of "done". + // matchedPrefixLength=3 reflects the wildcard end before + // the keyword candidate start. + const result = matchGrammarCompletion(grammar, "abc do"); + expectMetadata(result, { + completions: ["done"], + matchedPrefixLength: 3, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "all", + properties: [], + }); + }); + }); + + // ================================================================ + // Section 10: Deferred EOI/wildcard-string path with mixed modes + // + // When a wildcard is followed by a string part in a nested rule + // with mixed spacing modes, the deferred wildcard-string + // candidate path (post-processing) should still work correctly. + // ================================================================ + + describe("wildcard + string in nested rule with mixed spacing modes", () => { + // Use matching spacing modes (both none) so backward's + // path-dependent spacingMode matches forward's. + const g = ` + [spacing=none] = $(x:wildcard) done -> { x }; + = play $(y:) -> y; + `; + const grammar = loadGrammarRules("test.grammar", g); + + it("offers 'done' after wildcard in nested none-mode rule", () => { + // "play something" → "play" matched in (auto), + // then " something" enters . The wildcard + // absorbs "something", and "done" is the next keyword. + const result = matchGrammarCompletion( + grammar, + "play something", + ); + expectMetadata(result, { + completions: ["done"], + matchedPrefixLength: 14, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "all", + properties: [], + }); + }); + + it("partial keyword in nested none-mode rule after wildcard", () => { + // "play somethingdo" → wildcard absorbs "something", + // "do" partially matches "done" (no separator, none mode). + const result = matchGrammarCompletion( + grammar, + "play somethingdo", + ); + expectMetadata(result, { + completions: ["done"], + matchedPrefixLength: 14, + separatorMode: "none", + closedSet: true, + directionSensitive: true, + afterWildcard: "all", + properties: [], + }); + }); + }); + }, +); diff --git a/ts/packages/actionGrammar/test/grammarMatcherGroups.spec.ts b/ts/packages/actionGrammar/test/grammarMatcherGroups.spec.ts index c5f1b7096b..d956af3a45 100644 --- a/ts/packages/actionGrammar/test/grammarMatcherGroups.spec.ts +++ b/ts/packages/actionGrammar/test/grammarMatcherGroups.spec.ts @@ -95,5 +95,145 @@ describeForEachMatcher( ]); }); }); + + describe("Wildcard leaking into captured nested rule", () => { + it("wildcard sibling does not prevent default value in following captured rule", () => { + // Regression: when the wildcard alternative $(wc)->wc in + // is explored, the pending wildcard leaks into + // . matchStringPartWithWildcard must assign the + // default string value for single-part rules (just like the + // non-wildcard path) to avoid "No value assign to variable". + const g = ` + = $(v0:) $(v1:) + -> { genre: v0, suffix: v1 }; + = rock -> "rock" + | pop -> "pop" + | $(wc) -> wc; + = tunes; + `; + const grammar = loadGrammarRules("test.grammar", g); + + // Literal genre — may match via both literal and wildcard + // paths; verify at least one result is correct. + const rockResults = testMatchGrammar(grammar, "rock tunes"); + expect(rockResults).toContainEqual({ + genre: "rock", + suffix: "tunes", + }); + + // Unknown genre — wildcard path, wc captures "metal", + // then must still produce its default value. + expect(testMatchGrammar(grammar, "metal tunes")).toStrictEqual([ + { genre: "metal", suffix: "tunes" }, + ]); + }); + + it("wildcard with preceding literal and trailing captured rule", () => { + // Same pattern but with a non-captured literal part before + // the wildcard rule, matching the exportGrammar output shape. + const g = ` + = play $(v0:) $(v1:) + -> { genre: v0, suffix: v1 }; + = rock -> "rock" + | $(wc) -> wc; + = tunes; + `; + const grammar = loadGrammarRules("test.grammar", g); + + const rockResults = testMatchGrammar( + grammar, + "play rock tunes", + ); + expect(rockResults).toContainEqual({ + genre: "rock", + suffix: "tunes", + }); + + expect( + testMatchGrammar(grammar, "play metal tunes"), + ).toStrictEqual([{ genre: "metal", suffix: "tunes" }]); + }); + }); + + describe("Default value for single-part captured sub-rules", () => { + it("single string part produces default value (non-wildcard path)", () => { + // = play; is captured as $(v:) — no wildcard + // anywhere, so the non-wildcard path in + // matchStringPartWithoutWildcard must assign the default. + const g = ` + = $(v:) -> v; + = play; + `; + const grammar = loadGrammarRules("test.grammar", g); + expect(testMatchGrammar(grammar, "play")).toStrictEqual([ + "play", + ]); + }); + + it("single string part with multiple alternatives (non-wildcard)", () => { + const g = ` + = $(v:) -> v; + = play | pause | stop; + `; + const grammar = loadGrammarRules("test.grammar", g); + expect(testMatchGrammar(grammar, "play")).toStrictEqual([ + "play", + ]); + expect(testMatchGrammar(grammar, "pause")).toStrictEqual([ + "pause", + ]); + expect(testMatchGrammar(grammar, "stop")).toStrictEqual([ + "stop", + ]); + }); + + it("single number part produces default value (non-wildcard path)", () => { + // has a single $(n:number) part and no explicit value. + // The number variable capture provides the rule's value. + const g = ` + = set $(v:) -> v; + = $(n:number); + `; + const grammar = loadGrammarRules("test.grammar", g); + expect(testMatchGrammar(grammar, "set 42")).toStrictEqual([42]); + }); + + it("single wildcard part produces default value (no explicit ->)", () => { + // has a single $(wc) part and no explicit value. + // The wildcard variable capture provides the rule's value. + const g = ` + = find $(v:) -> v; + = $(wc); + `; + const grammar = loadGrammarRules("test.grammar", g); + expect( + testMatchGrammar(grammar, "find something"), + ).toStrictEqual(["something"]); + }); + + it("single number part in captured sub-rule with wildcard sibling", () => { + // Ensures the wildcard path for number parts also works + // when a pending wildcard leaks from a sibling rule. + const g = ` + = $(v0: