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 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/AppCommandHandlerTests.cs b/dotnet/autoShell.Tests/AppCommandHandlerTests.cs new file mode 100644 index 0000000000..2561f3f9ad --- /dev/null +++ b/dotnet/autoShell.Tests/AppCommandHandlerTests.cs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using autoShell.Handlers; +using autoShell.Logging; +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 Mock _windowMock = new(); + private readonly Mock _loggerMock = new(); + private readonly AppCommandHandler _handler; + + public AppCommandHandlerTests() + { + _handler = new AppCommandHandler(_appRegistryMock.Object, _processMock.Object, _windowMock.Object, _loggerMock.Object); + } + + // --- LaunchProgram --- + + /// + /// Verifies that launching a non-running app starts it using its executable path. + /// + [Fact] + public void LaunchProgram_AppNotRunning_StartsViaPath() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("chrome")).Returns("chrome"); + _processMock.Setup(p => p.GetProcessesByName("chrome")).Returns([]); + _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); + } + + /// + /// Verifies that launching an app with a configured working directory env var sets the working directory. + /// + [Fact] + public void LaunchProgram_WithWorkingDir_SetsWorkingDirectory() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("github copilot")).Returns("github copilot"); + _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"); + + Handle("LaunchProgram", "github copilot"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.WorkingDirectory != "")), Times.Once); + } + + /// + /// Verifies that launching an app with configured arguments passes them to the process start info. + /// + [Fact] + public void LaunchProgram_WithArguments_SetsArguments() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("github copilot")).Returns("github copilot"); + _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"); + + Handle("LaunchProgram", "github copilot"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.Arguments == "--allow-all-tools")), Times.Once); + } + + /// + /// Verifies that when no executable path is available, the app is launched via its AppUserModelId through explorer.exe. + /// + [Fact] + public void LaunchProgram_NoPath_UsesAppUserModelId() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("calculator")).Returns("calculator"); + _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"); + + Handle("LaunchProgram", "calculator"); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.FileName == "explorer.exe")), Times.Once); + } + + /// + /// Verifies that closing a program resolves its process name and looks up running processes. + /// Note: the actual CloseMainWindow() call path cannot be unit-tested because + /// Process.MainWindowHandle is not virtual and cannot be mocked. + /// + [Fact] + public void CloseProgram_ResolvesProcessNameAndLooksUpProcesses() + { + _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([]); + + Handle("CloseProgram", "notepad"); + + _processMock.Verify(p => p.GetProcessesByName("notepad"), Times.Once); + } + + /// + /// Verifies that closing a program that is not running does not throw an exception. + /// + [Fact] + public void CloseProgram_NotRunning_DoesNothing() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); + _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([]); + + var ex = Record.Exception(() => Handle("CloseProgram", "notepad")); + + Assert.Null(ex); + } + + /// + /// Verifies that the ListAppNames command invokes GetAllAppNames on the app registry. + /// + [Fact] + public void ListAppNames_CallsGetAllAppNames() + { + _appRegistryMock.Setup(a => a.GetAllAppNames()).Returns(["notepad", "chrome"]); + + Handle("ListAppNames", ""); + + _appRegistryMock.Verify(a => a.GetAllAppNames(), Times.Once); + } + + /// + /// Verifies that launching an already-running app raises its window instead of starting a new process. + /// + [Fact] + public void LaunchProgram_AlreadyRunning_RaisesWindow() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); + _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([Process.GetCurrentProcess()]); + _appRegistryMock.Setup(a => a.GetExecutablePath("notepad")).Returns("notepad.exe"); + + Handle("LaunchProgram", "notepad"); + + _windowMock.Verify(w => w.RaiseWindow("notepad", "notepad.exe"), Times.Once); + _processMock.Verify(p => p.Start(It.IsAny()), Times.Never); + } + + /// + /// Verifies that a Win32Exception on first launch attempt triggers a fallback retry using the friendly name. + /// + [Fact] + public void LaunchProgram_Win32Exception_FallsBackToFriendlyName() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("myapp")).Returns("myapp"); + _processMock.Setup(p => p.GetProcessesByName("myapp")).Returns([]); + _appRegistryMock.Setup(a => a.GetExecutablePath("myapp")).Returns("myapp.exe"); + _processMock.SetupSequence(p => p.Start(It.IsAny())) + .Throws(new System.ComponentModel.Win32Exception("not found")) + .Returns(Process.GetCurrentProcess()); + + Handle("LaunchProgram", "myapp"); + + _processMock.Verify(p => p.Start(It.IsAny()), Times.Exactly(2)); + } + + /// + /// Verifies that launching an app with no path and no AppUserModelId does not start any process. + /// + [Fact] + public void LaunchProgram_NoPathNoAppModelId_DoesNothing() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("unknown")).Returns("unknown"); + _processMock.Setup(p => p.GetProcessesByName("unknown")).Returns([]); + _appRegistryMock.Setup(a => a.GetExecutablePath("unknown")).Returns((string)null!); + _appRegistryMock.Setup(a => a.GetAppUserModelId("unknown")).Returns((string)null!); + + Handle("LaunchProgram", "unknown"); + + _processMock.Verify(p => p.Start(It.IsAny()), Times.Never); + } + + private void Handle(string key, string value) + { + _handler.Handle(key, value, JToken.FromObject(value)); + } +} diff --git a/dotnet/autoShell.Tests/AudioCommandHandlerTests.cs b/dotnet/autoShell.Tests/AudioCommandHandlerTests.cs new file mode 100644 index 0000000000..d6747137ea --- /dev/null +++ b/dotnet/autoShell.Tests/AudioCommandHandlerTests.cs @@ -0,0 +1,177 @@ +// 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); + } + + /// + /// Verifies that the handler exposes exactly the Volume, Mute, and RestoreVolume commands. + /// + [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 --- + + /// + /// Verifies that valid integer percentage values are forwarded to SetVolume. + /// + [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); + } + + /// + /// Verifies that setting volume reads and saves the current level before applying the new one. + /// + [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); + } + + /// + /// Verifies that non-integer input does not trigger a SetVolume call. + /// + [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 --- + + /// + /// Verifies that RestoreVolume restores the volume to the level saved before the last change. + /// + [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); + } + + /// + /// Verifies that RestoreVolume defaults to zero when no prior volume change has been recorded. + /// + [Fact] + public void RestoreVolume_WithoutPriorChange_RestoresZero() + { + Handle("RestoreVolume", ""); + + _audioMock.Verify(a => a.SetVolume(0), Times.Once); + } + + /// + /// Verifies that RestoreVolume uses the actual saved volume, not a hardcoded value, + /// by using a different initial volume than the other RestoreVolume test. + /// + [Fact] + public void RestoreVolume_DifferentInitialVolume_RestoresSavedLevel() + { + _audioMock.Setup(a => a.GetVolume()).Returns(30); + + Handle("Volume", "80"); + _audioMock.Invocations.Clear(); + + Handle("RestoreVolume", ""); + + _audioMock.Verify(a => a.SetVolume(30), Times.Once); + } + + // --- Mute --- + + /// + /// Verifies that valid boolean string values are forwarded to SetMute. + /// + [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); + } + + /// + /// Verifies that non-boolean input does not trigger a SetMute call. + /// + [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 --- + + /// + /// Verifies that an unknown command key does not invoke any audio service methods. + /// + [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/AutoShellProcess.cs b/dotnet/autoShell.Tests/AutoShellProcess.cs new file mode 100644 index 0000000000..00b1ac9dc3 --- /dev/null +++ b/dotnet/autoShell.Tests/AutoShellProcess.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace autoShell.Tests; + +/// +/// Manages an autoShell.exe child process with redirected stdin/stdout +/// for end-to-end testing of the JSON command protocol. +/// +internal sealed class AutoShellProcess : IDisposable +{ + private static readonly string s_exePath = Path.Combine(AppContext.BaseDirectory, "autoShell.exe"); + + private readonly Process _process; + + private AutoShellProcess(Process process) + { + _process = process; + } + + /// + /// Starts autoShell.exe in interactive (stdin) mode with redirected I/O. + /// + public static AutoShellProcess StartInteractive() + { + var psi = new ProcessStartInfo + { + FileName = s_exePath, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + var process = Process.Start(psi) + ?? throw new InvalidOperationException("Failed to start autoShell.exe"); + + return new AutoShellProcess(process); + } + + /// + /// Starts autoShell.exe with command-line arguments (non-interactive mode). + /// Returns stdout content and exit code after the process completes. + /// + public static (string Output, int ExitCode) RunWithArgs(string args, int timeoutMs = 10000) + { + var psi = new ProcessStartInfo + { + FileName = s_exePath, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(psi)!; + string output = process.StandardOutput.ReadToEnd(); + bool exited = process.WaitForExit(timeoutMs); + + if (!exited) + { + process.Kill(); + throw new TimeoutException($"autoShell.exe did not exit within {timeoutMs}ms"); + } + + return (output, process.ExitCode); + } + + /// + /// Sends a JSON command string to stdin, terminated with \r\n. + /// + public void SendCommand(string json) + { + _process.StandardInput.WriteLine(json); + _process.StandardInput.Flush(); + } + + /// + /// Reads a single line of stdout with a timeout. + /// Returns null if the timeout expires before a line is available. + /// + public async Task ReadLineAsync(int timeoutMs = 5000) + { + using var cts = new CancellationTokenSource(timeoutMs); + try + { + var lineTask = _process.StandardOutput.ReadLineAsync(); + var completedTask = await Task.WhenAny(lineTask, Task.Delay(timeoutMs, cts.Token)); + if (completedTask == lineTask) + { + cts.Cancel(); + return await lineTask; + } + return null; + } + catch (OperationCanceledException) + { + return null; + } + } + + /// + /// Sends {"quit":""} and waits for the process to exit. + /// + public void SendQuit(int timeoutMs = 5000) + { + SendCommand("""{"quit":""}"""); + _process.WaitForExit(timeoutMs); + } + + /// + /// Returns true if the process has exited. + /// + public bool HasExited => _process.HasExited; + + /// + /// Closes the stdin stream (sends EOF). + /// + public void CloseStdin() + { + _process.StandardInput.Close(); + } + + /// + /// Waits for the process to exit within the given timeout. + /// + public bool WaitForExit(int timeoutMs) + { + return _process.WaitForExit(timeoutMs); + } + + public void Dispose() + { + try + { + if (!_process.HasExited) + { + _process.Kill(); + _process.WaitForExit(3000); + } + } + catch { } + _process.Dispose(); + } +} diff --git a/dotnet/autoShell.Tests/CommandDispatcherIntegrationTests.cs b/dotnet/autoShell.Tests/CommandDispatcherIntegrationTests.cs new file mode 100644 index 0000000000..167f599e6e --- /dev/null +++ b/dotnet/autoShell.Tests/CommandDispatcherIntegrationTests.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Logging; +using autoShell.Services; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +/// +/// Integration tests that exercise the full Create() → Dispatch() → handler → service pipeline +/// using mock services. These verify that CommandDispatcher wiring is correct. +/// +public class CommandDispatcherIntegrationTests +{ + private readonly Mock _registryMock = new(); + private readonly Mock _systemParamsMock = new(); + private readonly Mock _processMock = new(); + private readonly Mock _audioMock = new(); + private readonly Mock _appRegistryMock = new(); + private readonly Mock _debuggerMock = new(); + private readonly Mock _brightnessMock = new(); + private readonly Mock _displayMock = new(); + private readonly Mock _windowMock = new(); + private readonly Mock _networkMock = new(); + private readonly Mock _virtualDesktopMock = new(); + private readonly Mock _loggerMock = new(); + private readonly CommandDispatcher _dispatcher; + + public CommandDispatcherIntegrationTests() + { + _dispatcher = CommandDispatcher.Create( + _loggerMock.Object, + _registryMock.Object, + _systemParamsMock.Object, + _processMock.Object, + _audioMock.Object, + _appRegistryMock.Object, + _debuggerMock.Object, + _brightnessMock.Object, + _displayMock.Object, + _windowMock.Object, + _networkMock.Object, + _virtualDesktopMock.Object); + } + + /// + /// Verifies that a Volume command dispatched through Create() reaches the audio service. + /// + [Fact] + public void Dispatch_Volume_ReachesAudioService() + { + _audioMock.Setup(a => a.GetVolume()).Returns(50); + + Dispatch("""{"Volume": "75"}"""); + + _audioMock.Verify(a => a.SetVolume(75), Times.Once); + } + + /// + /// Verifies that a Mute command dispatched through Create() reaches the audio service. + /// + [Fact] + public void Dispatch_Mute_ReachesAudioService() + { + Dispatch("""{"Mute": "true"}"""); + + _audioMock.Verify(a => a.SetMute(true), Times.Once); + } + + /// + /// Verifies that a LaunchProgram command dispatched through Create() reaches the process service. + /// + [Fact] + public void Dispatch_LaunchProgram_ReachesProcessService() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); + _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([]); + _appRegistryMock.Setup(a => a.GetExecutablePath("notepad")).Returns("notepad.exe"); + + Dispatch("""{"LaunchProgram": "notepad"}"""); + + _processMock.Verify(p => p.Start(It.IsAny()), Times.Once); + } + + /// + /// Verifies that a SetWallpaper command dispatched through Create() reaches the system parameters service. + /// + [Fact] + public void Dispatch_SetWallpaper_ReachesSystemParamsService() + { + Dispatch("""{"SetWallpaper": "C:\\wallpaper.jpg"}"""); + + _systemParamsMock.Verify(s => s.SetParameter(0x0014, 0, @"C:\wallpaper.jpg", 3), Times.Once); + } + + /// + /// Verifies that a ConnectWifi command dispatched through Create() reaches the network service. + /// + [Fact] + public void Dispatch_ConnectWifi_ReachesNetworkService() + { + Dispatch("""{"ConnectWifi": "{\"ssid\": \"MyNetwork\", \"password\": \"pass123\"}"}"""); + + _networkMock.Verify(n => n.ConnectToWifi(It.IsAny(), It.IsAny()), Times.Once); + } + + /// + /// Verifies that a NextDesktop command dispatched through Create() reaches the virtual desktop service. + /// + [Fact] + public void Dispatch_NextDesktop_ReachesVirtualDesktopService() + { + Dispatch("""{"NextDesktop": ""}"""); + + _virtualDesktopMock.Verify(v => v.NextDesktop(), Times.Once); + } + + /// + /// Verifies that a SetThemeMode command dispatched through Create() reaches the registry service. + /// + [Fact] + public void Dispatch_SetThemeMode_ReachesRegistryService() + { + Dispatch("""{"SetThemeMode": "dark"}"""); + + _registryMock.Verify(r => r.SetValue( + It.IsAny(), "AppsUseLightTheme", 0, It.IsAny()), Times.Once); + } + + /// + /// Verifies that an unknown command does not throw and logs a debug message. + /// + [Fact] + public void Dispatch_UnknownCommand_DoesNotThrow() + { + var ex = Record.Exception(() => Dispatch("""{"NonExistentCommand": "value"}""")); + + Assert.Null(ex); + } + + /// + /// Verifies that multiple commands in a single JSON object are all dispatched. + /// + [Fact] + public void Dispatch_MultipleCommands_AllReachServices() + { + _audioMock.Setup(a => a.GetVolume()).Returns(50); + + Dispatch("""{"Volume": "80", "Mute": "false"}"""); + + _audioMock.Verify(a => a.SetVolume(80), Times.Once); + _audioMock.Verify(a => a.SetMute(false), Times.Once); + } + + /// + /// Verifies that quit stops processing and returns true. + /// + [Fact] + public void Dispatch_Quit_ReturnsTrue() + { + bool result = _dispatcher.Dispatch(JObject.Parse("""{"quit": ""}""")); + + Assert.True(result); + } + + private void Dispatch(string json) + { + _dispatcher.Dispatch(JObject.Parse(json)); + } +} diff --git a/dotnet/autoShell.Tests/CommandDispatcherTests.cs b/dotnet/autoShell.Tests/CommandDispatcherTests.cs new file mode 100644 index 0000000000..1265b6f87d --- /dev/null +++ b/dotnet/autoShell.Tests/CommandDispatcherTests.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Handlers; +using autoShell.Logging; +using autoShell.Services; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +public class CommandDispatcherTests +{ + private readonly Mock _loggerMock = new(); + private readonly CommandDispatcher _dispatcher; + + public CommandDispatcherTests() + { + _dispatcher = new CommandDispatcher(_loggerMock.Object); + } + + /// + /// Verifies that dispatching a JSON object with a "quit" key returns true. + /// + [Fact] + public void Dispatch_QuitKey_ReturnsTrue() + { + var json = JObject.Parse("""{"quit": true}"""); + bool result = _dispatcher.Dispatch(json); + Assert.True(result); + } + + /// + /// Verifies that dispatching a non-quit command returns false. + /// + [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); + } + + /// + /// Verifies that commands are routed to the correct handler with the expected key and value. + /// + [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); + } + + /// + /// Verifies that dispatching an unrecognized command does not throw an exception. + /// + [Fact] + public void Dispatch_UnknownCommand_DoesNotThrow() + { + var json = JObject.Parse("""{"UnknownCmd": "value"}"""); + var ex = Record.Exception(() => _dispatcher.Dispatch(json)); + Assert.Null(ex); + } + + /// + /// Verifies that dispatching an empty JSON object returns false. + /// + [Fact] + public void Dispatch_EmptyObject_ReturnsFalse() + { + bool result = _dispatcher.Dispatch([]); + Assert.False(result); + } + + /// + /// Verifies that a "quit" key stops processing of any subsequent keys in the same JSON object. + /// + [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); + } + + /// + /// Verifies that an exception thrown by a handler does not propagate to the caller. + /// + [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); + } + + /// + /// Verifies that after a handler throws, subsequent keys in the same dispatch are still processed. + /// + [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/DisplayCommandHandlerTests.cs b/dotnet/autoShell.Tests/DisplayCommandHandlerTests.cs new file mode 100644 index 0000000000..f0d50065b2 --- /dev/null +++ b/dotnet/autoShell.Tests/DisplayCommandHandlerTests.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Handlers; +using autoShell.Logging; +using autoShell.Services; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +public class DisplayCommandHandlerTests +{ + private readonly Mock _displayMock = new(); + private readonly Mock _loggerMock = new(); + private readonly DisplayCommandHandler _handler; + + public DisplayCommandHandlerTests() + { + _handler = new DisplayCommandHandler(_displayMock.Object, _loggerMock.Object); + } + + /// + /// Verifies that the handler exposes exactly the ListResolutions, SetScreenResolution, and SetTextSize commands. + /// + [Fact] + public void SupportedCommands_ContainsExpectedCommands() + { + var commands = _handler.SupportedCommands.ToList(); + Assert.Contains("ListResolutions", commands); + Assert.Contains("SetScreenResolution", commands); + Assert.Contains("SetTextSize", commands); + Assert.Equal(3, commands.Count); + } + + // --- ListResolutions --- + + /// + /// Verifies that the ListResolutions command calls the display service to list available resolutions. + /// + [Fact] + public void ListResolutions_CallsService() + { + _displayMock.Setup(d => d.ListResolutions()).Returns("[{\"Width\":1920}]"); + + Handle("ListResolutions", ""); + + _displayMock.Verify(d => d.ListResolutions(), Times.Once); + } + + // --- SetScreenResolution --- + + /// + /// Verifies that a "WIDTHxHEIGHT" string is parsed and forwarded to the display service. + /// + [Fact] + public void SetScreenResolution_StringFormat_CallsServiceWithParsedValues() + { + _displayMock.Setup(d => d.SetResolution(1920, 1080, null)).Returns("ok"); + + Handle("SetScreenResolution", "1920x1080"); + + _displayMock.Verify(d => d.SetResolution(1920, 1080, null), Times.Once); + } + + /// + /// Verifies that a "WIDTHxHEIGHT@RATE" string includes the refresh rate in the service call. + /// + [Fact] + public void SetScreenResolution_WithRefreshRate_CallsServiceWithRefresh() + { + _displayMock.Setup(d => d.SetResolution(1920, 1080, (uint)60)).Returns("ok"); + + Handle("SetScreenResolution", "1920x1080@60"); + + _displayMock.Verify(d => d.SetResolution(1920, 1080, (uint)60), Times.Once); + } + + /// + /// Verifies that a JSON object with width and height properties is forwarded to the display service. + /// + [Fact] + public void SetScreenResolution_ObjectFormat_CallsServiceWithValues() + { + _displayMock.Setup(d => d.SetResolution(2560, 1440, null)).Returns("ok"); + + var rawValue = JObject.FromObject(new { width = 2560, height = 1440 }); + _handler.Handle("SetScreenResolution", "", rawValue); + + _displayMock.Verify(d => d.SetResolution(2560, 1440, null), Times.Once); + } + + /// + /// Verifies that an invalid resolution string does not invoke the display service. + /// + [Fact] + public void SetScreenResolution_InvalidFormat_DoesNotCallService() + { + Handle("SetScreenResolution", "invalid"); + + _displayMock.Verify(d => d.SetResolution(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + // --- SetTextSize --- + + /// + /// Verifies that a valid integer text size percentage is forwarded to the display service. + /// + [Fact] + public void SetTextSize_ValidPercent_CallsService() + { + Handle("SetTextSize", "150"); + + _displayMock.Verify(d => d.SetTextSize(150), Times.Once); + } + + /// + /// Verifies that non-numeric text size input does not invoke the display service. + /// + [Fact] + public void SetTextSize_InvalidInput_DoesNotCallService() + { + Handle("SetTextSize", "abc"); + + _displayMock.Verify(d => d.SetTextSize(It.IsAny()), Times.Never); + } + + // --- Unknown key --- + + /// + /// Verifies that an unknown command key does not invoke any display service methods. + /// + [Fact] + public void Handle_UnknownKey_DoesNothing() + { + Handle("UnknownDisplayCmd", "value"); + + _displayMock.VerifyNoOtherCalls(); + } + + /// + /// Verifies that a JSON object with width, height, and refreshRate is forwarded to the display service. + /// + [Fact] + public void SetScreenResolution_ObjectFormatWithRefreshRate_CallsServiceWithRefresh() + { + _displayMock.Setup(d => d.SetResolution(2560, 1440, (uint)144)).Returns("ok"); + + var rawValue = JObject.FromObject(new { width = 2560, height = 1440, refreshRate = 144 }); + _handler.Handle("SetScreenResolution", "", rawValue); + + _displayMock.Verify(d => d.SetResolution(2560, 1440, (uint)144), Times.Once); + } + + private void Handle(string key, string value) + { + _handler.Handle(key, value, JToken.FromObject(value)); + } +} diff --git a/dotnet/autoShell.Tests/EndToEndTests.cs b/dotnet/autoShell.Tests/EndToEndTests.cs new file mode 100644 index 0000000000..ef6c5e4545 --- /dev/null +++ b/dotnet/autoShell.Tests/EndToEndTests.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +/// +/// End-to-end tests that launch autoShell.exe as a child process and communicate +/// via the stdin/stdout JSON protocol. Tests the full pipeline including process +/// startup, JSON parsing, command dispatch, and response serialization. +/// +[Trait("Category", "E2E")] +public sealed class EndToEndTests : IDisposable +{ + private readonly AutoShellProcess _process; + + public EndToEndTests() + { + _process = AutoShellProcess.StartInteractive(); + } + + public void Dispose() + { + _process.Dispose(); + } + + // --- Query commands (assert JSON stdout) --- + + /// + /// Verifies that ListAppNames returns a valid JSON array of app names via stdout. + /// + [Fact] + public async Task ListAppNames_ReturnsJsonArray() + { + _process.SendCommand("""{"ListAppNames":""}"""); + + string? response = await _process.ReadLineAsync(); + + Assert.NotNull(response); + var array = JArray.Parse(response); + Assert.NotEmpty(array); + } + + /// + /// Verifies that ListThemes returns a valid JSON array of theme file paths via stdout. + /// + [Fact] + public async Task ListThemes_ReturnsJsonArray() + { + _process.SendCommand("""{"ListThemes":""}"""); + + // Theme scanning involves disk I/O; allow extra time + string? response = await _process.ReadLineAsync(10000); + + Assert.NotNull(response); + var array = JArray.Parse(response); + Assert.NotEmpty(array); + } + + /// + /// Verifies that multiple sequential query commands each return a separate response. + /// + [Fact] + public async Task MultipleQueries_EachReturnsResponse() + { + _process.SendCommand("""{"ListAppNames":""}"""); + string? response1 = await _process.ReadLineAsync(); + + _process.SendCommand("""{"ListThemes":""}"""); + string? response2 = await _process.ReadLineAsync(); + + Assert.NotNull(response1); + Assert.NotNull(response2); + JArray.Parse(response1); + JArray.Parse(response2); + } + + /// + /// Verifies that ListResolutions returns a valid JSON string via stdout. + /// + [Fact] + public async Task ListResolutions_ReturnsResponse() + { + _process.SendCommand("""{"ListResolutions":""}"""); + + string? response = await _process.ReadLineAsync(10000); + + Assert.NotNull(response); + Assert.NotEmpty(response); + } + + /// + /// Verifies that ListWifiNetworks returns a response via stdout. + /// + [Fact] + public async Task ListWifiNetworks_ReturnsResponse() + { + _process.SendCommand("""{"ListWifiNetworks":""}"""); + + string? response = await _process.ReadLineAsync(10000); + + Assert.NotNull(response); + Assert.NotEmpty(response); + } + + /// + /// Verifies that SetScreenResolution with an invalid value produces a response without crashing. + /// Uses an intentionally invalid resolution to avoid changing the actual display. + /// + [Fact] + public async Task SetScreenResolution_InvalidValue_ReturnsResponse() + { + _process.SendCommand("""{"SetScreenResolution":"99999x99999"}"""); + + // May produce an error message or status — just verify the process survives + // and we can still send commands + await _process.ReadLineAsync(5000); + + _process.SendCommand("""{"ListAppNames":""}"""); + string? response = await _process.ReadLineAsync(); + + Assert.False(_process.HasExited); + Assert.NotNull(response); + } + + // --- Protocol edge cases --- + + /// + /// Verifies that multiple commands in a single JSON object each produce stdout output. + /// + [Fact] + public async Task MultiCommandObject_ProducesMultipleResponses() + { + _process.SendCommand("""{"ListAppNames":"", "ListThemes":""}"""); + + string? response1 = await _process.ReadLineAsync(); + string? response2 = await _process.ReadLineAsync(); + + Assert.NotNull(response1); + Assert.NotNull(response2); + // One should be app names, the other themes — both valid JSON arrays + JArray.Parse(response1); + JArray.Parse(response2); + } + + /// + /// Verifies that quit stops processing mid-batch. Commands after quit should not execute. + /// + [Fact] + public async Task Quit_StopsMidBatch() + { + // ListAppNames produces output, quit should stop before ListThemes runs + _process.SendCommand("""{"ListAppNames":"", "quit":"", "ListThemes":""}"""); + + string? response1 = await _process.ReadLineAsync(); + Assert.NotNull(response1); + JArray.Parse(response1); + + // Process should have exited — no second response + _process.WaitForExit(3000); + Assert.True(_process.HasExited); + } + + /// + /// Verifies that sending {"quit":""} causes the process to exit cleanly. + /// + [Fact] + public void Quit_ProcessExits() + { + _process.SendQuit(); + + Assert.True(_process.HasExited); + } + + /// + /// Verifies that malformed JSON does not crash the process. + /// Sends invalid JSON followed by a valid query to confirm the process is still alive. + /// + [Fact] + public async Task MalformedJson_ProcessSurvives() + { + _process.SendCommand("this is not json"); + + // Process should still be alive — send a valid command to verify + _process.SendCommand("""{"ListAppNames":""}"""); + string? response = await _process.ReadLineAsync(); + + Assert.False(_process.HasExited); + Assert.NotNull(response); + } + + /// + /// Verifies that an empty line does not crash the process. + /// The error message goes to stdout (known protocol limitation), so we + /// consume it before verifying the process is still responsive. + /// + [Fact] + public async Task EmptyLine_ProcessSurvives() + { + _process.SendCommand(""); + // Consume the error message that goes to stdout + await _process.ReadLineAsync(); + + _process.SendCommand("""{"ListAppNames":""}"""); + string? response = await _process.ReadLineAsync(); + + Assert.False(_process.HasExited); + Assert.NotNull(response); + } + + /// + /// Verifies that an unknown command does not crash the process. + /// + [Fact] + public async Task UnknownCommand_ProcessSurvives() + { + _process.SendCommand("""{"NonExistentCommand":"value"}"""); + + _process.SendCommand("""{"ListAppNames":""}"""); + string? response = await _process.ReadLineAsync(); + + Assert.False(_process.HasExited); + Assert.NotNull(response); + } + + /// + /// Verifies that closing stdin (EOF) causes the process to exit cleanly. + /// + [Fact] + public void StdinClosed_ProcessExits() + { + _process.CloseStdin(); + _process.WaitForExit(5000); + + Assert.True(_process.HasExited); + } + + // --- Command-line mode --- + + /// + /// Verifies that passing a single JSON command as a command-line argument + /// executes it and exits (non-interactive mode). + /// + [Fact] + public void CommandLineMode_SingleObject_ExecutesAndExits() + { + var (output, exitCode) = AutoShellProcess.RunWithArgs( + """{"ListAppNames":""}"""); + + Assert.Equal(0, exitCode); + Assert.NotEmpty(output); + JArray.Parse(output.Trim()); + } + + /// + /// Verifies that passing a JSON array of commands as command-line arguments + /// executes all of them and exits. + /// + [Fact] + public void CommandLineMode_JsonArray_ExecutesAllAndExits() + { + var (output, exitCode) = AutoShellProcess.RunWithArgs( + """[{"ListAppNames":""},{"ListThemes":""}]"""); + + Assert.Equal(0, exitCode); + Assert.NotEmpty(output); + } +} diff --git a/dotnet/autoShell.Tests/HandlerRegistrationTests.cs b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs new file mode 100644 index 0000000000..c58dcf6c88 --- /dev/null +++ b/dotnet/autoShell.Tests/HandlerRegistrationTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Handlers; +using autoShell.Handlers.Settings; + +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(); + var appRegistryMock = new Moq.Mock(); + var debuggerMock = new Moq.Mock(); + var brightnessMock = new Moq.Mock(); + var displayMock = new Moq.Mock(); + var windowMock = new Moq.Mock(); + var networkMock = new Moq.Mock(); + var virtualDesktopMock = new Moq.Mock(); + var loggerMock = new Moq.Mock(); + + _handlers = + [ + new AudioCommandHandler(audioMock.Object), + new AppCommandHandler(appRegistryMock.Object, processMock.Object, windowMock.Object, loggerMock.Object), + new WindowCommandHandler(appRegistryMock.Object, windowMock.Object), + new ThemeCommandHandler(registryMock.Object, processMock.Object, systemParamsMock.Object), + new VirtualDesktopCommandHandler(appRegistryMock.Object, windowMock.Object, virtualDesktopMock.Object, loggerMock.Object), + new NetworkCommandHandler(networkMock.Object, processMock.Object, loggerMock.Object), + new DisplayCommandHandler(displayMock.Object, loggerMock.Object), + new TaskbarSettingsHandler(registryMock.Object), + new DisplaySettingsHandler(registryMock.Object, processMock.Object, brightnessMock.Object, loggerMock.Object), + new PersonalizationSettingsHandler(registryMock.Object, processMock.Object), + new MouseSettingsHandler(systemParamsMock.Object, processMock.Object, loggerMock.Object), + new AccessibilitySettingsHandler(registryMock.Object, processMock.Object), + new PrivacySettingsHandler(registryMock.Object), + new PowerSettingsHandler(registryMock.Object, processMock.Object), + new FileExplorerSettingsHandler(registryMock.Object), + new SystemSettingsHandler(registryMock.Object, processMock.Object, loggerMock.Object), + new SystemCommandHandler(processMock.Object, debuggerMock.Object), + ]; + } + + /// + /// Verifies that every registered handler declares at least one supported command. + /// + [Fact] + public void AllHandlers_HaveNonEmptySupportedCommands() + { + foreach (var handler in _handlers) + { + Assert.NotEmpty(handler.SupportedCommands); + } + } + + /// + /// Verifies that no handler declares the same command key more than once. + /// + [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.Empty(duplicates); + } + } + + /// + /// Verifies that no command key is claimed by more than one handler. + /// + [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.Empty(duplicates); + } + + /// + /// Verifies that every supported command across all handlers has at least one corresponding unit test. + /// + [Fact] + public void AllCommands_HaveAtLeastOneUnitTest() + { + // 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) + { + untested.Add($"{handlerTypeName}.{command}"); + } + } + } + + Assert.Empty(untested); + } + +} diff --git a/dotnet/autoShell.Tests/NetworkCommandHandlerTests.cs b/dotnet/autoShell.Tests/NetworkCommandHandlerTests.cs new file mode 100644 index 0000000000..ca72f91b59 --- /dev/null +++ b/dotnet/autoShell.Tests/NetworkCommandHandlerTests.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Handlers; +using autoShell.Logging; +using autoShell.Services; +using Moq; +using Newtonsoft.Json.Linq; + +namespace autoShell.Tests; + +public class NetworkCommandHandlerTests +{ + private readonly Mock _networkMock = new(); + private readonly Mock _processMock = new(); + private readonly Mock _loggerMock = new(); + private readonly NetworkCommandHandler _handler; + + public NetworkCommandHandlerTests() + { + _handler = new NetworkCommandHandler(_networkMock.Object, _processMock.Object, _loggerMock.Object); + } + + // --- ConnectWifi --- + + /// + /// Verifies that ConnectWifi with an SSID and password calls the network service with both values. + /// + [Fact] + public void ConnectWifi_WithSsidAndPassword_CallsService() + { + var json = JToken.Parse("""{"ssid":"TestNetwork","password":"pass123"}"""); + _handler.Handle("ConnectWifi", json.ToString(), json); + + _networkMock.Verify(n => n.ConnectToWifi("TestNetwork", "pass123"), Times.Once); + } + + /// + /// Verifies that ConnectWifi without a password calls the service with an empty password string. + /// + [Fact] + public void ConnectWifi_WithoutPassword_CallsServiceWithEmptyPassword() + { + var json = JToken.Parse("""{"ssid":"OpenNetwork"}"""); + _handler.Handle("ConnectWifi", json.ToString(), json); + + _networkMock.Verify(n => n.ConnectToWifi("OpenNetwork", ""), Times.Once); + } + + // --- DisconnectWifi --- + + /// + /// Verifies that DisconnectWifi invokes the network service disconnect method. + /// + [Fact] + public void DisconnectWifi_CallsService() + { + var json = JToken.Parse("{}"); + _handler.Handle("DisconnectWifi", json.ToString(), json); + + _networkMock.Verify(n => n.DisconnectFromWifi(), Times.Once); + } + + // --- ListWifiNetworks --- + + /// + /// Verifies that ListWifiNetworks calls the network service and retrieves the result. + /// + [Fact] + public void ListWifiNetworks_CallsServiceAndWritesResult() + { + _networkMock.Setup(n => n.ListWifiNetworks()).Returns("[]"); + + var json = JToken.Parse("{}"); + _handler.Handle("ListWifiNetworks", json.ToString(), json); + + _networkMock.Verify(n => n.ListWifiNetworks(), Times.Once); + } + + // --- ToggleAirplaneMode --- + + /// + /// Verifies that valid boolean values are forwarded to SetAirplaneMode. + /// + [Theory] + [InlineData("true", true)] + [InlineData("false", false)] + public void ToggleAirplaneMode_ValidBool_CallsService(string input, bool expected) + { + var json = JToken.Parse(input); + _handler.Handle("ToggleAirplaneMode", input, json); + + _networkMock.Verify(n => n.SetAirplaneMode(expected), Times.Once); + } + + // --- BluetoothToggle --- + + /// + /// Verifies that BluetoothToggle calls ToggleBluetooth with the parsed enable value. + /// + [Fact] + public void BluetoothToggle_Enable_CallsToggleBluetooth() + { + var json = JToken.Parse("""{"enableBluetooth":true}"""); + _handler.Handle("BluetoothToggle", json.ToString(), json); + + _networkMock.Verify(n => n.ToggleBluetooth(true), Times.Once); + } + + /// + /// Verifies that BluetoothToggle defaults to true when the parameter is missing. + /// + [Fact] + public void BluetoothToggle_DefaultsToTrue() + { + var json = JToken.Parse("{}"); + _handler.Handle("BluetoothToggle", json.ToString(), json); + + _networkMock.Verify(n => n.ToggleBluetooth(true), Times.Once); + } + + // --- EnableWifi --- + + /// + /// Verifies that EnableWifi calls the network service with the parsed enable value. + /// + [Fact] + public void EnableWifi_Enable_CallsService() + { + var json = JToken.Parse("""{"enable":true}"""); + _handler.Handle("EnableWifi", json.ToString(), json); + + _networkMock.Verify(n => n.EnableWifi(true), Times.Once); + } + + /// + /// Verifies that EnableWifi with enable=false disables wifi. + /// + [Fact] + public void EnableWifi_Disable_CallsService() + { + var json = JToken.Parse("""{"enable":false}"""); + _handler.Handle("EnableWifi", json.ToString(), json); + + _networkMock.Verify(n => n.EnableWifi(false), Times.Once); + } + + // --- EnableMeteredConnections --- + + /// + /// Verifies that EnableMeteredConnections opens the network settings URI. + /// + [Fact] + public void EnableMeteredConnections_OpensSettingsUri() + { + _handler.Handle("EnableMeteredConnections", "true", JToken.FromObject("true")); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:network-status"), Times.Once); + } +} diff --git a/dotnet/autoShell.Tests/SettingsHandlerTests.cs b/dotnet/autoShell.Tests/SettingsHandlerTests.cs new file mode 100644 index 0000000000..b995d4a267 --- /dev/null +++ b/dotnet/autoShell.Tests/SettingsHandlerTests.cs @@ -0,0 +1,1125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using autoShell.Handlers.Settings; +using autoShell.Logging; +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); + } + + /// + /// Verifies that enabling title bar color sets ColorPrevalence to 1 in the DWM registry key. + /// + [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); + } + + /// + /// Verifies that disabling title bar color sets ColorPrevalence to 0 in the DWM registry key. + /// + [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); + } + + /// + /// Verifies that enabling transparency sets EnableTransparency to 1 in the Personalize registry key. + /// + [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); + } + + /// + /// Verifies that disabling transparency sets EnableTransparency to 0 in the Personalize registry key. + /// + [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); + } + + /// + /// Verifies that light system theme mode sets both app and system light-theme registry keys to 1. + /// + [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); + } + + /// + /// Verifies that dark system theme mode sets both app and system light-theme registry keys to 0. + /// + [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); + } + + /// + /// Verifies that the HighContrastTheme command opens the high-contrast accessibility settings page. + /// + [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); + } + + /// + /// Verifies that denying camera access writes "Deny" to the webcam consent store registry key. + /// + [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); + } + + /// + /// Verifies that allowing camera access writes "Allow" to the webcam consent store registry key. + /// + [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); + } + + /// + /// Verifies that denying microphone access writes "Deny" to the microphone consent store registry key. + /// + [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); + } + + /// + /// Verifies that allowing location access writes "Allow" to the location consent store registry key. + /// + [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); + } + + /// + /// Verifies that enabling file extensions sets HideFileExt to 0 in the Explorer Advanced registry key. + /// + [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); + } + + /// + /// Verifies that disabling file extensions sets HideFileExt to 1 in the Explorer Advanced registry key. + /// + [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); + } + + /// + /// Verifies that enabling hidden files sets Hidden to 1 and ShowSuperHidden to 1. + /// + [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); + } + + /// + /// Verifies that disabling hidden files sets Hidden to 2 and ShowSuperHidden to 0. + /// + [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); + } + + /// + /// Verifies that the battery saver activation level writes the threshold value to the registry. + /// + [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); + } + + /// + /// Verifies that SetPowerModeOnBattery opens the power and sleep settings page. + /// + [Fact] + public void SetPowerModeOnBattery_OpensSettings() + { + Handle("SetPowerModeOnBattery", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:powersleep"), Times.Once); + } + + /// + /// Verifies that SetPowerModePluggedIn opens the power and sleep settings page. + /// + [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); + } + + /// + /// Verifies that enabling sticky keys sets the Flags registry value to "510". + /// + [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); + } + + /// + /// Verifies that disabling sticky keys sets the Flags registry value to "506". + /// + [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); + } + + /// + /// Verifies that enabling filter keys sets the Flags registry value to "2". + /// + [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); + } + + /// + /// Verifies that disabling filter keys sets the Flags registry value to "126". + /// + [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); + } + + /// + /// Verifies that enabling mono audio sets AccessibilityMonoMixState to 1 in the registry. + /// + [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); + } + + /// + /// Verifies that enabling the magnifier starts the magnify.exe process. + /// + [Fact] + public void EnableMagnifier_Enable_StartsProcess() + { + Handle("EnableMagnifier", """{"enable":true}"""); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.FileName == "magnify.exe")), Times.Once); + } + + /// + /// Verifies that enabling narrator starts the narrator.exe process. + /// + [Fact] + public void EnableNarratorAction_Enable_StartsNarrator() + { + Handle("EnableNarratorAction", """{"enable":true}"""); + + _processMock.Verify(p => p.Start(It.Is( + psi => psi.FileName == "narrator.exe")), Times.Once); + } + + /// + /// Verifies that disabling narrator attempts to find and stop the Narrator process by name. + /// + [Fact] + public void EnableNarratorAction_Disable_CallsGetProcessesByName() + { + _processMock.Setup(p => p.GetProcessesByName("Narrator")).Returns([]); + + 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, new Mock().Object); + } + + /// + /// Verifies that mouse cursor speed is set via SystemParametersService with the specified speed level. + /// + [Fact] + public void MouseCursorSpeed_SetsSpeed() + { + Handle("MouseCursorSpeed", """{"speedLevel":10}"""); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x0071, 0, (IntPtr)10, 3), Times.Once); + } + + /// + /// Verifies that mouse wheel scroll lines are set via SystemParametersService. + /// + [Fact] + public void MouseWheelScrollLines_SetsLines() + { + Handle("MouseWheelScrollLines", """{"scrollLines":5}"""); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x0069, 5, IntPtr.Zero, 3), Times.Once); + } + + /// + /// Verifies that enabling enhanced pointer precision updates the mouse speed array with value 1. + /// + [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); + } + + /// + /// Verifies that disabling enhanced pointer precision updates the mouse speed array with value 0. + /// + [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); + } + + /// + /// Verifies that AdjustMousePointerSize opens the ease-of-access mouse settings page. + /// + [Fact] + public void AdjustMousePointerSize_OpensMouseSettings() + { + Handle("AdjustMousePointerSize", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:easeofaccess-mouse"), Times.Once); + } + + /// + /// Verifies that EnableTouchPad opens the touchpad settings page. + /// + [Fact] + public void EnableTouchPad_OpensTouchpadSettings() + { + Handle("EnableTouchPad", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:devices-touchpad"), Times.Once); + } + + /// + /// Verifies that MousePointerCustomization opens the ease-of-access mouse settings page. + /// + [Fact] + public void MousePointerCustomization_OpensMouseSettings() + { + Handle("MousePointerCustomization", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:easeofaccess-mouse"), Times.Once); + } + + /// + /// Verifies that TouchpadCursorSpeed opens the touchpad settings page. + /// + [Fact] + public void TouchpadCursorSpeed_OpensTouchpadSettings() + { + Handle("TouchpadCursorSpeed", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:devices-touchpad"), Times.Once); + } + + /// + /// Verifies that setting the primary mouse button to right swaps the mouse buttons. + /// + [Fact] + public void SetPrimaryMouseButton_Right_SwapsButtons() + { + Handle("SetPrimaryMouseButton", """{"primaryButton":"right"}"""); + + _systemParamsMock.Verify(s => s.SwapMouseButton(true), Times.Once); + } + + /// + /// Verifies that enabling cursor trail sets the trail length to the specified value. + /// + [Fact] + public void CursorTrail_Enable_SetsTrailLength() + { + Handle("CursorTrail", """{"enable":true,"length":7}"""); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x005D, 7, IntPtr.Zero, 3), Times.Once); + } + + /// + /// Verifies that disabling cursor trail sets the trail value to zero. + /// + [Fact] + public void CursorTrail_Disable_SetsTrailValueZero() + { + Handle("CursorTrail", """{"enable":false}"""); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x005D, 0, IntPtr.Zero, It.IsAny()), Times.Once); + } + + /// + /// Verifies that cursor trail length is clamped to the minimum of 2 when a lower value is provided. + /// + [Fact] + public void CursorTrail_LengthClampsToMin2() + { + Handle("CursorTrail", """{"enable":true,"length":0}"""); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x005D, 2, IntPtr.Zero, It.IsAny()), Times.Once); + } + + /// + /// Verifies that cursor trail length is clamped to the maximum of 12 when a higher value is provided. + /// + [Fact] + public void CursorTrail_LengthClampsToMax12() + { + Handle("CursorTrail", """{"enable":true,"length":99}"""); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x005D, 12, IntPtr.Zero, It.IsAny()), Times.Once); + } + + /// + /// Verifies that setting the primary mouse button to left does not swap the mouse buttons. + /// + [Fact] + public void SetPrimaryMouseButton_Left_DoesNotSwap() + { + Handle("SetPrimaryMouseButton", """{"primaryButton":"left"}"""); + + _systemParamsMock.Verify(s => s.SwapMouseButton(false), 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); + } + + /// + /// Verifies that enabling taskbar auto-hide sets the auto-hide bit in the StuckRects3 binary settings. + /// + [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); + } + + /// + /// Verifies that disabling taskbar auto-hide clears the auto-hide bit in the StuckRects3 binary settings. + /// + [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); + } + + /// + /// Verifies that enabling seconds in the system tray clock sets ShowSecondsInSystemClock to 1. + /// + [Fact] + public void DisplaySecondsInSystrayClock_Enable_SetsShowSeconds1() + { + Handle("DisplaySecondsInSystrayClock", """{"enable":true}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "ShowSecondsInSystemClock", 1, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that disabling seconds in the system tray clock sets ShowSecondsInSystemClock to 0. + /// + [Fact] + public void DisplaySecondsInSystrayClock_Disable_SetsShowSeconds0() + { + Handle("DisplaySecondsInSystrayClock", """{"enable":false}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "ShowSecondsInSystemClock", 0, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that enabling the taskbar on all monitors sets MMTaskbarEnabled to 1. + /// + [Fact] + public void DisplayTaskbarOnAllMonitors_Enable_SetsMMTaskbar1() + { + Handle("DisplayTaskbarOnAllMonitors", """{"enable":true}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "MMTaskbarEnabled", 1, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that disabling the taskbar on all monitors sets MMTaskbarEnabled to 0. + /// + [Fact] + public void DisplayTaskbarOnAllMonitors_Disable_SetsMMTaskbar0() + { + Handle("DisplayTaskbarOnAllMonitors", """{"enable":false}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "MMTaskbarEnabled", 0, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that enabling badges on the taskbar sets TaskbarBadges to 1. + /// + [Fact] + public void ShowBadgesOnTaskbar_Enable_SetsBadges1() + { + Handle("ShowBadgesOnTaskbar", """{"enableBadging":true}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarBadges", 1, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that disabling badges on the taskbar sets TaskbarBadges to 0. + /// + [Fact] + public void ShowBadgesOnTaskbar_Disable_SetsBadges0() + { + Handle("ShowBadgesOnTaskbar", """{"enableBadging":false}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarBadges", 0, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that center taskbar alignment sets TaskbarAl to 1. + /// + [Fact] + public void TaskbarAlignment_Center_SetsTaskbarAl1() + { + Handle("TaskbarAlignment", """{"alignment":"center"}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarAl", 1, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that left taskbar alignment sets TaskbarAl to 0. + /// + [Fact] + public void TaskbarAlignment_Left_SetsTaskbarAl0() + { + Handle("TaskbarAlignment", """{"alignment":"left"}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarAl", 0, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that showing the Task View button sets ShowTaskViewButton to 1. + /// + [Fact] + public void TaskViewVisibility_Show_SetsButton1() + { + Handle("TaskViewVisibility", """{"visibility":true}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "ShowTaskViewButton", 1, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that hiding the Task View button sets ShowTaskViewButton to 0. + /// + [Fact] + public void TaskViewVisibility_Hide_SetsButton0() + { + Handle("TaskViewVisibility", """{"visibility":false}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "ShowTaskViewButton", 0, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that showing the widgets button sets TaskbarDa to 1. + /// + [Fact] + public void ToggleWidgetsButtonVisibility_Show_SetsTaskbarDa1() + { + Handle("ToggleWidgetsButtonVisibility", """{"visibility":"show"}"""); + + _registryMock.Verify(r => r.SetValue(ExplorerAdvanced, + "TaskbarDa", 1, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that hiding the widgets button sets TaskbarDa to 0. + /// + [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 Mock _brightnessMock = new(); + private readonly DisplaySettingsHandler _handler; + + public DisplaySettingsHandlerTests() + { + _handler = new DisplaySettingsHandler(_registryMock.Object, _processMock.Object, _brightnessMock.Object, new Mock().Object); + } + + /// + /// Verifies that AdjustColorTemperature opens the night light settings page. + /// + [Fact] + public void AdjustColorTemperature_OpensNightLightSettings() + { + Handle("AdjustColorTemperature", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:nightlight"), Times.Once); + } + + /// + /// Verifies that AdjustScreenOrientation opens the display settings page. + /// + [Fact] + public void AdjustScreenOrientation_OpensDisplaySettings() + { + Handle("AdjustScreenOrientation", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + /// + /// Verifies that DisplayResolutionAndAspectRatio opens the display settings page. + /// + [Fact] + public void DisplayResolutionAndAspectRatio_OpensDisplaySettings() + { + Handle("DisplayResolutionAndAspectRatio", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + /// + /// Verifies that DisplayScaling with a valid percentage opens the display settings page. + /// + [Fact] + public void DisplayScaling_WithPercentage_OpensDisplaySettings() + { + Handle("DisplayScaling", """{"sizeOverride":"150"}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + /// + /// Verifies that enabling the blue light filter schedule writes the enable byte pattern to the registry. + /// + [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); + } + + /// + /// Verifies that disabling the blue light filter schedule writes the disable byte pattern to the registry. + /// + [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); + } + + /// + /// Verifies that enabling rotation lock sets RotationLockPreference to 1 in the registry. + /// + [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); + } + + /// + /// Verifies that disabling rotation lock sets RotationLockPreference to 0 in the registry. + /// + [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); + } + + /// + /// Verifies that increasing brightness adds 10 to the current brightness level. + /// + [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); + } + + /// + /// Verifies that decreasing brightness subtracts 10 from the current brightness level. + /// + [Fact] + public void AdjustScreenBrightness_Decrease_SetsBrightnessMinus10() + { + _brightnessMock.Setup(b => b.GetCurrentBrightness()).Returns((byte)50); + + Handle("AdjustScreenBrightness", """{"brightnessLevel":"decrease"}"""); + + _brightnessMock.Verify(b => b.SetBrightness(40), Times.Once); + } + + /// + /// Verifies that decreasing brightness is clamped to a minimum of 0. + /// + [Fact] + public void AdjustScreenBrightness_Decrease_ClampsToZero() + { + _brightnessMock.Setup(b => b.GetCurrentBrightness()).Returns((byte)5); + + Handle("AdjustScreenBrightness", """{"brightnessLevel":"decrease"}"""); + + _brightnessMock.Verify(b => b.SetBrightness(0), Times.Once); + } + + /// + /// Verifies that increasing brightness is clamped to a maximum of 100. + /// + [Fact] + public void AdjustScreenBrightness_Increase_ClampsTo100() + { + _brightnessMock.Setup(b => b.GetCurrentBrightness()).Returns((byte)95); + + Handle("AdjustScreenBrightness", """{"brightnessLevel":"increase"}"""); + + _brightnessMock.Verify(b => b.SetBrightness(100), Times.Once); + } + + /// + /// Verifies that DisplayScaling at 125% opens the display settings page. + /// + [Fact] + public void DisplayScaling_125Percent_OpensSettings() + { + Handle("DisplayScaling", """{"sizeOverride":"125"}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + /// + /// Verifies that DisplayScaling with non-numeric input does not open any settings page. + /// + [Fact] + public void DisplayScaling_InvalidInput_DoesNotOpenSettings() + { + Handle("DisplayScaling", """{"sizeOverride":"abc"}"""); + + _processMock.Verify(p => p.StartShellExecute(It.IsAny()), Times.Never); + } + + private void Handle(string key, string jsonValue) + { + _handler.Handle(key, jsonValue, JObject.Parse(jsonValue)); + } +} + +#endregion + +#region SystemSettingsHandler + +public class SystemSettingsHandlerTests +{ + private readonly Mock _registryMock = new(); + private readonly Mock _processMock = new(); + private readonly SystemSettingsHandler _handler; + + public SystemSettingsHandlerTests() + { + _handler = new SystemSettingsHandler(_registryMock.Object, _processMock.Object, new Mock().Object); + } + + /// + /// Verifies that AutomaticTimeSettingAction opens the date and time settings page. + /// + [Fact] + public void AutomaticTimeSettingAction_OpensDateTimeSettings() + { + Handle("AutomaticTimeSettingAction", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:dateandtime"), Times.Once); + } + + /// + /// Verifies that EnableGameMode opens the gaming game-mode settings page. + /// + [Fact] + public void EnableGameMode_OpensGamingSettings() + { + Handle("EnableGameMode", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:gaming-gamemode"), Times.Once); + } + + /// + /// Verifies that EnableQuietHours opens the quiet hours (focus assist) settings page. + /// + [Fact] + public void EnableQuietHours_OpensQuietHoursSettings() + { + Handle("EnableQuietHours", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:quiethours"), Times.Once); + } + + /// + /// Verifies that MinimizeWindowsOnMonitorDisconnectAction opens the display settings page. + /// + [Fact] + public void MinimizeWindowsOnMonitorDisconnectAction_OpensDisplaySettings() + { + Handle("MinimizeWindowsOnMonitorDisconnectAction", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + /// + /// Verifies that RememberWindowLocations opens the display settings page. + /// + [Fact] + public void RememberWindowLocations_OpensDisplaySettings() + { + Handle("RememberWindowLocations", """{}"""); + + _processMock.Verify(p => p.StartShellExecute("ms-settings:display"), Times.Once); + } + + /// + /// Verifies that enabling automatic DST adjustment sets DynamicDaylightTimeDisabled to 0. + /// + [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); + } + + /// + /// Verifies that disabling automatic DST adjustment sets DynamicDaylightTimeDisabled to 1. + /// + [Fact] + public void AutomaticDSTAdjustment_Disable_SetsRegistryValue() + { + Handle("AutomaticDSTAdjustment", """{"enable":false}"""); + + _registryMock.Verify(r => r.SetValueLocalMachine( + @"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", + "DynamicDaylightTimeDisabled", + 1, + RegistryValueKind.DWord), 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..6f5fe9739a --- /dev/null +++ b/dotnet/autoShell.Tests/SystemCommandHandlerTests.cs @@ -0,0 +1,48 @@ +// 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 Mock _debuggerMock = new(); + private readonly SystemCommandHandler _handler; + + public SystemCommandHandlerTests() + { + _handler = new SystemCommandHandler(_processMock.Object, _debuggerMock.Object); + } + + /// + /// Verifies that the Debug command launches the debugger. + /// + [Fact] + public void Debug_LaunchesDebugger() + { + Handle("Debug", ""); + + _debuggerMock.Verify(d => d.Launch(), Times.Once); + } + + /// + /// Verifies that the ToggleNotifications command opens the Windows Action Center. + /// + [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..4a109f153b --- /dev/null +++ b/dotnet/autoShell.Tests/ThemeCommandHandlerTests.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +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); + } + + /// + /// Verifies that SetWallpaper calls SystemParametersService with the wallpaper file path. + /// + [Fact] + public void SetWallpaper_CallsSetParameter() + { + Handle("SetWallpaper", @"C:\wallpaper.jpg"); + + _systemParamsMock.Verify(s => s.SetParameter( + 0x0014, 0, @"C:\wallpaper.jpg", 3), Times.Once); + } + + /// + /// Verifies that setting theme mode to dark writes 0 for both app and system light-theme registry keys. + /// + [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); + } + + /// + /// Verifies that setting theme mode to light writes 1 for both app and system light-theme registry keys. + /// + [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); + } + + /// + /// Verifies that applying an unknown theme name does not launch any process. + /// + [Fact] + public void ApplyTheme_UnknownTheme_DoesNotCallProcess() + { + Handle("ApplyTheme", "nonexistent_theme_xyz_12345"); + + _processMock.Verify(p => p.StartShellExecute(It.IsAny()), Times.Never); + } + + /// + /// Verifies that the ListThemes command completes without throwing an exception. + /// + [Fact] + public void ListThemes_ReturnsWithoutError() + { + var ex = Record.Exception(() => Handle("ListThemes", "{}")); + + Assert.Null(ex); + } + + /// + /// Verifies that "toggle" reads the current theme mode and switches to the opposite. + /// + [Fact] + public void SetThemeMode_Toggle_ReadsCurrentModeAndToggles() + { + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + // Current mode is light (1), so toggle should set dark (0) + _registryMock.Setup(r => r.GetValue(Path, "AppsUseLightTheme", null)).Returns(1); + + Handle("SetThemeMode", "toggle"); + + _registryMock.Verify(r => r.GetValue(Path, "AppsUseLightTheme", null), Times.Once); + _registryMock.Verify(r => r.SetValue(Path, "AppsUseLightTheme", 0, RegistryValueKind.DWord), Times.Once); + _registryMock.Verify(r => r.SetValue(Path, "SystemUsesLightTheme", 0, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that the string "true" sets light mode in the theme registry keys. + /// + [Fact] + public void SetThemeMode_BoolTrue_SetsLightMode() + { + Handle("SetThemeMode", "true"); + + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(Path, "AppsUseLightTheme", 1, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that the string "false" sets dark mode in the theme registry keys. + /// + [Fact] + public void SetThemeMode_BoolFalse_SetsDarkMode() + { + Handle("SetThemeMode", "false"); + + const string Path = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + _registryMock.Verify(r => r.SetValue(Path, "AppsUseLightTheme", 0, RegistryValueKind.DWord), Times.Once); + } + + /// + /// Verifies that an unrecognized theme mode value does not write any registry keys. + /// + [Fact] + public void SetThemeMode_InvalidValue_DoesNothing() + { + Handle("SetThemeMode", "invalidvalue"); + + _registryMock.Verify(r => r.SetValue( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + /// + /// Verifies that applying "previous" with no prior theme does not launch any process. + /// + [Fact] + public void ApplyTheme_Previous_RevertsToPreviousTheme() + { + // Applying "previous" with no previous theme should not call StartShellExecute + Handle("ApplyTheme", "previous"); + + _processMock.Verify(p => p.StartShellExecute(It.IsAny()), Times.Never); + } + + private void Handle(string key, string value) + { + _handler.Handle(key, value, JToken.FromObject(value)); + } +} diff --git a/dotnet/autoShell.Tests/VirtualDesktopCommandHandlerTests.cs b/dotnet/autoShell.Tests/VirtualDesktopCommandHandlerTests.cs new file mode 100644 index 0000000000..dafe4c5e47 --- /dev/null +++ b/dotnet/autoShell.Tests/VirtualDesktopCommandHandlerTests.cs @@ -0,0 +1,207 @@ +// 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 VirtualDesktopCommandHandlerTests +{ + private readonly Mock _appRegistryMock = new(); + private readonly Mock _windowMock = new(); + private readonly Mock _virtualDesktopMock = new(); + private readonly VirtualDesktopCommandHandler _handler; + + public VirtualDesktopCommandHandlerTests() + { + _handler = new VirtualDesktopCommandHandler(_appRegistryMock.Object, _windowMock.Object, _virtualDesktopMock.Object, new Mock().Object); + } + + // --- CreateDesktop --- + + /// + /// Verifies that CreateDesktop forwards the JSON desktop names to the virtual desktop service. + /// + [Fact] + public void CreateDesktop_CallsServiceWithJsonValue() + { + var json = JToken.Parse("""["Work","Personal"]"""); + _handler.Handle("CreateDesktop", json.ToString(), json); + + _virtualDesktopMock.Verify(v => v.CreateDesktops(It.IsAny()), Times.Once); + } + + // --- NextDesktop --- + + /// + /// Verifies that NextDesktop invokes the service to switch to the next virtual desktop. + /// + [Fact] + public void NextDesktop_CallsService() + { + _handler.Handle("NextDesktop", "", JToken.FromObject("")); + + _virtualDesktopMock.Verify(v => v.NextDesktop(), Times.Once); + } + + // --- PreviousDesktop --- + + /// + /// Verifies that PreviousDesktop invokes the service to switch to the previous virtual desktop. + /// + [Fact] + public void PreviousDesktop_CallsService() + { + _handler.Handle("PreviousDesktop", "", JToken.FromObject("")); + + _virtualDesktopMock.Verify(v => v.PreviousDesktop(), Times.Once); + } + + // --- SwitchDesktop --- + + /// + /// Verifies that SwitchDesktop with a numeric index forwards it to the service. + /// + [Fact] + public void SwitchDesktop_ByIndex_CallsService() + { + _handler.Handle("SwitchDesktop", "2", JToken.FromObject("2")); + + _virtualDesktopMock.Verify(v => v.SwitchDesktop("2"), Times.Once); + } + + /// + /// Verifies that SwitchDesktop with a desktop name forwards it to the service. + /// + [Fact] + public void SwitchDesktop_ByName_CallsService() + { + _handler.Handle("SwitchDesktop", "Work", JToken.FromObject("Work")); + + _virtualDesktopMock.Verify(v => v.SwitchDesktop("Work"), Times.Once); + } + + // --- MoveWindowToDesktop --- + + /// + /// Verifies that MoveWindowToDesktop resolves the process name and looks up its window handle. + /// Note: the actual MoveWindowToDesktop service call cannot be verified because + /// FindProcessWindowHandle returns IntPtr.Zero by default from the mock. + /// + [Fact] + public void MoveWindowToDesktop_ResolvesProcessNameAndLooksUpWindowHandle() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + _windowMock.Setup(w => w.FindProcessWindowHandle("notepad")).Returns(IntPtr.Zero); + + var json = JToken.Parse("""{"process":"Notepad","desktop":"2"}"""); + _handler.Handle("MoveWindowToDesktop", json.ToString(), json); + + _appRegistryMock.Verify(a => a.ResolveProcessName("Notepad"), Times.Once); + _windowMock.Verify(w => w.FindProcessWindowHandle("notepad"), Times.Once); + } + + // --- PinWindow --- + + /// + /// Verifies that PinWindow resolves the process name and looks up its window handle. + /// + [Fact] + public void PinWindow_ResolvesProcessNameAndLooksUpWindowHandle() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + _windowMock.Setup(w => w.FindProcessWindowHandle("notepad")).Returns(IntPtr.Zero); + + _handler.Handle("PinWindow", "Notepad", JToken.FromObject("Notepad")); + + _appRegistryMock.Verify(a => a.ResolveProcessName("Notepad"), Times.Once); + _windowMock.Verify(w => w.FindProcessWindowHandle("notepad"), Times.Once); + } + + /// + /// Verifies that MoveWindowToDesktop without a process field does not call the service. + /// + [Fact] + public void MoveWindowToDesktop_MissingProcess_DoesNotCallService() + { + var json = JToken.Parse("""{"desktop":"2"}"""); + _handler.Handle("MoveWindowToDesktop", json.ToString(), json); + + _virtualDesktopMock.Verify(v => v.MoveWindowToDesktop(It.IsAny(), It.IsAny()), Times.Never); + } + + /// + /// Verifies that MoveWindowToDesktop without a desktop field does not call the service. + /// + [Fact] + public void MoveWindowToDesktop_MissingDesktop_DoesNotCallService() + { + var json = JToken.Parse("""{"process":"Notepad"}"""); + _handler.Handle("MoveWindowToDesktop", json.ToString(), json); + + _virtualDesktopMock.Verify(v => v.MoveWindowToDesktop(It.IsAny(), It.IsAny()), Times.Never); + } + + /// + /// Verifies that MoveWindowToDesktop with no matching window handle does not call the move service. + /// + [Fact] + public void MoveWindowToDesktop_NoWindowHandle_DoesNotCallService() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + _windowMock.Setup(w => w.FindProcessWindowHandle("notepad")).Returns(IntPtr.Zero); + + var json = JToken.Parse("""{"process":"Notepad","desktop":"2"}"""); + _handler.Handle("MoveWindowToDesktop", json.ToString(), json); + + _virtualDesktopMock.Verify(v => v.MoveWindowToDesktop(It.IsAny(), It.IsAny()), Times.Never); + } + + /// + /// Verifies that MoveWindowToDesktop calls the service when a valid window handle is found. + /// + [Fact] + public void MoveWindowToDesktop_ValidHandle_CallsService() + { + var handle = new IntPtr(12345); + _appRegistryMock.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + _windowMock.Setup(w => w.FindProcessWindowHandle("notepad")).Returns(handle); + + var json = JToken.Parse("""{"process":"Notepad","desktop":"2"}"""); + _handler.Handle("MoveWindowToDesktop", json.ToString(), json); + + _virtualDesktopMock.Verify(v => v.MoveWindowToDesktop(handle, "2"), Times.Once); + } + + /// + /// Verifies that PinWindow with no matching window handle does not call the pin service. + /// + [Fact] + public void PinWindow_NoWindowHandle_DoesNotCallPinWindow() + { + _appRegistryMock.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + _windowMock.Setup(w => w.FindProcessWindowHandle("notepad")).Returns(IntPtr.Zero); + + _handler.Handle("PinWindow", "Notepad", JToken.FromObject("Notepad")); + + _virtualDesktopMock.Verify(v => v.PinWindow(It.IsAny()), Times.Never); + } + + /// + /// Verifies that PinWindow calls the service when a valid window handle is found. + /// + [Fact] + public void PinWindow_ValidHandle_CallsService() + { + var handle = new IntPtr(12345); + _appRegistryMock.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + _windowMock.Setup(w => w.FindProcessWindowHandle("notepad")).Returns(handle); + + _handler.Handle("PinWindow", "Notepad", JToken.FromObject("Notepad")); + + _virtualDesktopMock.Verify(v => v.PinWindow(handle), Times.Once); + } +} diff --git a/dotnet/autoShell.Tests/WindowCommandHandlerTests.cs b/dotnet/autoShell.Tests/WindowCommandHandlerTests.cs new file mode 100644 index 0000000000..0e20dc7d89 --- /dev/null +++ b/dotnet/autoShell.Tests/WindowCommandHandlerTests.cs @@ -0,0 +1,127 @@ +// 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 WindowCommandHandlerTests +{ + private readonly Mock _mockAppRegistry = new(); + private readonly Mock _mockWindow = new(); + private readonly WindowCommandHandler _handler; + + public WindowCommandHandlerTests() + { + _handler = new WindowCommandHandler(_mockAppRegistry.Object, _mockWindow.Object); + } + + /// + /// Verifies that the handler exposes exactly the Maximize, Minimize, SwitchTo, and Tile commands. + /// + [Fact] + public void SupportedCommands_ContainsExpectedCommands() + { + var commands = _handler.SupportedCommands.ToList(); + Assert.Contains("Maximize", commands); + Assert.Contains("Minimize", commands); + Assert.Contains("SwitchTo", commands); + Assert.Contains("Tile", commands); + Assert.Equal(4, commands.Count); + } + + // --- Maximize --- + + /// + /// Verifies that Maximize resolves the process name and calls MaximizeWindow. + /// + [Fact] + public void Maximize_ResolvesAndMaximizes() + { + _mockAppRegistry.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + + Handle("Maximize", "Notepad"); + + _mockWindow.Verify(w => w.MaximizeWindow("notepad"), Times.Once); + } + + // --- Minimize --- + + /// + /// Verifies that Minimize resolves the process name and calls MinimizeWindow. + /// + [Fact] + public void Minimize_ResolvesAndMinimizes() + { + _mockAppRegistry.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + + Handle("Minimize", "Notepad"); + + _mockWindow.Verify(w => w.MinimizeWindow("notepad"), Times.Once); + } + + // --- SwitchTo --- + + /// + /// Verifies that SwitchTo resolves the process name and raises its window. + /// + [Fact] + public void SwitchTo_ResolvesAndRaisesWindow() + { + _mockAppRegistry.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + _mockAppRegistry.Setup(a => a.GetExecutablePath("Notepad")).Returns("C:\\Windows\\notepad.exe"); + + Handle("SwitchTo", "Notepad"); + + _mockWindow.Verify(w => w.RaiseWindow("notepad", "C:\\Windows\\notepad.exe"), Times.Once); + } + + // --- Tile --- + + /// + /// Verifies that Tile resolves both app names and tiles their windows side by side. + /// + [Fact] + public void Tile_ResolvesBothAndTiles() + { + _mockAppRegistry.Setup(a => a.ResolveProcessName("Notepad")).Returns("notepad"); + _mockAppRegistry.Setup(a => a.ResolveProcessName("Calculator")).Returns("calc"); + + Handle("Tile", "Notepad,Calculator"); + + _mockWindow.Verify(w => w.TileWindows("notepad", "calc"), Times.Once); + } + + /// + /// Verifies that Tile with only one app name does not call the tiling service. + /// + [Fact] + public void Tile_SingleApp_DoesNotCallService() + { + Handle("Tile", "Notepad"); + + _mockWindow.Verify(w => w.TileWindows(It.IsAny(), It.IsAny()), Times.Never); + } + + // --- Unknown key --- + + /// + /// Verifies that an unknown command key does not invoke any window or registry service methods. + /// + [Fact] + public void Handle_UnknownKey_DoesNothing() + { + Handle("UnknownWindowCmd", "value"); + + _mockAppRegistry.VerifyNoOtherCalls(); + _mockWindow.VerifyNoOtherCalls(); + } + + private void Handle(string key, string value) + { + _handler.Handle(key, value, JToken.FromObject(value)); + } +} diff --git a/dotnet/autoShell.Tests/WindowsAppRegistryTests.cs b/dotnet/autoShell.Tests/WindowsAppRegistryTests.cs new file mode 100644 index 0000000000..7f2ea502fc --- /dev/null +++ b/dotnet/autoShell.Tests/WindowsAppRegistryTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using autoShell.Logging; +using Moq; + +namespace autoShell.Tests; + +/// +/// Tests the real WindowsAppRegistry implementation to verify dictionary lookups, +/// null-return contracts, and case-insensitive matching. +/// +public class WindowsAppRegistryTests +{ + private readonly WindowsAppRegistry _registry; + + public WindowsAppRegistryTests() + { + _registry = new WindowsAppRegistry(new Mock().Object); + } + + /// + /// Verifies that a known hardcoded app returns its executable path. + /// + [Fact] + public void GetExecutablePath_KnownApp_ReturnsPath() + { + string path = _registry.GetExecutablePath("notepad"); + + Assert.NotNull(path); + Assert.Contains("notepad", path, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Verifies that an unknown app returns null instead of throwing. + /// + [Fact] + public void GetExecutablePath_UnknownApp_ReturnsNull() + { + string path = _registry.GetExecutablePath("nonexistent_app_xyz"); + + Assert.Null(path); + } + + /// + /// Verifies that lookups are case-insensitive. + /// + [Theory] + [InlineData("Notepad")] + [InlineData("NOTEPAD")] + [InlineData("notepad")] + public void GetExecutablePath_CaseInsensitive_ReturnsPath(string name) + { + string path = _registry.GetExecutablePath(name); + + Assert.NotNull(path); + } + + /// + /// Verifies that an unknown app returns null for AppUserModelId instead of throwing. + /// + [Fact] + public void GetAppUserModelId_UnknownApp_ReturnsNull() + { + string id = _registry.GetAppUserModelId("nonexistent_app_xyz"); + + Assert.Null(id); + } + + /// + /// Verifies that ResolveProcessName returns the executable filename (without extension) for a known app. + /// + [Fact] + public void ResolveProcessName_KnownApp_ReturnsProcessName() + { + string name = _registry.ResolveProcessName("notepad"); + + Assert.Equal("notepad", name); + } + + /// + /// Verifies that ResolveProcessName returns the input unchanged for an unknown app. + /// + [Fact] + public void ResolveProcessName_UnknownApp_ReturnsFriendlyName() + { + string name = _registry.ResolveProcessName("unknown_app"); + + Assert.Equal("unknown_app", name); + } + + /// + /// Verifies that GetWorkingDirectoryEnvVar returns null for apps without a configured working directory. + /// + [Fact] + public void GetWorkingDirectoryEnvVar_AppWithoutWorkDir_ReturnsNull() + { + string envVar = _registry.GetWorkingDirectoryEnvVar("notepad"); + + Assert.Null(envVar); + } + + /// + /// Verifies that GetArguments returns null for apps without configured arguments. + /// + [Fact] + public void GetArguments_AppWithoutArgs_ReturnsNull() + { + string args = _registry.GetArguments("notepad"); + + Assert.Null(args); + } +} diff --git a/dotnet/autoShell.Tests/autoShell.Tests.csproj b/dotnet/autoShell.Tests/autoShell.Tests.csproj new file mode 100644 index 0000000000..1c6050d5a2 --- /dev/null +++ b/dotnet/autoShell.Tests/autoShell.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0-windows + enable + enable + false + true + + + $(NoWarn);JSON002;IDE1006 + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/autoShell/App.config b/dotnet/autoShell/App.config deleted file mode 100644 index 193aecc675..0000000000 --- a/dotnet/autoShell/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/dotnet/autoShell/AutoShell.cs b/dotnet/autoShell/AutoShell.cs index b51ae1ff06..08717bd041 100644 --- a/dotnet/autoShell/AutoShell.cs +++ b/dotnet/autoShell/AutoShell.cs @@ -2,110 +2,31 @@ // Licensed under the MIT License. using System; -using System.Collections; -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 Microsoft.VisualBasic; -using Microsoft.VisualBasic.ApplicationServices; -using Microsoft.WindowsAPICodePack.Shell; +using autoShell.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using static autoShell.AutoShell; - namespace autoShell; -internal partial class AutoShell +internal 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; - - 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; - - - /// - /// Constructor used to get system wide information required for specific commands. - /// - 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" } }, - }; + #region P/Invoke - // 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); - } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern IntPtr GetCommandLineW(); - // Load the installed themes - LoadThemes(); + #endregion P/Invoke - // 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); - } + private static readonly ConsoleLogger s_logger = new(); + private static readonly CommandDispatcher s_dispatcher = CommandDispatcher.Create(s_logger); /// /// Program entry point /// /// Any command line arguments - static void Main(string[] args) + private static void Main(string[] args) { string rawCmdLine = Marshal.PtrToStringUni(GetCommandLineW()); @@ -114,19 +35,27 @@ static void Main(string[] args) if (args.Length > 0) { string exe = $"\"{Environment.ProcessPath}\""; - string cmdLine = rawCmdLine.Replace(exe, ""); + string cmdLine = rawCmdLine!.Replace(exe, ""); if (cmdLine.StartsWith(exe, StringComparison.OrdinalIgnoreCase)) { cmdLine = cmdLine[exe.Length..]; } - else if (cmdLine.StartsWith(Path.GetFileName(Environment.ProcessPath), StringComparison.OrdinalIgnoreCase)) - { - cmdLine = cmdLine[Path.GetFileName(Environment.ProcessPath).Length..]; - } - else if (cmdLine.StartsWith(Path.GetFileNameWithoutExtension(Environment.ProcessPath), StringComparison.OrdinalIgnoreCase)) + else { - cmdLine = cmdLine[Path.GetFileNameWithoutExtension(Environment.ProcessPath).Length..]; + var processFileName = Path.GetFileName(Environment.ProcessPath)!; + if (cmdLine.StartsWith(processFileName, StringComparison.OrdinalIgnoreCase)) + { + cmdLine = cmdLine[processFileName.Length..]; + } + else + { + var processFileNameNoExt = Path.GetFileNameWithoutExtension(processFileName); + if (cmdLine.StartsWith(processFileNameNoExt, StringComparison.OrdinalIgnoreCase)) + { + cmdLine = cmdLine[processFileNameNoExt.Length..]; + } + } } try @@ -134,12 +63,12 @@ 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 @@ -165,1618 +94,15 @@ static void Main(string[] args) JObject root = JObject.Parse(line); // execute the line - quit = execLine(root); + quit = ExecLine(root); } catch (Exception ex) { - LogError(ex); - } - } - } - - 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; - } - - 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; - } - - 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); - } - } - - 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); - } - } - - 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); - } - } - - static string ResolveProcessNameFromFriendlyName(string friendlyName) - { - string path = (string)s_friendlyNameToPath[friendlyName.ToLowerInvariant()]; - if (path != null) - { - return Path.GetFileNameWithoutExtension(path); - } - else - { - return friendlyName; - } - } - - 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 - 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); - } - } - } - - 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); - } - } - - 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); - } - } - - 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. - 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 - 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 - 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(); - } - } - } - } - - 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"] - 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}"); - } + s_logger.Error(ex); } } - catch (JsonException ex) - { - Debug.WriteLine($"Failed to parse desktop names JSON: {ex.Message}"); - } - catch (Exception ex) - { - Debug.WriteLine($"Error creating desktops: {ex.Message}"); - } - } - - 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); - } - } - - 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); } - 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); - } - - 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; - } - - 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 - 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); - } - } - - 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; - } - } - - 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; - case "CursorTrail": - HandleMouseCursorTrail(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; - } - - /// - /// Sets the airplane mode state using the Radio Management API. - /// - /// True to enable airplane mode, false to disable. - 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. - /// - 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. - 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. - /// - 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). - 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. - /// - 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". - 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); - } - } - - 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); - } - } + private static bool ExecLine(JObject root) + => s_dispatcher.Dispatch(root); } diff --git a/dotnet/autoShell/AutoShell_Settings.cs b/dotnet/autoShell/AutoShell_Settings.cs deleted file mode 100644 index 9960c7ac32..0000000000 --- a/dotnet/autoShell/AutoShell_Settings.cs +++ /dev/null @@ -1,1580 +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}"} - /// - 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}"} - /// - 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}"} - /// - 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\"}"} - /// - 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}"} - /// - 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\"}"} - /// - 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\"}"} - /// - 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\"}"} - /// - 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\"}"} - /// - 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}"} - /// - 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\"}"} - /// - 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}"} - /// - 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}"} - /// - 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": "{}"} - /// - 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}"} - /// - 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\"}"} - /// - 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}"} - /// - 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\"}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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\"}"} - /// - 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}"} - /// - 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\"}"} - /// - 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\"}"} - /// - 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); - } - } - - /// - /// 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 - /// - static void HandleMouseCursorTrail(string jsonParams) - { - try - { - 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; - - SystemParametersInfo(SPI_SETMOUSETRAILS, trailValue, IntPtr.Zero, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); - Debug.WriteLine(enable - ? $"Cursor trail enabled with length {length}" - : "Cursor trail disabled"); - } - catch (Exception ex) - { - LogError(ex); - } - } - - #endregion - - #region Touchpad Settings - - /// - /// Enables or disables the touchpad - /// Command: {"EnableTouchPad": "{\"enable\":true}"} - /// - 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}"} - /// - 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\"}"} - /// - 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\"}"} - /// - 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\"}"} - /// - 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}"} - /// - 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\"}"} - /// - 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\"}"} - /// - 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": "{}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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}"} - /// - 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 - /// - 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 - /// - 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 - /// - 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 - /// - 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 - /// - 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 - /// - 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 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; - const int SPI_SETMOUSETRAILS = 0x005D; - // 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); - - [DllImport("user32.dll", SetLastError = true)] - static extern bool SystemParametersInfo(int uiAction, int uiParam, int[] pvParam, int fWinIni); - - [DllImport("user32.dll")] - static extern bool SwapMouseButton(int fSwap); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - 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 c57c9898b2..0000000000 --- a/dotnet/autoShell/AutoShell_Themes.cs +++ /dev/null @@ -1,397 +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.Linq; -using System.Text; -using System.Threading.Tasks; -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 = 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) - { - 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.Substring("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.Substring(1); - // Expand environment variables - int commaIndex = resourcePath.LastIndexOf(','); - if (commaIndex > 0) - { - 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) - { - 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 = new HashSet(); - - themes.UnionWith(s_themeDictionary.Keys); - themes.UnionWith(s_themeDisplayNameDictionary.Keys); - - return themes.ToList(); - } - - /// - /// 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.ToLowerInvariant() != "previous") - { - // 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(); - if (currentMode.HasValue) - { - return SetLightDarkMode(!currentMode.Value); - } - return 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; - IntPtr result; - SendMessageTimeout( - (IntPtr)HWND_BROADCAST, - WM_SETTINGCHANGE, - IntPtr.Zero, - "ImmersiveColorSet", - SMTO_ABORTIFHUNG, - 1000, - out 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"); - if (value is int intValue) - { - return intValue == 1; - } - - return null; - } - } - catch - { - return null; - } - } - - } -} diff --git a/dotnet/autoShell/AutoShell_Win32.cs b/dotnet/autoShell/AutoShell_Win32.cs deleted file mode 100644 index c2794a625e..0000000000 --- a/dotnet/autoShell/AutoShell_Win32.cs +++ /dev/null @@ -1,595 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// 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 -{ - internal unsafe 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); - - // 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")] - static extern bool GetWindowRect(IntPtr hWnd, ref RECT Rect); - - // import GetShellWindow - [DllImport("user32.dll")] - static extern IntPtr GetShellWindow(); - - // import GetDesktopWindow - [DllImport("user32.dll")] - 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); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - static extern IntPtr SendMessageTimeout( - IntPtr hWnd, - uint Msg, - IntPtr wParam, - string lParam, - uint fuFlags, - uint uTimeout, - out IntPtr lpdwResult); - - // import SetWindowPos - [DllImport("user32.dll")] - 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); - - // import FindWindowEx - [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 - { - 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)] - 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); - - [DllImport("user32.dll")] - 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); - - [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")] - 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); - - [DllImport("wlanapi.dll")] - 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); - - [DllImport("wlanapi.dll")] - static extern int WlanScan(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pDot11Ssid, IntPtr pIeData, IntPtr pReserved); - - [DllImport("wlanapi.dll")] - 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); - - [DllImport("wlanapi.dll")] - 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); - - [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 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; - private const int DM_BITSPERPEL = 0x40000; - private const int DM_DISPLAYFREQUENCY = 0x400000; - - 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 - { - 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/CommandDispatcher.cs b/dotnet/autoShell/CommandDispatcher.cs new file mode 100644 index 0000000000..9994381aeb --- /dev/null +++ b/dotnet/autoShell/CommandDispatcher.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Handlers; +using autoShell.Handlers.Settings; +using autoShell.Logging; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell; + +/// +/// Routes incoming JSON commands to the appropriate handler via a direct dictionary lookup. +/// +internal class CommandDispatcher +{ + private readonly Dictionary _handlers = []; + private readonly ILogger _logger; + + public CommandDispatcher(ILogger logger) + { + _logger = logger; + } + + /// + /// Creates a CommandDispatcher with all production services and handlers registered. + /// + public static CommandDispatcher Create(ILogger logger) + { + return Create( + logger, + new WindowsRegistryService(), + new WindowsSystemParametersService(), + new WindowsProcessService(), + new WindowsAudioService(logger), + new WindowsAppRegistry(logger), + new WindowsDebuggerService(), + new WindowsBrightnessService(logger), + new WindowsDisplayService(logger), + new WindowsWindowService(logger), + new WindowsNetworkService(logger), + new WindowsVirtualDesktopService(logger) + ); + } + + /// + /// Creates a CommandDispatcher with the specified services, enabling integration testing + /// with mock services while exercising real handler wiring. + /// + internal static CommandDispatcher Create( + ILogger logger, + IRegistryService registry, + ISystemParametersService systemParams, + IProcessService process, + IAudioService audio, + IAppRegistry appRegistry, + IDebuggerService debugger, + IBrightnessService brightness, + IDisplayService display, + IWindowService window, + INetworkService network, + IVirtualDesktopService virtualDesktop) + { + var dispatcher = new CommandDispatcher(logger); + dispatcher.Register( + new AudioCommandHandler(audio), + new AppCommandHandler(appRegistry, process, window, logger), + new WindowCommandHandler(appRegistry, window), + new ThemeCommandHandler(registry, process, systemParams), + new VirtualDesktopCommandHandler(appRegistry, window, virtualDesktop, logger), + new NetworkCommandHandler(network, process, logger), + new DisplayCommandHandler(display, logger), + new TaskbarSettingsHandler(registry), + new DisplaySettingsHandler(registry, process, brightness, logger), + new PersonalizationSettingsHandler(registry, process), + new MouseSettingsHandler(systemParams, process, logger), + new AccessibilitySettingsHandler(registry, process), + new PrivacySettingsHandler(registry), + new PowerSettingsHandler(registry, process), + new FileExplorerSettingsHandler(registry), + new SystemSettingsHandler(registry, process, logger), + new SystemCommandHandler(process, debugger) + ); + + return dispatcher; + } + + /// + /// Registers one or more command handlers with the dispatcher. + /// + 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. + /// + /// True if a "quit" command was encountered; otherwise false. + public bool Dispatch(JObject root) + { + foreach (var kvp in root) + { + string key = kvp.Key; + + if (key == "quit") + { + return true; + } + + try + { + if (_handlers.TryGetValue(key, out ICommandHandler handler)) + { + string value = kvp.Value?.ToString(); + handler.Handle(key, value, kvp.Value); + } + else + { + _logger.Debug("Unknown command: " + key); + } + } + catch (Exception ex) + { + _logger.Error(ex); + } + } + return false; + } +} diff --git a/dotnet/autoShell/CoreAudioInterop.cs b/dotnet/autoShell/CoreAudioInterop.cs deleted file mode 100644 index 4c3b32b3fb..0000000000 --- a/dotnet/autoShell/CoreAudioInterop.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Runtime.InteropServices; - -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 ERole - { - eConsole = 0, - eMultimedia = 1, - eCommunications = 2 - } - - [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("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); - } -} diff --git a/dotnet/autoShell/Handlers/AppCommandHandler.cs b/dotnet/autoShell/Handlers/AppCommandHandler.cs new file mode 100644 index 0000000000..7daef259a0 --- /dev/null +++ b/dotnet/autoShell/Handlers/AppCommandHandler.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using autoShell.Logging; +using autoShell.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles application lifecycle commands: CloseProgram, LaunchProgram, and ListAppNames. +/// +internal class AppCommandHandler : ICommandHandler +{ + private readonly IAppRegistry _appRegistry; + private readonly IProcessService _processService; + private readonly IWindowService _window; + private readonly ILogger _logger; + + public AppCommandHandler(IAppRegistry appRegistry, IProcessService processService, IWindowService window, ILogger logger) + { + _appRegistry = appRegistry; + _processService = processService; + _window = window; + _logger = logger; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "CloseProgram", + "LaunchProgram", + "ListAppNames", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "CloseProgram": + CloseApplication(value); + break; + + case "LaunchProgram": + OpenApplication(value); + break; + + case "ListAppNames": + Console.WriteLine(JsonConvert.SerializeObject(_appRegistry.GetAllAppNames())); + break; + } + } + + private void CloseApplication(string friendlyName) + { + string processName = _appRegistry.ResolveProcessName(friendlyName); + Process[] processes = _processService.GetProcessesByName(processName); + if (processes.Length != 0) + { + _logger.Debug("Closing " + friendlyName); + foreach (Process p in processes) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + p.CloseMainWindow(); + } + } + } + } + + private void OpenApplication(string friendlyName) + { + string processName = _appRegistry.ResolveProcessName(friendlyName); + Process[] processes = _processService.GetProcessesByName(processName); + + if (processes.Length == 0) + { + _logger.Debug("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 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 (Exception ex) { _logger.Debug($"Failed to launch UWP app: {ex.Message}"); } + } + } + } + else + { + _logger.Debug("Raising " + friendlyName); + string processName2 = _appRegistry.ResolveProcessName(friendlyName); + string path2 = _appRegistry.GetExecutablePath(friendlyName); + _window.RaiseWindow(processName2, path2); + } + } +} diff --git a/dotnet/autoShell/Handlers/AudioCommandHandler.cs b/dotnet/autoShell/Handlers/AudioCommandHandler.cs new file mode 100644 index 0000000000..269b36470b --- /dev/null +++ b/dotnet/autoShell/Handlers/AudioCommandHandler.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles audio commands: Mute, RestoreVolume, and Volume. +/// +internal class AudioCommandHandler : ICommandHandler +{ + private readonly IAudioService _audio; + private double _savedVolumePct; + + public AudioCommandHandler(IAudioService audio) + { + _audio = audio; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "Mute", + "RestoreVolume", + "Volume", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "Mute": + if (bool.TryParse(value, out bool mute)) + { + _audio.SetMute(mute); + } + break; + case "RestoreVolume": + _audio.SetVolume((int)_savedVolumePct); + break; + case "Volume": + if (int.TryParse(value, out int pct)) + { + int currentVolume = _audio.GetVolume(); + if (currentVolume > 0) + { + _savedVolumePct = currentVolume; + } + _audio.SetVolume(pct); + } + break; + } + } +} diff --git a/dotnet/autoShell/Handlers/DisplayCommandHandler.cs b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs new file mode 100644 index 0000000000..cd50edd77a --- /dev/null +++ b/dotnet/autoShell/Handlers/DisplayCommandHandler.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Logging; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles display commands: ListResolutions, SetScreenResolution, and SetTextSize. +/// +internal class DisplayCommandHandler : ICommandHandler +{ + private readonly IDisplayService _display; + private readonly ILogger _logger; + + public DisplayCommandHandler(IDisplayService display, ILogger logger) + { + _display = display; + _logger = logger; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "ListResolutions", + "SetScreenResolution", + "SetTextSize", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "ListResolutions": + try + { + Console.WriteLine(_display.ListResolutions()); + } + catch (Exception ex) + { + _logger.Error(ex); + } + break; + + case "SetScreenResolution": + try + { + uint width; + uint height; + uint? refreshRate = null; + + if (rawValue.Type == JTokenType.Object) + { + width = rawValue.Value("width"); + height = rawValue.Value("height"); + if (rawValue["refreshRate"] != null) + { + refreshRate = rawValue.Value("refreshRate"); + } + } + else + { + string resString = rawValue.ToString(); + string[] parts = resString.ToLowerInvariant().Split('x', '@'); + if (parts.Length < 2) + { + _logger.Warning("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)) + { + _logger.Warning("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; + } + } + + string result = _display.SetResolution(width, height, refreshRate); + Console.WriteLine(result); + } + catch (Exception ex) + { + _logger.Error(ex); + } + break; + + case "SetTextSize": + try + { + if (int.TryParse(value, out int textSizePct)) + { + _display.SetTextSize(textSizePct); + } + } + catch (Exception ex) + { + _logger.Error(ex); + } + break; + } + } +} 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..c84d78895f --- /dev/null +++ b/dotnet/autoShell/Handlers/NetworkCommandHandler.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Logging; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles network commands: BluetoothToggle, ConnectWifi, DisconnectWifi, +/// EnableMeteredConnections, EnableWifi, ListWifiNetworks, and ToggleAirplaneMode. +/// +internal class NetworkCommandHandler : ICommandHandler +{ + private readonly INetworkService _network; + private readonly IProcessService _process; + private readonly ILogger _logger; + + public NetworkCommandHandler(INetworkService network, IProcessService process, ILogger logger) + { + _network = network; + _process = process; + _logger = logger; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "BluetoothToggle", + "ConnectWifi", + "DisconnectWifi", + "EnableMeteredConnections", + "EnableWifi", + "ListWifiNetworks", + "ToggleAirplaneMode", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "BluetoothToggle": + var btParam = JObject.Parse(value); + bool enableBt = btParam.Value("enableBluetooth") ?? true; + _network.ToggleBluetooth(enableBt); + break; + + case "EnableMeteredConnections": + _process.StartShellExecute("ms-settings:network-status"); + break; + + case "EnableWifi": + var wifiParam = JObject.Parse(value); + bool enableWifi = wifiParam.Value("enable") ?? true; + _network.EnableWifi(enableWifi); + break; + + case "ConnectWifi": + var netInfo = JObject.Parse(value); + string ssid = netInfo.Value("ssid"); + string password = netInfo["password"] is not null ? netInfo.Value("password") : ""; + _network.ConnectToWifi(ssid, password); + break; + + case "DisconnectWifi": + _network.DisconnectFromWifi(); + break; + + case "ListWifiNetworks": + Console.WriteLine(_network.ListWifiNetworks()); + break; + + case "ToggleAirplaneMode": + _network.SetAirplaneMode(bool.Parse(value)); + break; + } + } +} diff --git a/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs new file mode 100644 index 0000000000..36ce35a732 --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/AccessibilitySettingsHandler.cs @@ -0,0 +1,111 @@ +// 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.Settings; + +/// +/// Handles accessibility settings: filter keys, magnifier, narrator, sticky keys, and mono audio. +/// +internal class AccessibilitySettingsHandler : ICommandHandler +{ + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public AccessibilitySettingsHandler(IRegistryService registry, IProcessService process) + { + _registry = registry; + _process = process; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "EnableFilterKeysAction", + "EnableMagnifier", + "EnableNarratorAction", + "EnableStickyKeys", + "MonoAudioToggle", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + 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; + } + } + + 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/Settings/DisplaySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs new file mode 100644 index 0000000000..2f2dc6ffab --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/DisplaySettingsHandler.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Logging; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers.Settings; + +/// +/// Handles display settings: brightness, color temperature, orientation, resolution, scaling, +/// blue light filter, and rotation lock. +/// +internal class DisplaySettingsHandler : ICommandHandler +{ + private readonly IRegistryService _registry; + private readonly IProcessService _process; + private readonly IBrightnessService _brightness; + private readonly ILogger _logger; + + public DisplaySettingsHandler(IRegistryService registry, IProcessService process, IBrightnessService brightness, ILogger logger) + { + _registry = registry; + _process = process; + _brightness = brightness; + _logger = logger; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "AdjustColorTemperature", + "AdjustScreenBrightness", + "AdjustScreenOrientation", + "DisplayResolutionAndAspectRatio", + "DisplayScaling", + "EnableBlueLightFilterSchedule", + "RotationLock", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + var param = JObject.Parse(value); + + switch (key) + { + case "AdjustColorTemperature": + _process.StartShellExecute("ms-settings:nightlight"); + break; + + case "AdjustScreenBrightness": + HandleAdjustScreenBrightness(param); + break; + + case "AdjustScreenOrientation": + case "DisplayResolutionAndAspectRatio": + _process.StartShellExecute("ms-settings:display"); + break; + + case "DisplayScaling": + HandleDisplayScaling(param); + break; + + case "EnableBlueLightFilterSchedule": + HandleBlueLightFilter(param); + break; + + case "RotationLock": + HandleRotationLock(param); + break; + } + } + + private void HandleAdjustScreenBrightness(JObject param) + { + string level = param.Value("brightnessLevel"); + bool increase = level == "increase"; + + byte currentBrightness = _brightness.GetCurrentBrightness(); + byte newBrightness = increase + ? (byte)Math.Min(100, currentBrightness + 10) + : (byte)Math.Max(0, currentBrightness - 10); + + _brightness.SetBrightness(newBrightness); + _logger.Debug($"Brightness adjusted to: {newBrightness}%"); + } + + private void HandleDisplayScaling(JObject param) + { + 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 + }; + + // DPI scaling requires opening settings + _process.StartShellExecute("ms-settings:display"); + _logger.Debug($"Display scaling target: {percentage}%"); + } + } + + private void HandleBlueLightFilter(JObject param) + { + bool disabled = param.Value("nightLightScheduleDisabled") ?? false; + byte[] data = disabled + ? [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", + "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/Settings/FileExplorerSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs new file mode 100644 index 0000000000..eb846a9041 --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/FileExplorerSettingsHandler.cs @@ -0,0 +1,85 @@ +// 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.Settings; + +/// +/// Handles File Explorer settings: file extensions and hidden/system files visibility. +/// +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); + #endregion P/Invoke + + private readonly IRegistryService _registry; + + public FileExplorerSettingsHandler(IRegistryService registry) + { + _registry = registry; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "ShowFileExtensions", + "ShowHiddenAndSystemFiles", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + var param = JObject.Parse(value); + + switch (key) + { + case "ShowFileExtensions": + HandleShowFileExtensions(param); + break; + + case "ShowHiddenAndSystemFiles": + HandleShowHiddenAndSystemFiles(param); + break; + } + + NotifySettingsChange(); + } + + private static void NotifySettingsChange() + { + try + { + SendNotifyMessage((IntPtr)0xffff, 0x001A, IntPtr.Zero, IntPtr.Zero); + } + catch (EntryPointNotFoundException) + { + // P/Invoke may not be available in all environments + } + } + + 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; + // 1 = show hidden files, 2 = don't show hidden files + _registry.SetValue(ExplorerAdvanced, "Hidden", enable ? 1 : 2, RegistryValueKind.DWord); + // Show protected operating system files + _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 new file mode 100644 index 0000000000..ce6711f3cc --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/MouseSettingsHandler.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Logging; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers.Settings; + +/// +/// Handles mouse and touchpad settings: pointer size, precision, cursor speed, scroll lines, +/// primary button, customization, and touchpad. +/// +internal 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_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; + + private readonly ISystemParametersService _systemParams; + private readonly IProcessService _process; + private readonly ILogger _logger; + + public MouseSettingsHandler(ISystemParametersService systemParams, IProcessService process, ILogger logger) + { + _systemParams = systemParams; + _process = process; + _logger = logger; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "AdjustMousePointerSize", + "CursorTrail", + "EnableTouchPad", + "EnhancePointerPrecision", + "MouseCursorSpeed", + "MousePointerCustomization", + "MouseWheelScrollLines", + "SetPrimaryMouseButton", + "TouchpadCursorSpeed", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + var param = JObject.Parse(value); + + switch (key) + { + case "AdjustMousePointerSize": + case "MousePointerCustomization": + _process.StartShellExecute("ms-settings:easeofaccess-mouse"); + break; + + case "CursorTrail": + HandleMouseCursorTrail(value); + break; + + case "EnableTouchPad": + case "TouchpadCursorSpeed": + _process.StartShellExecute("ms-settings:devices-touchpad"); + break; + + case "EnhancePointerPrecision": + HandleEnhancePointerPrecision(param); + break; + + case "MouseCursorSpeed": + HandleMouseCursorSpeed(param); + break; + + case "MouseWheelScrollLines": + HandleMouseWheelScrollLines(param); + break; + + case "SetPrimaryMouseButton": + HandleSetPrimaryMouseButton(param); + break; + } + } + + private void HandleEnhancePointerPrecision(JObject param) + { + bool enable = param.Value("enable") ?? true; + int[] mouseParams = new int[3]; + _systemParams.GetParameter(SPI_GETMOUSE, 0, mouseParams, 0); + // Set acceleration (third parameter): 1 = enhanced precision on, 0 = off + mouseParams[2] = enable ? 1 : 0; + _systemParams.SetParameter(SPI_SETMOUSE, 0, mouseParams, SPIF_UPDATEINIFILE_SENDCHANGE); + } + + private void HandleMouseCursorSpeed(JObject param) + { + // Speed range: 1-20 (default 10) + int speed = param.Value("speedLevel") ?? 10; + speed = Math.Clamp(speed, 1, 20); + _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 = off, 2-12 = 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; + + _systemParams.SetParameter(SPI_SETMOUSETRAILS, trailValue, IntPtr.Zero, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + _logger.Debug(enable + ? $"Cursor trail enabled with length {length}" + : "Cursor trail disabled"); + } + + private void HandleMouseWheelScrollLines(JObject param) + { + int lines = param.Value("scrollLines") ?? 3; + lines = Math.Clamp(lines, 1, 100); + _systemParams.SetParameter(SPI_SETWHEELSCROLLLINES, lines, IntPtr.Zero, SPIF_UPDATEINIFILE_SENDCHANGE); + } + + private void HandleSetPrimaryMouseButton(JObject param) + { + string button = param.Value("primaryButton") ?? "left"; + bool leftPrimary = button.Equals("left", StringComparison.OrdinalIgnoreCase); + _systemParams.SwapMouseButton(!leftPrimary); + } +} diff --git a/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs new file mode 100644 index 0000000000..2bae7d157e --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/PersonalizationSettingsHandler.cs @@ -0,0 +1,92 @@ +// 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.Settings; + +/// +/// Handles personalization settings: title bar color, transparency, high contrast, and theme mode. +/// +internal class PersonalizationSettingsHandler : ICommandHandler +{ + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public PersonalizationSettingsHandler(IRegistryService registry, IProcessService process) + { + _registry = registry; + _process = process; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "ApplyColorToTitleBar", + "EnableTransparency", + "HighContrastTheme", + "SystemThemeMode", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + 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; + } + } + + 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"; + // Set apps theme (AppsUseLightTheme: 0 = dark, 1 = light) + _registry.SetValue(PersonalizePath, "AppsUseLightTheme", value, RegistryValueKind.DWord); + // Set system theme — taskbar, Start menu, etc. + _registry.SetValue(PersonalizePath, "SystemUsesLightTheme", value, RegistryValueKind.DWord); + _registry.BroadcastSettingChange("ImmersiveColorSet"); + } +} diff --git a/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs new file mode 100644 index 0000000000..e83e28d949 --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/PowerSettingsHandler.cs @@ -0,0 +1,62 @@ +// 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.Settings; + +/// +/// Handles power settings: battery saver threshold and power mode (on battery / plugged in). +/// +internal class PowerSettingsHandler : ICommandHandler +{ + private readonly IRegistryService _registry; + private readonly IProcessService _process; + + public PowerSettingsHandler(IRegistryService registry, IProcessService process) + { + _registry = registry; + _process = process; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "BatterySaverActivationLevel", + "SetPowerModeOnBattery", + "SetPowerModePluggedIn", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + var param = JObject.Parse(value); + + switch (key) + { + case "BatterySaverActivationLevel": + HandleBatterySaverThreshold(param); + break; + + case "SetPowerModeOnBattery": + case "SetPowerModePluggedIn": + _process.StartShellExecute("ms-settings:powersleep"); + break; + } + } + + private void HandleBatterySaverThreshold(JObject param) + { + int threshold = param.Value("thresholdValue") ?? 20; + threshold = Math.Clamp(threshold, 0, 100); + _registry.SetValue( + @"Software\Microsoft\Windows\CurrentVersion\Power\BatterySaver", + "ActivationThreshold", + threshold, + RegistryValueKind.DWord); + } +} diff --git a/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs new file mode 100644 index 0000000000..23fad65bbe --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/PrivacySettingsHandler.cs @@ -0,0 +1,64 @@ +// 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.Settings; + +/// +/// Handles privacy settings: camera, location, and microphone access. +/// +internal class PrivacySettingsHandler : ICommandHandler +{ + private const string ConsentStoreBase = @"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore"; + + private readonly IRegistryService _registry; + + public PrivacySettingsHandler(IRegistryService registry) + { + _registry = registry; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "ManageCameraAccess", + "ManageLocationAccess", + "ManageMicrophoneAccess", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + var param = JObject.Parse(value); + + string subKey = key switch + { + "ManageCameraAccess" => "webcam", + "ManageLocationAccess" => "location", + "ManageMicrophoneAccess" => "microphone", + _ => null, + }; + + if (subKey != null) + { + SetAccessSetting(param, subKey); + } + } + + 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/Settings/SystemSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs new file mode 100644 index 0000000000..5773e83678 --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/SystemSettingsHandler.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Logging; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers.Settings; + +/// +/// Handles miscellaneous system settings: time/region, focus assist, gaming, and multi-monitor. +/// +internal class SystemSettingsHandler : ICommandHandler +{ + private readonly IRegistryService _registry; + private readonly IProcessService _process; + private readonly ILogger _logger; + + public SystemSettingsHandler(IRegistryService registry, IProcessService process, ILogger logger) + { + _registry = registry; + _process = process; + _logger = logger; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "AutomaticDSTAdjustment", + "AutomaticTimeSettingAction", + "EnableGameMode", + "EnableQuietHours", + "MinimizeWindowsOnMonitorDisconnectAction", + "RememberWindowLocations", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + 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; + } + } + + private void HandleAutomaticDSTAdjustment(string jsonParams) + { + var param = JObject.Parse(jsonParams); + bool enable = param.Value("enable") ?? true; + + _registry.SetValueLocalMachine( + @"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", + "DynamicDaylightTimeDisabled", + enable ? 0 : 1, + Microsoft.Win32.RegistryValueKind.DWord); + + _logger.Debug($"Automatic DST adjustment {(enable ? "enabled" : "disabled")}"); + } +} diff --git a/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs b/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs new file mode 100644 index 0000000000..c8045ff47b --- /dev/null +++ b/dotnet/autoShell/Handlers/Settings/TaskbarSettingsHandler.cs @@ -0,0 +1,139 @@ +// 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.Settings; + +/// +/// Handles taskbar settings: auto-hide, alignment, task view, widgets, badges, multi-monitor, clock. +/// +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) + { + _registry = registry; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "AutoHideTaskbar", + "DisplaySecondsInSystrayClock", + "DisplayTaskbarOnAllMonitors", + "ShowBadgesOnTaskbar", + "TaskbarAlignment", + "TaskViewVisibility", + "ToggleWidgetsButtonVisibility", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + var param = JObject.Parse(value); + + switch (key) + { + case "AutoHideTaskbar": + HandleAutoHideTaskbar(param); + break; + case "DisplaySecondsInSystrayClock": + SetToggle(param, "enable", "ShowSecondsInSystemClock"); + break; + case "DisplayTaskbarOnAllMonitors": + SetToggle(param, "enable", "MMTaskbarEnabled"); + break; + case "ShowBadgesOnTaskbar": + SetToggle(param, "enableBadging", "TaskbarBadges"); + break; + case "TaskbarAlignment": + HandleTaskbarAlignment(param); + break; + case "TaskViewVisibility": + SetToggle(param, "visibility", "ShowTaskViewButton"); + break; + case "ToggleWidgetsButtonVisibility": + SetToggle(param, "visibility", "TaskbarDa", trueValue: "show"); + break; + } + + NotifySettingsChange(); + } + + private static void NotifySettingsChange() + { + try + { + SendNotifyMessage((IntPtr)0xffff, 0x001A, IntPtr.Zero, IntPtr.Zero); + } + catch (EntryPointNotFoundException) + { + // P/Invoke may not be available in all environments + } + } + + 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) + { + // 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"; + // 0 = left, 1 = center + bool useCenter = alignment.Equals("center", StringComparison.OrdinalIgnoreCase); + _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; + } + + _registry.SetValue(ExplorerAdvanced, registryValue, regValue, RegistryValueKind.DWord); + } +} diff --git a/dotnet/autoShell/Handlers/SystemCommandHandler.cs b/dotnet/autoShell/Handlers/SystemCommandHandler.cs new file mode 100644 index 0000000000..57f6447623 --- /dev/null +++ b/dotnet/autoShell/Handlers/SystemCommandHandler.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles system/utility commands: Debug and ToggleNotifications. +/// +internal class SystemCommandHandler : ICommandHandler +{ + private readonly IProcessService _process; + private readonly IDebuggerService _debugger; + + public SystemCommandHandler(IProcessService process, IDebuggerService debugger) + { + _process = process; + _debugger = debugger; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "Debug", + "ToggleNotifications", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "Debug": + _debugger.Launch(); + break; + + case "ToggleNotifications": + _process.StartShellExecute("ms-actioncenter:"); + break; + } + } +} diff --git a/dotnet/autoShell/Handlers/ThemeCommandHandler.cs b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs new file mode 100644 index 0000000000..37203f08bf --- /dev/null +++ b/dotnet/autoShell/Handlers/ThemeCommandHandler.cs @@ -0,0 +1,441 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using autoShell.Services; +using Microsoft.Win32; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles theme-related commands: ApplyTheme, ListThemes, SetThemeMode, and SetWallpaper. +/// Contains all Windows theme management logic including discovery, application, +/// and light/dark mode toggling. +/// +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; + + [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); + + #endregion P/Invoke + + 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 IEnumerable SupportedCommands { get; } = + [ + "ApplyTheme", + "ListThemes", + "SetThemeMode", + "SetWallpaper", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "ApplyTheme": + ApplyTheme(value); + break; + + case "ListThemes": + var themes = GetInstalledThemes(); + Console.WriteLine(JsonConvert.SerializeObject(themes)); + break; + + case "SetThemeMode": + HandleSetThemeMode(value); + break; + + case "SetWallpaper": + _systemParams.SetParameter(SPI_SETDESKWALLPAPER, 0, value, SPIF_UPDATEINIFILE_SENDCHANGE); + break; + } + } + + #region Theme Management + + /// + /// 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.OrdinalIgnoreCase)) + { + _process.StartShellExecute(themePath); + _previousTheme = previous; + return true; + } + else + { + bool success = RevertToPreviousTheme(); + + if (success) + { + _previousTheme = previous; + } + + return success; + } + } + catch + { + return false; + } + } + + /// + /// 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; + } + + /// + /// 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 name of the previous theme. + /// + public string GetPreviousTheme() + { + return _previousTheme; + } + + /// + /// 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; + } + } + + #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); + } + + #endregion + + /// + /// Handles SetThemeMode command. + /// Value can be "light", "dark", "toggle", or a boolean. + /// + 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); + } + } + + /// + /// 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); + } + + /// + /// 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; + } + + /// + /// 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"; + // AppsUseLightTheme: 0 = dark, 1 = light + object value = _registry.GetValue(PersonalizePath, "AppsUseLightTheme"); + return value is int intValue ? intValue == 1 : null; + } + catch + { + return null; + } + } + + /// + /// 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() + { + _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.TryAdd(themeName, themeFile)) + { + // Parse display name from theme file + string displayName = GetThemeDisplayName(themeFile); + if (!string.IsNullOrEmpty(displayName)) + { + _themeDisplayNameDictionary.TryAdd(displayName, themeName); + } + } + } + } + } + + _themeDictionary["previous"] = 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 new file mode 100644 index 0000000000..bc7b908cbc --- /dev/null +++ b/dotnet/autoShell/Handlers/VirtualDesktopCommandHandler.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using autoShell.Logging; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles virtual desktop commands: CreateDesktop, MoveWindowToDesktop, NextDesktop, +/// PinWindow, PreviousDesktop, and SwitchDesktop. +/// +internal class VirtualDesktopCommandHandler : ICommandHandler +{ + private readonly IAppRegistry _appRegistry; + private readonly ILogger _logger; + private readonly IVirtualDesktopService _virtualDesktop; + private readonly IWindowService _window; + + public VirtualDesktopCommandHandler(IAppRegistry appRegistry, IWindowService window, IVirtualDesktopService virtualDesktop, ILogger logger) + { + _appRegistry = appRegistry; + _logger = logger; + _virtualDesktop = virtualDesktop; + _window = window; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "CreateDesktop", + "MoveWindowToDesktop", + "NextDesktop", + "PinWindow", + "PreviousDesktop", + "SwitchDesktop", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "CreateDesktop": + _virtualDesktop.CreateDesktops(value); + break; + + case "MoveWindowToDesktop": + string process = rawValue.SelectToken("process")?.ToString(); + string desktop = rawValue.SelectToken("desktop")?.ToString(); + if (!string.IsNullOrEmpty(process) && !string.IsNullOrEmpty(desktop)) + { + string resolvedName = _appRegistry.ResolveProcessName(process); + IntPtr hWnd = _window.FindProcessWindowHandle(resolvedName); + if (hWnd != IntPtr.Zero) + { + _virtualDesktop.MoveWindowToDesktop(hWnd, desktop); + } + } + break; + + case "NextDesktop": + _virtualDesktop.NextDesktop(); + break; + + case "PinWindow": + string pinProcess = _appRegistry.ResolveProcessName(value); + IntPtr pinHWnd = _window.FindProcessWindowHandle(pinProcess); + if (pinHWnd != IntPtr.Zero) + { + _virtualDesktop.PinWindow(pinHWnd); + } + else + { + _logger.Warning($"The window handle for '{value}' could not be found"); + } + break; + + case "PreviousDesktop": + _virtualDesktop.PreviousDesktop(); + break; + + case "SwitchDesktop": + _virtualDesktop.SwitchDesktop(value); + break; + } + } +} \ No newline at end of file diff --git a/dotnet/autoShell/Handlers/WindowCommandHandler.cs b/dotnet/autoShell/Handlers/WindowCommandHandler.cs new file mode 100644 index 0000000000..c978f1732d --- /dev/null +++ b/dotnet/autoShell/Handlers/WindowCommandHandler.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using autoShell.Services; +using Newtonsoft.Json.Linq; + +namespace autoShell.Handlers; + +/// +/// Handles window management commands: Maximize, Minimize, SwitchTo, and Tile. +/// +internal class WindowCommandHandler : ICommandHandler +{ + private readonly IAppRegistry _appRegistry; + private readonly IWindowService _window; + + public WindowCommandHandler(IAppRegistry appRegistry, IWindowService window) + { + _appRegistry = appRegistry; + _window = window; + } + + /// + public IEnumerable SupportedCommands { get; } = + [ + "Maximize", + "Minimize", + "SwitchTo", + "Tile", + ]; + + /// + public void Handle(string key, string value, JToken rawValue) + { + switch (key) + { + case "Maximize": + string maxProcess = _appRegistry.ResolveProcessName(value); + _window.MaximizeWindow(maxProcess); + break; + + case "Minimize": + string minProcess = _appRegistry.ResolveProcessName(value); + _window.MinimizeWindow(minProcess); + break; + + case "SwitchTo": + string switchProcess = _appRegistry.ResolveProcessName(value); + string path = _appRegistry.GetExecutablePath(value); + _window.RaiseWindow(switchProcess, path); + break; + + case "Tile": + string[] apps = value.Split(','); + if (apps.Length == 2) + { + string processName1 = _appRegistry.ResolveProcessName(apps[0]); + string processName2 = _appRegistry.ResolveProcessName(apps[1]); + _window.TileWindows(processName1, processName2); + } + break; + } + } +} \ No newline at end of file diff --git a/dotnet/autoShell/IAppRegistry.cs b/dotnet/autoShell/IAppRegistry.cs new file mode 100644 index 0000000000..dd96668633 --- /dev/null +++ b/dotnet/autoShell/IAppRegistry.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace autoShell; + +/// +/// 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/Logging/ConsoleLogger.cs b/dotnet/autoShell/Logging/ConsoleLogger.cs new file mode 100644 index 0000000000..7170dd09a7 --- /dev/null +++ b/dotnet/autoShell/Logging/ConsoleLogger.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace autoShell.Logging; + +/// +/// Logger that writes errors and warnings to the console with color formatting, +/// info messages to the console without formatting, and debug messages to the diagnostics output. +/// +internal class ConsoleLogger : ILogger +{ + /// + public void Error(Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + ConsoleColor previousColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Error: " + ex.Message); + Console.ForegroundColor = previousColor; + } + + /// + public void Warning(string message) + { + System.Diagnostics.Debug.WriteLine(message); + ConsoleColor previousColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Warning: " + message); + Console.ForegroundColor = previousColor; + } + + /// + public void Info(string message) + { + System.Diagnostics.Debug.WriteLine(message); + ConsoleColor previousColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("Info: " + message); + Console.ForegroundColor = previousColor; + } + + /// + public void Debug(string message) + { + System.Diagnostics.Debug.WriteLine(message); + } +} diff --git a/dotnet/autoShell/Logging/ILogger.cs b/dotnet/autoShell/Logging/ILogger.cs new file mode 100644 index 0000000000..f0717b903c --- /dev/null +++ b/dotnet/autoShell/Logging/ILogger.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace autoShell.Logging; + +/// +/// Provides logging methods for error, warning, info, and debug messages. +/// +internal interface ILogger +{ + /// + /// Logs an exception as an error. + /// + /// The exception to log. + void Error(Exception ex); + + /// + /// Logs a warning message. + /// + /// The warning message. + void Warning(string message); + + /// + /// Logs an informational message visible to the user. + /// + /// The info message. + void Info(string message); + + /// + /// Logs a debug-level message. + /// + /// The debug message. + void Debug(string message); +} diff --git a/dotnet/autoShell/README.md b/dotnet/autoShell/README.md index 94dd50baae..4926dc298a 100644 --- a/dotnet/autoShell/README.md +++ b/dotnet/autoShell/README.md @@ -11,13 +11,17 @@ AutoShell is part of the [TypeAgent](https://github.com/microsoft/TypeAgent) pro - **Application Management**: Launch, close, and switch between applications using friendly names - **Window Management**: Maximize, minimize, and tile windows side-by-side - **Audio Control**: Set volume levels, mute/unmute, and restore previous volume -- **Theme Management**: List and apply Windows themes -- **Desktop Customization**: Set desktop wallpaper -- **Virtual Desktop Management**: Create new virtual desktops -- **Notification Center**: Toggle the Windows notification center -- **Airplane Mode Control**: Enable or disable Windows airplane mode -- **Wi-Fi Management**: Connect to Wi-Fi networks by SSID -- **Display Resolution**: List available resolutions and change display settings +- **Theme & Personalization**: Apply themes, set wallpaper, toggle transparency, and configure title bar colors +- **Virtual Desktop Management**: Create, switch, pin, and move windows across virtual desktops +- **Display Settings**: Set resolution, brightness, scaling, orientation, color temperature, and blue light filter +- **Network & Connectivity**: Wi-Fi, Bluetooth, airplane mode, and metered connection controls +- **Mouse & Touchpad**: Cursor speed, pointer size, scroll lines, touchpad settings, and cursor trail +- **Taskbar Customization**: Alignment, auto-hide, badges, Task View, Widgets, and multi-monitor display +- **Accessibility**: Narrator, Magnifier, Sticky Keys, Filter Keys, and mono audio +- **Privacy Controls**: Manage camera, microphone, and location access +- **Power Management**: Battery saver levels and power mode configuration +- **System Settings**: Notifications, game mode, focus assist, time settings, and multi-monitor behavior +- **File Explorer**: Toggle file extensions and hidden/system file visibility ## Requirements @@ -40,35 +44,36 @@ Run the application and send JSON commands via stdin: | Command | Parameter | Description | |---------|-----------|-------------| -| `applyTheme` | Theme name | Applies a Windows theme | -| `closeProgram` | Application name | Closes an application | -| `connectWifi` | `{"ssid": "name", "password": "pass"}` | Connects to a Wi-Fi network | -| `createDesktop` | JSON array of names | Creates one or more virtual desktops | -| `disconnectWifi` | (none) | Disconnects from the current Wi-Fi network | -| `launchProgram` | Application name | Opens an application (or raises if already running) | -| `listAppNames` | (none) | Outputs installed applications as JSON | -| `listResolutions` | (none) | Outputs available display resolutions as JSON | -| `listThemes` | (none) | Outputs installed themes as JSON | -| `listWifiNetworks` | (none) | Lists available Wi-Fi networks as JSON | -| `maximize` | Application name | Maximizes the application window | -| `minimize` | Application name | Minimizes the application window | -| `moveWindowToDesktop` | `{"process": "app", "desktop": "name"}` | Moves a window to a specific virtual desktop | -| `mute` | `true`/`false` | Mutes or unmutes system audio | -| `nextDesktop` | (none) | Switches to the next virtual desktop | -| `pinWindow` | Application name | Pins a window to appear on all virtual desktops | -| `previousDesktop` | (none) | Switches to the previous virtual desktop | +| `ApplyTheme` | Theme name | Applies a Windows theme | +| `CloseProgram` | Application name | Closes an application | +| `ConnectWifi` | `{"ssid": "name", "password": "pass"}` | Connects to a Wi-Fi network | +| `CreateDesktop` | JSON array of names | Creates one or more virtual desktops | +| `Debug` | (none) | Launches the debugger | +| `DisconnectWifi` | (none) | Disconnects from the current Wi-Fi network | +| `LaunchProgram` | Application name | Opens an application (or raises if already running) | +| `ListAppNames` | (none) | Outputs installed applications as JSON | +| `ListResolutions` | (none) | Outputs available display resolutions as JSON | +| `ListThemes` | (none) | Outputs installed themes as JSON | +| `ListWifiNetworks` | (none) | Lists available Wi-Fi networks as JSON | +| `Maximize` | Application name | Maximizes the application window | +| `Minimize` | Application name | Minimizes the application window | +| `MoveWindowToDesktop` | `{"process": "app", "desktop": "name"}` | Moves a window to a specific virtual desktop | +| `Mute` | `true`/`false` | Mutes or unmutes system audio | +| `NextDesktop` | (none) | Switches to the next virtual desktop | +| `PinWindow` | Application name | Pins a window to appear on all virtual desktops | +| `PreviousDesktop` | (none) | Switches to the previous virtual desktop | | `quit` | (none) | Exits the application | -| `restoreVolume` | (none) | Restores previously saved volume level | -| `setScreenResolution` | `"WIDTHxHEIGHT"` or `{"width": W, "height": H}` | Sets the display resolution | -| `setTextSize` | `100-225` | Sets system text scaling percentage | -| `setThemeMode` | `"light"`, `"dark"`, `"toggle"`, or boolean | Sets light/dark mode | -| `setWallpaper` | File path | Sets the desktop wallpaper | -| `switchDesktop` | Index or name | Switches to a virtual desktop by index or name | -| `switchTo` | Application name | Brings application window to foreground | -| `tile` | `"app1,app2"` | Tiles two applications side-by-side | -| `toggleAirplaneMode` | `true`/`false` | Enables or disables Windows airplane mode | -| `toggleNotifications` | (none) | Toggles the Windows notification center | -| `volume` | `0-100` | Sets system volume percentage | +| `RestoreVolume` | (none) | Restores previously saved volume level | +| `SetScreenResolution` | `"WIDTHxHEIGHT"` or `{"width": W, "height": H}` | Sets the display resolution | +| `SetTextSize` | `100-225` | Sets system text scaling percentage | +| `SetThemeMode` | `"light"`, `"dark"`, `"toggle"`, or boolean | Sets light/dark mode | +| `SetWallpaper` | File path | Sets the desktop wallpaper | +| `SwitchDesktop` | Index or name | Switches to a virtual desktop by index or name | +| `SwitchTo` | Application name | Brings application window to foreground | +| `Tile` | `"app1,app2"` | Tiles two applications side-by-side | +| `ToggleAirplaneMode` | `true`/`false` | Enables or disables Windows airplane mode | +| `ToggleNotifications` | (none) | Toggles the Windows notification center | +| `Volume` | `0-100` | Sets system volume percentage | #### Settings Commands @@ -77,91 +82,80 @@ Run the application and send JSON commands via stdin: | Command | Parameter | Description | |---------|-----------|-------------| | `BluetoothToggle` | `true`/`false` | Toggles Bluetooth on/off | -| `enableWifi` | `true`/`false` | Enables or disables Wi-Fi | -| `enableMeteredConnections` | `true`/`false` | Enables or disables metered connections | +| `EnableMeteredConnections` | `true`/`false` | Enables or disables metered connections | +| `EnableWifi` | `true`/`false` | Enables or disables Wi-Fi | ##### Display Settings | Command | Parameter | Description | |---------|-----------|-------------| +| `AdjustColorTemperature` | value | Adjusts color temperature | | `AdjustScreenBrightness` | value | Adjusts screen brightness | -| `EnableBlueLightFilterSchedule` | `true`/`false` | Enables or disables blue light filter schedule | -| `adjustColorTemperature` | value | Adjusts color temperature | -| `DisplayScaling` | value | Sets display scaling | | `AdjustScreenOrientation` | value | Adjusts screen orientation | | `DisplayResolutionAndAspectRatio` | value | Sets display resolution and aspect ratio | +| `DisplayScaling` | value | Sets display scaling | +| `EnableBlueLightFilterSchedule` | `true`/`false` | Enables or disables blue light filter schedule | | `RotationLock` | `true`/`false` | Enables or disables rotation lock | ##### Personalization Settings | Command | Parameter | Description | |---------|-----------|-------------| -| `SystemThemeMode` | value | Sets the system theme mode | -| `EnableTransparency` | `true`/`false` | Enables or disables transparency effects | | `ApplyColorToTitleBar` | `true`/`false` | Applies accent color to title bars | +| `EnableTransparency` | `true`/`false` | Enables or disables transparency effects | | `HighContrastTheme` | value | Sets high contrast theme | +| `SystemThemeMode` | value | Sets the system theme mode | ##### Taskbar Settings | Command | Parameter | Description | |---------|-----------|-------------| | `AutoHideTaskbar` | `true`/`false` | Auto-hides the taskbar | +| `DisplaySecondsInSystrayClock` | `true`/`false` | Shows seconds in system tray clock | +| `DisplayTaskbarOnAllMonitors` | `true`/`false` | Displays taskbar on all monitors | +| `ShowBadgesOnTaskbar` | `true`/`false` | Shows or hides badges on taskbar | | `TaskbarAlignment` | value | Sets taskbar alignment | | `TaskViewVisibility` | `true`/`false` | Shows or hides Task View button | | `ToggleWidgetsButtonVisibility` | `true`/`false` | Shows or hides Widgets button | -| `ShowBadgesOnTaskbar` | `true`/`false` | Shows or hides badges on taskbar | -| `DisplayTaskbarOnAllMonitors` | `true`/`false` | Displays taskbar on all monitors | -| `DisplaySecondsInSystrayClock` | `true`/`false` | Shows seconds in system tray clock | -##### Mouse Settings +##### Mouse & Touchpad Settings | Command | Parameter | Description | |---------|-----------|-------------| -| `MouseCursorSpeed` | value | Sets mouse cursor speed | -| `MouseWheelScrollLines` | value | Sets mouse wheel scroll lines | -| `setPrimaryMouseButton` | value | Sets primary mouse button (left/right) | -| `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 - -| Command | Parameter | Description | -|---------|-----------|-------------| | `EnableTouchPad` | `true`/`false` | Enables or disables touchpad | +| `EnhancePointerPrecision` | `true`/`false` | Enables or disables pointer precision | +| `MouseCursorSpeed` | value | Sets mouse cursor speed | +| `MousePointerCustomization` | value | Customizes mouse pointer | +| `MouseWheelScrollLines` | value | Sets mouse wheel scroll lines | +| `SetPrimaryMouseButton` | value | Sets primary mouse button (left/right) | | `TouchpadCursorSpeed` | value | Sets touchpad cursor speed | ##### Privacy Settings | Command | Parameter | Description | |---------|-----------|-------------| -| `ManageMicrophoneAccess` | `true`/`false` | Manages microphone access | | `ManageCameraAccess` | `true`/`false` | Manages camera access | | `ManageLocationAccess` | `true`/`false` | Manages location access | +| `ManageMicrophoneAccess` | `true`/`false` | Manages microphone access | ##### Power Settings | Command | Parameter | Description | |---------|-----------|-------------| | `BatterySaverActivationLevel` | value | Sets battery saver activation level | -| `setPowerModePluggedIn` | value | Sets power mode when plugged in | | `SetPowerModeOnBattery` | value | Sets power mode on battery | - -##### Gaming Settings - -| Command | Parameter | Description | -|---------|-----------|-------------| -| `enableGameMode` | `true`/`false` | Enables or disables game mode | +| `SetPowerModePluggedIn` | value | Sets power mode when plugged in | ##### Accessibility Settings | Command | Parameter | Description | |---------|-----------|-------------| -| `EnableNarratorAction` | `true`/`false` | Enables or disables Narrator | -| `EnableMagnifier` | `true`/`false` | Enables or disables Magnifier | -| `enableStickyKeys` | `true`/`false` | Enables or disables Sticky Keys | | `EnableFilterKeysAction` | `true`/`false` | Enables or disables Filter Keys | +| `EnableMagnifier` | `true`/`false` | Enables or disables Magnifier | +| `EnableNarratorAction` | `true`/`false` | Enables or disables Narrator | +| `EnableStickyKeys` | `true`/`false` | Enables or disables Sticky Keys | | `MonoAudioToggle` | `true`/`false` | Toggles mono audio | ##### File Explorer Settings @@ -171,121 +165,112 @@ Run the application and send JSON commands via stdin: | `ShowFileExtensions` | `true`/`false` | Shows or hides file extensions | | `ShowHiddenAndSystemFiles` | `true`/`false` | Shows or hides hidden and system files | -##### Time & Region Settings +##### System Settings | Command | Parameter | Description | |---------|-----------|-------------| -| `AutomaticTimeSettingAction` | `true`/`false` | Enables or disables automatic time setting | | `AutomaticDSTAdjustment` | `true`/`false` | Enables or disables automatic DST adjustment | - -##### Focus Assist Settings - -| Command | Parameter | Description | -|---------|-----------|-------------| +| `AutomaticTimeSettingAction` | `true`/`false` | Enables or disables automatic time setting | +| `EnableGameMode` | `true`/`false` | Enables or disables game mode | | `EnableQuietHours` | `true`/`false` | Enables or disables quiet hours | - -##### Multi-Monitor Settings - -| Command | Parameter | Description | -|---------|-----------|-------------| -| `RememberWindowLocations` | `true`/`false` | Remembers window locations per monitor | | `MinimizeWindowsOnMonitorDisconnectAction` | `true`/`false` | Minimizes windows when monitor disconnects | +| `RememberWindowLocations` | `true`/`false` | Remembers window locations per monitor | ### Examples Launch a program: ```json -{"launchProgram": "notepad"} +{"LaunchProgram": "notepad"} ``` Set the system volume at 50%: ```json -{"volume": 50} +{"Volume": 50} ``` Tile notepad on the left and calculator on the right of the screen: ```json -{"tile": "notepad,calculator"} +{"Tile": "notepad,calculator"} ``` Apply the 'dark' Windows theme: ```json -{"applyTheme": "dark"} +{"ApplyTheme": "dark"} ``` Set dark mode: ```json -{"setThemeMode": "dark"} +{"SetThemeMode": "dark"} ``` Toggle between light and dark mode: ```json -{"setThemeMode": "toggle"} +{"SetThemeMode": "toggle"} ``` Mute the system audio: ```json -{"mute": true} +{"Mute": true} ``` Set the desktop wallpaper and then quit AutoShell: ```json -{"setWallpaper": "C:\\Users\\Public\\Pictures\\wallpaper.jpg"} {"quit": true} +{"SetWallpaper": "C:\\Users\\Public\\Pictures\\wallpaper.jpg"} {"quit": true} ``` Create a new virtual desktop named "Design Work": ```json -{"createDesktop": "Design Work"} +{"CreateDesktop": "Design Work"} ``` Toggle the Windows notification center: ```json -{"toggleNotifications": true} +{"ToggleNotifications": true} ``` Enable airplane mode: ```json -{"toggleAirplaneMode": true} +{"ToggleAirplaneMode": true} ``` Disable airplane mode: ```json -{"toggleAirplaneMode": false} +{"ToggleAirplaneMode": false} ``` List available Wi-Fi networks: ```json -{"listWifiNetworks": true} +{"ListWifiNetworks": true} ``` Connect to a Wi-Fi network: ```json -{"connectWifi": {"ssid": "MyNetwork", "password": "MyPassword123"}} +{"ConnectWifi": {"ssid": "MyNetwork", "password": "MyPassword123"}} ``` Set system text size to 125%: ```json -{"setTextSize": 125} +{"SetTextSize": 125} ``` List available display resolutions: ```json -{"listResolutions": true} +{"ListResolutions": true} ``` Set display resolution to 1920x1080: ```json -{"setScreenResolution": "1920x1080"} +{"SetScreenResolution": "1920x1080"} ``` Set display resolution with specific refresh rate: ```json -{"setScreenResolution": "1920x1080@144"} +{"SetScreenResolution": "1920x1080@144"} ``` Set display resolution using JSON object: ```json -{"setScreenResolution": {"width": 2560, "height": 1440, "refreshRate": 60}} +{"SetScreenResolution": {"width": 2560, "height": 1440, "refreshRate": 60}} ``` Enable cursor trail with length 7: @@ -315,13 +300,60 @@ Additionally, AutoShell automatically discovers all installed Windows Store appl ## Architecture -The application is structured as a partial class across multiple files: +AutoShell uses a handler-based architecture with dependency injection. All platform-specific (P/Invoke, COM, WMI) code is isolated behind service interfaces, keeping handlers thin and fully unit-testable. + +``` +autoShell/ +├── AutoShell.cs # Entry point — stdin/stdout command loop +├── CommandDispatcher.cs # Routes JSON keys to handlers; Create() wires all dependencies +├── IAppRegistry.cs # App registry interface (shared across handlers) +├── WindowsAppRegistry.cs # Maps friendly app names to paths and AppUserModelIDs +├── Handlers/ +│ ├── ICommandHandler.cs # Handler interface (SupportedCommands + Handle) +│ ├── AppCommandHandler.cs # LaunchProgram, CloseProgram, ListAppNames +│ ├── AudioCommandHandler.cs # Volume, Mute, RestoreVolume +│ ├── DisplayCommandHandler.cs # SetScreenResolution, ListResolutions, SetTextSize +│ ├── NetworkCommandHandler.cs # ConnectWifi, ToggleAirplaneMode, etc. +│ ├── SystemCommandHandler.cs # Debug, ToggleNotifications +│ ├── ThemeCommandHandler.cs # ApplyTheme, ListThemes, SetThemeMode, SetWallpaper +│ ├── VirtualDesktopCommandHandler.cs # CreateDesktop, SwitchDesktop, PinWindow, etc. +│ ├── WindowCommandHandler.cs # Maximize, Minimize, SwitchTo, Tile +│ └── Settings/ +│ ├── AccessibilitySettingsHandler.cs +│ ├── DisplaySettingsHandler.cs +│ ├── FileExplorerSettingsHandler.cs +│ ├── MouseSettingsHandler.cs +│ ├── PersonalizationSettingsHandler.cs +│ ├── PowerSettingsHandler.cs +│ ├── PrivacySettingsHandler.cs +│ ├── SystemSettingsHandler.cs +│ └── TaskbarSettingsHandler.cs +├── Services/ # Interfaces + Windows implementations +│ ├── IAudioService.cs / WindowsAudioService.cs +│ ├── IBrightnessService.cs / WindowsBrightnessService.cs +│ ├── IDebuggerService.cs / WindowsDebuggerService.cs +│ ├── IDisplayService.cs / WindowsDisplayService.cs +│ ├── INetworkService.cs / WindowsNetworkService.cs +│ ├── IProcessService.cs / WindowsProcessService.cs +│ ├── IRegistryService.cs / WindowsRegistryService.cs +│ ├── ISystemParametersService.cs / WindowsSystemParametersService.cs +│ ├── IVirtualDesktopService.cs / WindowsVirtualDesktopService.cs +│ ├── IWindowService.cs / WindowsWindowService.cs +│ └── Interop/ +│ ├── CoreAudioInterop.cs # COM interop definitions for Windows audio +│ └── UIAutomation.cs # UI Automation helpers (last-resort) +├── Logging/ +│ ├── ILogger.cs # Logging interface (Error, Warning, Debug) +│ └── ConsoleLogger.cs # Colored console + diagnostics output +└── autoShell.Tests/ # 165 unit tests (Moq-based, all services mocked) +``` + +### Key design decisions -- `AutoShell.cs` - Main logic, application management, audio control -- `AutoShell_Themes.cs` - Windows theme management -- `AutoShell_Win32.cs` - Win32 API P/Invoke declarations -- `AutoShell_Settings.cs` - Windows settings management -- `UIAutomation.cs` - UI Automation helpers +- **CommandDispatcher.Create()** is the composition root — it creates all concrete services and wires them into handlers. Tests bypass this and inject mocks directly. +- **Handlers are thin** — they parse JSON parameters and delegate to services. No P/Invoke or COM code lives in handlers. +- **Services own all platform calls** — P/Invoke, COM, WMI, and registry access are encapsulated behind interfaces (`I*Service` / `Windows*Service`). +- **ILogger** abstracts all diagnostic output. `ConsoleLogger` preserves the original colored error/warning formatting. ## License 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/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/IDisplayService.cs b/dotnet/autoShell/Services/IDisplayService.cs new file mode 100644 index 0000000000..c91adf3e60 --- /dev/null +++ b/dotnet/autoShell/Services/IDisplayService.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace autoShell.Services; + +/// +/// Abstracts display resolution and text-scaling operations for testability. +/// +internal interface IDisplayService +{ + /// + /// Lists all unique display resolutions as a JSON string. + /// + string ListResolutions(); + + /// + /// Sets the display resolution. Returns a status message. + /// + string SetResolution(uint width, uint height, uint? refreshRate = null); + + /// + /// Sets the text scaling percentage via UIAutomation. + /// + void SetTextSize(int percentage); +} diff --git a/dotnet/autoShell/Services/INetworkService.cs b/dotnet/autoShell/Services/INetworkService.cs new file mode 100644 index 0000000000..fd8ffdf747 --- /dev/null +++ b/dotnet/autoShell/Services/INetworkService.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace autoShell.Services; + +/// +/// Abstracts WiFi and airplane-mode operations for testability. +/// +internal interface INetworkService +{ + /// + /// Connects to a WiFi network by SSID and optional password. + /// + void ConnectToWifi(string ssid, string password); + + /// + /// Disconnects from the currently connected WiFi network. + /// + void DisconnectFromWifi(); + + /// + /// Enables or disables the Wi-Fi network interface. + /// + void EnableWifi(bool enable); + + /// + /// Lists available WiFi networks as a JSON string. + /// + string ListWifiNetworks(); + + /// + /// Sets the airplane mode state. + /// + void SetAirplaneMode(bool enable); + + /// + /// Toggles Bluetooth radio on or off. + /// + void ToggleBluetooth(bool enable); +} 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..fa77eac007 --- /dev/null +++ b/dotnet/autoShell/Services/IRegistryService.cs @@ -0,0 +1,42 @@ +// 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); + + /// + /// 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); + + /// + /// Broadcasts a WM_SETTINGCHANGE message to notify the system of a setting change. + /// + /// The setting name to broadcast (e.g., "ImmersiveColorSet"), or null for a generic notification. + void BroadcastSettingChange(string setting = null); +} diff --git a/dotnet/autoShell/Services/ISystemParametersService.cs b/dotnet/autoShell/Services/ISystemParametersService.cs new file mode 100644 index 0000000000..d0bf38193f --- /dev/null +++ b/dotnet/autoShell/Services/ISystemParametersService.cs @@ -0,0 +1,54 @@ +// 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); + + /// + /// 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. + /// + /// 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); + + /// + /// 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/IVirtualDesktopService.cs b/dotnet/autoShell/Services/IVirtualDesktopService.cs new file mode 100644 index 0000000000..c3797507d5 --- /dev/null +++ b/dotnet/autoShell/Services/IVirtualDesktopService.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace autoShell.Services; + +/// +/// Abstracts Windows virtual desktop COM operations for testability. +/// +internal interface IVirtualDesktopService +{ + /// + /// Creates one or more virtual desktops from a JSON array of names. + /// + void CreateDesktops(string jsonDesktopNames); + + /// + /// Moves a window to the specified desktop by index (1-based) or name. + /// + void MoveWindowToDesktop(IntPtr hWnd, string desktopIdentifier); + + /// + /// Switches to the next virtual desktop. + /// + void NextDesktop(); + + /// + /// Pins a window (by handle) to all desktops. + /// + void PinWindow(IntPtr hWnd); + + /// + /// Switches to the previous virtual desktop. + /// + void PreviousDesktop(); + + /// + /// Switches to a virtual desktop by index or name. + /// + void SwitchDesktop(string desktopIdentifier); +} diff --git a/dotnet/autoShell/Services/IWindowService.cs b/dotnet/autoShell/Services/IWindowService.cs new file mode 100644 index 0000000000..2ee4b4022a --- /dev/null +++ b/dotnet/autoShell/Services/IWindowService.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace autoShell.Services; + +/// +/// Abstracts Win32 window-management operations for testability. +/// +internal interface IWindowService +{ + /// + /// Maximizes a window by process name or window title. + /// + void MaximizeWindow(string processName); + + /// + /// Minimizes a window by process name or window title. + /// + void MinimizeWindow(string processName); + + /// + /// Brings a window to the foreground by process name, launching it if needed. + /// + void RaiseWindow(string processName, string executablePath); + + /// + /// Tiles two windows side by side. + /// + void TileWindows(string processName1, string processName2); + + /// + /// Finds a window handle by process name, falling back to title search. + /// + /// The process name or window title to search for. + /// The window handle if found; otherwise IntPtr.Zero. + IntPtr FindProcessWindowHandle(string processName); +} diff --git a/dotnet/autoShell/Services/Interop/CoreAudioInterop.cs b/dotnet/autoShell/Services/Interop/CoreAudioInterop.cs new file mode 100644 index 0000000000..da94e3ec32 --- /dev/null +++ b/dotnet/autoShell/Services/Interop/CoreAudioInterop.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; + +namespace autoShell.Services.Interop; +// 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 ERole +{ + eConsole = 0, + eMultimedia = 1, + eCommunications = 2 +} + +[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("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); +} diff --git a/dotnet/autoShell/UIAutomation.cs b/dotnet/autoShell/Services/Interop/UIAutomation.cs similarity index 78% rename from dotnet/autoShell/UIAutomation.cs rename to dotnet/autoShell/Services/Interop/UIAutomation.cs index 5a78c70a8d..0e441244a0 100644 --- a/dotnet/autoShell/UIAutomation.cs +++ b/dotnet/autoShell/Services/Interop/UIAutomation.cs @@ -2,15 +2,12 @@ // 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 autoShell.Logging; using UIAutomationClient = Interop.UIAutomationClient; -namespace autoShell; +namespace autoShell.Services.Interop; /// /// This is a placeholder for UIAutomation related functionalities. @@ -19,11 +16,38 @@ 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); + + [DllImport("user32.dll")] + private static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName); + + [DllImport("user32.dll")] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + #endregion P/Invoke + /// /// Uses UI Automation to navigate the Settings app and set the text size. /// /// The text scaling percentage (100-225). - internal static void SetTextSizeViaUIAutomation(int percentage) + internal static void SetTextSizeViaUIAutomation(int percentage, ILogger logger) { // UI Automation Property IDs (from UIAutomationClient.h) const int UIA_AutomationIdPropertyId = 30011; @@ -31,8 +55,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,17 +65,17 @@ 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 IntPtr hWnd = IntPtr.Zero; while ((hWnd = - AutoShell.FindWindowEx(IntPtr.Zero, hWnd, "ApplicationFrameWindow", null)) != IntPtr.Zero) + FindWindowEx(IntPtr.Zero, hWnd, "ApplicationFrameWindow", null)) != IntPtr.Zero) { StringBuilder windowTitle = new StringBuilder(256); - int hr = AutoShell.GetWindowText(hWnd, windowTitle, windowTitle.Capacity); - Debug.WriteLine(windowTitle + $"(hResult: {hr})"); + int hr = GetWindowText(hWnd, windowTitle, windowTitle.Capacity); + logger.Debug(windowTitle + $"(hResult: {hr})"); if (windowTitle.ToString().Contains("Settings", StringComparison.OrdinalIgnoreCase)) { // Get the automation element directly from the window handle @@ -65,31 +89,31 @@ internal static void SetTextSizeViaUIAutomation(int percentage) break; } - System.Threading.Thread.Sleep(retryDelayMs); + System.Threading.Thread.Sleep(RetryDelayMs); } if (settingsWindow == null) { - AutoShell.LogWarning("Could not find Settings window."); + logger.Warning("Could not find Settings window."); return; } - Debug.WriteLine("Found Settings window via FindWindowEx"); + logger.Debug("Found Settings window via FindWindowEx"); // Wait a moment for the UI to fully load System.Threading.Thread.Sleep(500); // Find and click the "Text Size" navigation item - var textSizeNavItem = FindTextSizeNavigationItem(uiAutomation, settingsWindow); + var textSizeNavItem = FindTextSizeNavigationItem(uiAutomation, settingsWindow, logger); if (textSizeNavItem != null) { - Debug.WriteLine("Found Text Size navigation item, clicking..."); - ClickElement(textSizeNavItem); + logger.Debug("Found Text Size navigation item, clicking..."); + ClickElement(textSizeNavItem, logger); System.Threading.Thread.Sleep(500); // Wait for page to load } else { - Debug.WriteLine("Text Size navigation item not found, may already be on the page"); + logger.Debug("Text Size navigation item not found, may already be on the page"); } // Find the text size slider @@ -98,7 +122,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,16 +133,16 @@ internal static void SetTextSizeViaUIAutomation(int percentage) break; } - System.Threading.Thread.Sleep(retryDelayMs); + System.Threading.Thread.Sleep(RetryDelayMs); } if (slider == null) { - AutoShell.LogWarning("Could not find text size slider."); + logger.Warning("Could not find text size slider."); return; } - Debug.WriteLine("Found text size slider"); + logger.Debug("Found text size slider"); // Set the slider value using RangeValue pattern var rangeValuePattern = (UIAutomationClient.IUIAutomationRangeValuePattern)slider.GetCurrentPattern( @@ -126,12 +150,12 @@ internal static void SetTextSizeViaUIAutomation(int percentage) if (rangeValuePattern != null) { - Debug.WriteLine($"Setting slider value to {percentage}"); + logger.Debug($"Setting slider value to {percentage}"); rangeValuePattern.SetValue(percentage); } else { - AutoShell.LogWarning("Slider does not support RangeValue pattern."); + logger.Warning("Slider does not support RangeValue pattern."); return; } @@ -157,7 +181,7 @@ internal static void SetTextSizeViaUIAutomation(int percentage) } catch (Exception ex) { - Debug.WriteLine($"Error simulating input on slider: {ex.Message}"); + logger.Debug($"Error simulating input on slider: {ex.Message}"); } // Find and click the Apply button @@ -166,7 +190,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,32 +201,33 @@ internal static void SetTextSizeViaUIAutomation(int percentage) break; } - System.Threading.Thread.Sleep(retryDelayMs); + System.Threading.Thread.Sleep(RetryDelayMs); } if (applyButton != null) { - Debug.WriteLine("Found Apply button, clicking..."); - ClickElement(applyButton); - Console.WriteLine($"Text size set to {percentage}%"); + logger.Debug("Found Apply button, clicking..."); + ClickElement(applyButton, logger); + logger.Info($"Text size set to {percentage}%"); } else { - AutoShell.LogWarning("Could not find Apply button. The setting may need to be applied manually."); + logger.Warning("Could not find Apply button. The setting may need to be applied manually."); } } catch (Exception ex) { - AutoShell.LogError(ex); + logger.Error(ex); } } /// /// 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) + UIAutomationClient.IUIAutomationElement settingsWindow, + ILogger logger) { // UI Automation Property IDs const int UIA_NamePropertyId = 30005; @@ -246,7 +271,7 @@ static UIAutomationClient.IUIAutomationElement FindTextSizeNavigationItem( } catch (Exception ex) { - Debug.WriteLine($"Error finding Text Size navigation item: {ex.Message}"); + logger.Debug($"Error finding Text Size navigation item: {ex.Message}"); } return null; @@ -255,7 +280,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, ILogger logger) { // UI Automation Pattern IDs const int UIA_InvokePatternId = 10000; @@ -295,25 +320,7 @@ static void ClickElement(UIAutomationClient.IUIAutomationElement element) } catch (Exception ex) { - Debug.WriteLine($"Error clicking element: {ex.Message}"); + logger.Debug($"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 diff --git a/dotnet/autoShell/Services/WindowsAudioService.cs b/dotnet/autoShell/Services/WindowsAudioService.cs new file mode 100644 index 0000000000..3c9fdc9414 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsAudioService.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using autoShell.Logging; +using autoShell.Services.Interop; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IAudioService using Windows Core Audio COM API. +/// +internal class WindowsAudioService : IAudioService +{ + private readonly ILogger _logger; + + public WindowsAudioService(ILogger logger) + { + _logger = logger; + } + + /// + 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) + { + _logger.Debug("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) + { + _logger.Debug("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) + { + _logger.Debug("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) + { + _logger.Debug("Failed to get mute: " + ex.Message); + return false; + } + } +} diff --git a/dotnet/autoShell/Services/WindowsBrightnessService.cs b/dotnet/autoShell/Services/WindowsBrightnessService.cs new file mode 100644 index 0000000000..711af9982a --- /dev/null +++ b/dotnet/autoShell/Services/WindowsBrightnessService.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq; +using autoShell.Logging; +using Microsoft.Win32; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IBrightnessService using WMI and Windows Registry. +/// +internal class WindowsBrightnessService : IBrightnessService +{ + private readonly ILogger _logger; + + public WindowsBrightnessService(ILogger logger) + { + _logger = logger; + } + + /// + 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) + { + _logger.Debug($"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/WindowsDisplayService.cs b/dotnet/autoShell/Services/WindowsDisplayService.cs new file mode 100644 index 0000000000..8c5b00b441 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsDisplayService.cs @@ -0,0 +1,212 @@ +// 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 autoShell.Logging; +using autoShell.Services.Interop; +using Newtonsoft.Json; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IDisplayService using Win32 P/Invoke and UIAutomation. +/// +internal class WindowsDisplayService : IDisplayService +{ + #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 + + private readonly ILogger _logger; + + public WindowsDisplayService(ILogger logger) + { + _logger = logger; + } + + /// + public string ListResolutions() + { + 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(); + + return JsonConvert.SerializeObject(uniqueResolutions); + } + + /// + public string SetResolution(uint width, uint height, uint? refreshRate = null) + { + DEVMODE currentMode = new DEVMODE(); + currentMode.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE)); + + if (!EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref currentMode)) + { + return "Failed to get current display settings."; + } + + 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) + { + return $"Resolution {width}x{height}" + (refreshRate.HasValue ? $"@{refreshRate}Hz" : "") + " is not supported."; + } + + 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) + { + return $"Display mode test failed with code: {testResult}"; + } + + int result = ChangeDisplaySettings(ref bestMatch, CDS_UPDATEREGISTRY); + return result switch + { + DISP_CHANGE_SUCCESSFUL => $"Resolution changed to {bestMatch.dmPelsWidth}x{bestMatch.dmPelsHeight}@{bestMatch.dmDisplayFrequency}Hz", + DISP_CHANGE_RESTART => $"Resolution will change to {bestMatch.dmPelsWidth}x{bestMatch.dmPelsHeight} after restart.", + _ => $"Failed to change resolution. Error code: {result}", + }; + } + + /// + public void SetTextSize(int percentage) + { + 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, _logger); +#pragma warning restore CS0618 + } +} diff --git a/dotnet/autoShell/Services/WindowsNetworkService.cs b/dotnet/autoShell/Services/WindowsNetworkService.cs new file mode 100644 index 0000000000..8dc025dd42 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsNetworkService.cs @@ -0,0 +1,560 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using autoShell.Logging; +using Newtonsoft.Json; + +namespace autoShell.Services; + +/// +/// Concrete implementation of INetworkService using Windows WLAN API and Radio Management COM API. +/// +internal class WindowsNetworkService : INetworkService +{ + #region COM / P/Invoke + + 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")] + [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 pDOT11_SSID, 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 pDOT11_SSID; + 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 + + private readonly ILogger _logger; + + public WindowsNetworkService(ILogger logger) + { + _logger = logger; + } + + /// + public void ConnectToWifi(string ssid, string password) + { + IntPtr clientHandle = IntPtr.Zero; + IntPtr wlanInterfaceList = IntPtr.Zero; + + try + { + int result = WlanOpenHandle(2, IntPtr.Zero, out uint negotiatedVersion, out clientHandle); + if (result != 0) + { + _logger.Warning($"Failed to open WLAN handle: {result}"); + return; + } + + result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); + if (result != 0) + { + _logger.Warning($"Failed to enumerate WLAN interfaces: {result}"); + return; + } + + WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + + if (interfaceList.dwNumberOfItems == 0) + { + _logger.Warning("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) + { + _logger.Warning($"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) + { + _logger.Warning($"Failed to connect to WiFi network '{ssid}': {result}"); + return; + } + + _logger.Info($"Connecting to WiFi network: {ssid}"); + } + catch (Exception ex) + { + _logger.Error(ex); + } + finally + { + if (wlanInterfaceList != IntPtr.Zero) + { + WlanFreeMemory(wlanInterfaceList); + } + + if (clientHandle != IntPtr.Zero) + { + _ = WlanCloseHandle(clientHandle, IntPtr.Zero); + } + } + } + + /// + public 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) + { + _logger.Warning($"Failed to open WLAN handle: {result}"); + return; + } + + result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); + if (result != 0) + { + _logger.Warning($"Failed to enumerate WLAN interfaces: {result}"); + return; + } + + WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + + if (interfaceList.dwNumberOfItems == 0) + { + _logger.Warning("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) + { + _logger.Warning($"Failed to disconnect from WiFi on interface {i}: {result}"); + } + else + { + _logger.Info("Disconnected from WiFi"); + } + } + } + catch (Exception ex) + { + _logger.Error(ex); + } + finally + { + if (wlanInterfaceList != IntPtr.Zero) + { + WlanFreeMemory(wlanInterfaceList); + } + + if (clientHandle != IntPtr.Zero) + { + _ = WlanCloseHandle(clientHandle, IntPtr.Zero); + } + } + } + + /// + public string 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) + { + _logger.Debug($"Failed to open WLAN handle: {result}"); + return "[]"; + } + + result = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out wlanInterfaceList); + if (result != 0) + { + _logger.Debug($"Failed to enumerate WLAN interfaces: {result}"); + return "[]"; + } + + WLAN_INTERFACE_INFO_LIST interfaceList = Marshal.PtrToStructure(wlanInterfaceList); + + if (interfaceList.dwNumberOfItems == 0) + { + 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); + + // Small delay to allow scan to complete + System.Threading.Thread.Sleep(100); + + result = WlanGetAvailableNetworkList(clientHandle, ref interfaceInfo.InterfaceGuid, 0, IntPtr.Zero, out networkList); + if (result != 0) + { + _logger.Debug($"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(); + + return JsonConvert.SerializeObject(uniqueNetworks); + } + catch (Exception ex) + { + _logger.Debug($"Error listing WiFi networks: {ex.Message}"); + return "[]"; + } + finally + { + if (networkList != IntPtr.Zero) + { + WlanFreeMemory(networkList); + } + + if (wlanInterfaceList != IntPtr.Zero) + { + WlanFreeMemory(wlanInterfaceList); + } + + if (clientHandle != IntPtr.Zero) + { + _ = WlanCloseHandle(clientHandle, IntPtr.Zero); + } + } + } + + /// + public void SetAirplaneMode(bool enable) + { + IRadioManager radioManager = null; + try + { + Type radioManagerType = Type.GetTypeFromCLSID(s_clsidRadioManagementApi); + if (radioManagerType == null) + { + _logger.Debug("Failed to get Radio Management API type"); + return; + } + + object obj = Activator.CreateInstance(radioManagerType); + radioManager = (IRadioManager)obj; + + if (radioManager == null) + { + _logger.Debug("Failed to create Radio Manager instance"); + return; + } + + int hr = radioManager.GetSystemRadioState(out int currentState, out int _, out int _); + if (hr < 0) + { + _logger.Debug($"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; + _logger.Debug($"Current airplane mode state: {(airplaneModeCurrentlyOn ? "on" : "off")}"); + + // 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) + { + _logger.Debug($"Failed to set system radio state: HRESULT 0x{hr:X8}"); + return; + } + + _logger.Debug($"Airplane mode set to: {(enable ? "on" : "off")}"); + } + catch (COMException ex) + { + _logger.Debug($"COM Exception setting airplane mode: {ex.Message} (HRESULT: 0x{ex.HResult:X8})"); + } + catch (Exception ex) + { + _logger.Debug($"Failed to set airplane mode: {ex.Message}"); + } + finally + { + if (radioManager != null) + { + Marshal.ReleaseComObject(radioManager); + } + } + } + + /// + public void EnableWifi(bool enable) + { + string command = enable ? "interface set interface \"Wi-Fi\" enabled" : + "interface set interface \"Wi-Fi\" disabled"; + try + { + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = "netsh", + Arguments = command, + CreateNoWindow = true, + UseShellExecute = false + }; + System.Diagnostics.Process.Start(psi)?.WaitForExit(); + _logger.Debug($"WiFi set to: {(enable ? "enabled" : "disabled")}"); + } + catch (Exception ex) + { + _logger.Debug($"Failed to set WiFi state: {ex.Message}"); + } + } + + /// + /// + /// Uses the registry instead of the IRadioManager COM API because + /// IRadioManager controls all radios. For Bluetooth-specific control, + /// we'd need IRadioInstanceCollection, but the registry approach is more reliable. + /// + public void ToggleBluetooth(bool enable) + { + try + { + using var key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey( + @"SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters\Radio Support"); + key?.SetValue("SupportDLL", enable ? 1 : 0, Microsoft.Win32.RegistryValueKind.DWord); + _logger.Debug($"Bluetooth set to: {(enable ? "on" : "off")}"); + } + catch (Exception ex) + { + _logger.Debug($"Failed to toggle Bluetooth: {ex.Message}"); + } + } + + /// + /// 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} + + + +"; + } +} 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..6c5949ef89 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsRegistryService.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +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); + } + + /// + public void SetValueLocalMachine(string keyPath, string valueName, object value, RegistryValueKind valueKind) + { + using var key = Registry.LocalMachine.CreateSubKey(keyPath); + key?.SetValue(valueName, value, valueKind); + } + + /// + public void BroadcastSettingChange(string setting = null) + { + const int HWND_BROADCAST = 0xffff; + const uint WM_SETTINGCHANGE = 0x001A; + const uint SMTO_ABORTIFHUNG = 0x0002; + SendMessageTimeout( + (IntPtr)HWND_BROADCAST, + WM_SETTINGCHANGE, + IntPtr.Zero, + setting, + SMTO_ABORTIFHUNG, + 1000, + out _); + } + + [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)] + private static extern IntPtr SendMessageTimeout( + IntPtr hWnd, uint Msg, IntPtr wParam, string lParam, + uint fuFlags, uint uTimeout, out IntPtr lpdwResult); +} diff --git a/dotnet/autoShell/Services/WindowsSystemParametersService.cs b/dotnet/autoShell/Services/WindowsSystemParametersService.cs new file mode 100644 index 0000000000..d2a2e09586 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsSystemParametersService.cs @@ -0,0 +1,58 @@ +// 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 +{ + [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 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) + { + 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/dotnet/autoShell/Services/WindowsVirtualDesktopService.cs b/dotnet/autoShell/Services/WindowsVirtualDesktopService.cs new file mode 100644 index 0000000000..1c1bccb31b --- /dev/null +++ b/dotnet/autoShell/Services/WindowsVirtualDesktopService.cs @@ -0,0 +1,503 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; +using System.Windows; +using autoShell.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IVirtualDesktopService using Windows COM APIs. +/// +internal class WindowsVirtualDesktopService : IVirtualDesktopService +{ + #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 IServiceProvider10 _shell; + private readonly IVirtualDesktopManagerInternal _virtualDesktopManagerInternal; + private readonly IVirtualDesktopManagerInternal_BUGBUG _virtualDesktopManagerInternal_BUGBUG; + private readonly IVirtualDesktopManager _virtualDesktopManager; + private readonly IApplicationViewCollection _applicationViewCollection; + private readonly IVirtualDesktopPinnedApps _virtualDesktopPinnedApps; + private readonly ILogger _logger; + + public WindowsVirtualDesktopService(ILogger logger) + { + _logger = logger; + _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); + } + + /// + public void CreateDesktops(string jsonDesktopNames) + { + try + { + JArray desktopNames = JArray.Parse(jsonDesktopNames); + + if (desktopNames.Count == 0) + { + desktopNames = ["desktop X"]; + } + + if (_virtualDesktopManagerInternal == null) + { + _logger.Debug($"Failed to get Virtual Desktop Manager Internal"); + return; + } + + foreach (JToken desktopNameToken in desktopNames) + { + string desktopName = desktopNameToken.ToString(); + + try + { + IVirtualDesktop newDesktop = _virtualDesktopManagerInternal.CreateDesktop(); + + if (newDesktop != null) + { + try + { + // TODO: debug & get working + // Works in .NET framework but not .NET + //s_virtualDesktopManagerInternal_BUGBUG.SetDesktopName(newDesktop, desktopName); + } + catch (Exception ex2) + { + _logger.Debug($"Created virtual desktop (naming not supported on this Windows version): {ex2.Message}"); + } + } + } + catch (Exception ex) + { + _logger.Debug($"Failed to create desktop '{desktopName}': {ex.Message}"); + } + } + } + catch (JsonException ex) + { + _logger.Debug($"Failed to parse desktop names JSON: {ex.Message}"); + } + catch (Exception ex) + { + _logger.Debug($"Error creating desktops: {ex.Message}"); + } + } + + /// + /// + /// Currently may return ACCESS_DENIED on some configurations. TODO: investigate. + /// + public void MoveWindowToDesktop(IntPtr hWnd, string desktopIdentifier) + { + if (hWnd == IntPtr.Zero) + { + _logger.Debug("Invalid window handle"); + return; + } + + if (string.IsNullOrEmpty(desktopIdentifier)) + { + _logger.Debug("No desktop id supplied"); + return; + } + + if (int.TryParse(desktopIdentifier, out int desktopIndex)) + { + _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + if (desktopIndex < 1 || desktopIndex > _virtualDesktopManagerInternal.GetCount()) + { + _logger.Debug("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(desktopIdentifier); + if (ivd is not null) + { + Guid desktopGuid = ivd.GetId(); + _virtualDesktopManager.MoveWindowToDesktop(hWnd, ref desktopGuid); + } + } + + /// + public void NextDesktop() + { + BumpDesktopIndex(1); + } + + /// + public void PinWindow(IntPtr hWnd) + { + if (hWnd != IntPtr.Zero) + { + _applicationViewCollection.GetViewForHwnd(hWnd, out IApplicationView view); + + if (view is not null) + { + _virtualDesktopPinnedApps.PinView(view); + } + } + else + { + _logger.Warning("The window handle could not be found"); + } + } + + /// + public void PreviousDesktop() + { + BumpDesktopIndex(-1); + } + + /// + public void SwitchDesktop(string desktopIdentifier) + { + if (!int.TryParse(desktopIdentifier, out int index)) + { + _virtualDesktopManagerInternal.SwitchDesktop(FindDesktopByName(desktopIdentifier)); + } + else + { + SwitchDesktopByIndex(index); + } + } + + private void SwitchDesktopByIndex(int index) + { + _virtualDesktopManagerInternal.GetDesktops(out IObjectArray desktops); + desktops.GetAt(index, typeof(IVirtualDesktop).GUID, out object od); + + // BUGBUG: different Windows versions use different COM interfaces for desktop switching. + // Windows 11 22H2 (build 22621) and later use the updated "BUGBUG" interface. + if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22621)) + { + _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) + { + _logger.Debug("Unable to get the index of the current desktop"); + return; + } + + index += bump; + + if (index > count) + { + index = 0; + } + else if (index < 0) + { + index = count - 1; + } + + SwitchDesktopByIndex(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 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; + } +} diff --git a/dotnet/autoShell/Services/WindowsWindowService.cs b/dotnet/autoShell/Services/WindowsWindowService.cs new file mode 100644 index 0000000000..b3a0e505f8 --- /dev/null +++ b/dotnet/autoShell/Services/WindowsWindowService.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using autoShell.Logging; +using Microsoft.VisualBasic; + +namespace autoShell.Services; + +/// +/// Concrete implementation of IWindowService using Win32 P/Invoke. +/// +internal class WindowsWindowService : IWindowService +{ + #region P/Invoke + + private const uint WM_SYSCOMMAND = 0x112; + private const uint SC_MAXIMIZE = 0xF030; + private const uint SC_MINIMIZE = 0xF020; + + private 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")] + private static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName); + + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + private 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 ILogger _logger; + + public WindowsWindowService(ILogger logger) + { + _logger = logger; + } + + /// + public void MaximizeWindow(string processName) + { + 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); + } + } + + /// + public void MinimizeWindow(string processName) + { + 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); + } + } + + /// + public void RaiseWindow(string processName, string executablePath) + { + 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. + if (executablePath != null) + { + Process.Start(executablePath); + } + else + { + // Try to find by window title + (nint hWnd1, int pid) = FindWindowByTitle(processName); + if (hWnd1 != nint.Zero) + { + SetForegroundWindow(hWnd1); + Interaction.AppActivate(pid); + } + } + } + + /// + public void TileWindows(string processName1, string processName2) + { + // TODO: Update this to account for UWP apps (e.g. calculator). UWPs are hosted by ApplicationFrameHost.exe + IntPtr hWnd1 = IntPtr.Zero; + IntPtr hWnd2 = IntPtr.Zero; + int pid1 = -1; + int pid2 = -1; + + Process[] processes1 = Process.GetProcessesByName(processName1); + 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); + } + + 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) + { + _logger.Debug("Taskbar not found"); + return; + } + else + { + RECT taskbarRect = new RECT(); + GetWindowRect(hWnd, ref taskbarRect); + _logger.Debug("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); + } + } + + /// + public IntPtr FindProcessWindowHandle(string processName) + { + Process[] processes = Process.GetProcessesByName(processName); + foreach (Process p in processes) + { + if (p.MainWindowHandle != IntPtr.Zero) + { + return p.MainWindowHandle; + } + } + + return FindWindowByTitle(processName).hWnd; + } + + /// + /// Finds a top-level window by partial title match (case-insensitive). + /// + private 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); + } +} diff --git a/dotnet/autoShell/WindowsAppRegistry.cs b/dotnet/autoShell/WindowsAppRegistry.cs new file mode 100644 index 0000000000..04c55f3b15 --- /dev/null +++ b/dotnet/autoShell/WindowsAppRegistry.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using autoShell.Logging; +using Microsoft.WindowsAPICodePack.Shell; + +namespace autoShell; + +/// +/// 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 ILogger _logger; + private readonly Dictionary _friendlyNameToPath = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _friendlyNameToId = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _appMetadata; + + public WindowsAppRegistry(ILogger logger) + { + _logger = logger; + string userName = Environment.UserName; + + _appMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "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]); + } + + PopulateInstalledAppIds(); + } + + /// + public string GetExecutablePath(string friendlyName) + => _friendlyNameToPath.GetValueOrDefault(friendlyName); + + /// + public string GetAppUserModelId(string friendlyName) + => _friendlyNameToId.GetValueOrDefault(friendlyName); + + /// + public string ResolveProcessName(string friendlyName) + { + string path = GetExecutablePath(friendlyName); + return path != null ? Path.GetFileNameWithoutExtension(path) : friendlyName; + } + + /// + public string GetWorkingDirectoryEnvVar(string friendlyName) + { + return _appMetadata.TryGetValue(friendlyName, out string[] value) && value.Length > 1 + ? value[1] + : null; + } + + /// + public string GetArguments(string friendlyName) + { + return _appMetadata.TryGetValue(friendlyName, out string[] value) && value.Length > 2 + ? string.Join(" ", value.Skip(2)) + : null; + } + + /// + public IEnumerable GetAllAppNames() + => _friendlyNameToId.Keys; + + private void PopulateInstalledAppIds() + { + // GUID taken from https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid + var appsFolderId = new Guid("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}"); + + try + { + ShellObject appsFolder = (ShellObject)KnownFolderHelper.FromKnownFolderId(appsFolderId); + + foreach (var app in (IKnownFolder)appsFolder) + { + string appName = app.Name; + if (_friendlyNameToId.ContainsKey(appName)) + { + _logger.Debug("Key has multiple values: " + appName); + } + else + { + // The ParsingName property is the AppUserModelID + _friendlyNameToId.Add(appName, app.ParsingName); + } + } + } + catch (Exception ex) + { + _logger.Debug($"Failed to enumerate installed apps: {ex.Message}"); + } + } +} diff --git a/dotnet/autoShell/autoShell.csproj b/dotnet/autoShell/autoShell.csproj index 0a9e9d1cb9..b14bb30e85 100644 --- a/dotnet/autoShell/autoShell.csproj +++ b/dotnet/autoShell/autoShell.csproj @@ -8,7 +8,13 @@ true bin\$(Configuration) false + + $(NoWarn);SYSLIB1054;SYSLIB1096;CA1712;CA2101;CA1838 + + + + 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 diff --git a/dotnet/autoShell/packages.config b/dotnet/autoShell/packages.config deleted file mode 100644 index 968d3b49df..0000000000 --- a/dotnet/autoShell/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file 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 94cc64ce2c..e115db8959 100644 --- a/ts/packages/agents/desktop/src/connector.ts +++ b/ts/packages/agents/desktop/src/connector.ts @@ -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, @@ -555,7 +555,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, }); @@ -569,7 +569,7 @@ export async function runDesktopActions( } // Gaming Settings - case "enableGameMode": { + case "EnableGameMode": { actionData = JSON.stringify({}); confirmationMessage = `Opening Game Mode settings`; break; @@ -590,7 +590,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; @@ -692,7 +692,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/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/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/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/inputActionsSchema.ts b/ts/packages/agents/desktop/src/windows/inputActionsSchema.ts index 015f941628..0c1205c269 100644 --- a/ts/packages/agents/desktop/src/windows/inputActionsSchema.ts +++ b/ts/packages/agents/desktop/src/windows/inputActionsSchema.ts @@ -31,7 +31,7 @@ export type MouseWheelScrollLinesAction = { // Sets the primary mouse button export type SetPrimaryMouseButtonAction = { - actionName: "setPrimaryMouseButton"; + actionName: "SetPrimaryMouseButton"; parameters: { primaryButton: "left" | "right"; }; @@ -55,7 +55,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/inputSchema.agr b/ts/packages/agents/desktop/src/windows/inputSchema.agr index 91ded5ddc8..0b40c7254e 100644 --- a/ts/packages/agents/desktop/src/windows/inputSchema.agr +++ b/ts/packages/agents/desktop/src/windows/inputSchema.agr @@ -27,11 +27,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 } } @@ -50,9 +50,19 @@ 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 cursor trail -> { actionName: "CursorTrail", parameters: { enable: true } } + | disable cursor trail -> { actionName: "CursorTrail", parameters: { enable: false } } + | turn on cursor trail -> { actionName: "CursorTrail", parameters: { enable: true } } + | turn off cursor trail -> { actionName: "CursorTrail", parameters: { enable: false } } + | enable mouse trail -> { actionName: "CursorTrail", parameters: { enable: true } } + | disable mouse trail -> { actionName: "CursorTrail", parameters: { enable: false } } + | set cursor trail length to $(length:number) -> { actionName: "CursorTrail", parameters: { enable: true, length } } + | cursor trail $(length:number) -> { actionName: "CursorTrail", parameters: { enable: true, length } }; = enable cursor trail -> { actionName: "CursorTrail", parameters: { enable: true } } 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/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/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; }; 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 } }