This commit is contained in:
kaitao-ms
2025-11-28 23:55:08 +08:00
parent 45ee64b644
commit 8658ae1fb8
12 changed files with 213 additions and 128 deletions

View File

@@ -829,6 +829,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Workspaces.ModuleServices",
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.ModuleContracts", "src\common\PowerToys.ModuleContracts\PowerToys.ModuleContracts.csproj", "{094E65D5-585E-4898-B465-97A47CD44380}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.ModuleContracts", "src\common\PowerToys.ModuleContracts\PowerToys.ModuleContracts.csproj", "{094E65D5-585E-4898-B465-97A47CD44380}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Awake.ModuleServices", "src\modules\awake\Awake.ModuleServices\Awake.ModuleServices.csproj", "{2141FF78-5F51-ED6B-E11B-C7079CCA1456}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -5255,6 +5257,22 @@ Global
{094E65D5-585E-4898-B465-97A47CD44380}.Release|x64.Build.0 = Release|x64 {094E65D5-585E-4898-B465-97A47CD44380}.Release|x64.Build.0 = Release|x64
{094E65D5-585E-4898-B465-97A47CD44380}.Release|x86.ActiveCfg = Release|x64 {094E65D5-585E-4898-B465-97A47CD44380}.Release|x86.ActiveCfg = Release|x64
{094E65D5-585E-4898-B465-97A47CD44380}.Release|x86.Build.0 = Release|x64 {094E65D5-585E-4898-B465-97A47CD44380}.Release|x86.Build.0 = Release|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Debug|Any CPU.ActiveCfg = Debug|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Debug|Any CPU.Build.0 = Debug|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Debug|ARM64.ActiveCfg = Debug|ARM64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Debug|ARM64.Build.0 = Debug|ARM64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Debug|x64.ActiveCfg = Debug|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Debug|x64.Build.0 = Debug|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Debug|x86.ActiveCfg = Debug|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Debug|x86.Build.0 = Debug|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Release|Any CPU.ActiveCfg = Release|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Release|Any CPU.Build.0 = Release|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Release|ARM64.ActiveCfg = Release|ARM64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Release|ARM64.Build.0 = Release|ARM64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Release|x64.ActiveCfg = Release|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Release|x64.Build.0 = Release|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Release|x86.ActiveCfg = Release|x64
{2141FF78-5F51-ED6B-E11B-C7079CCA1456}.Release|x86.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -5588,6 +5606,7 @@ Global
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{D52AAF95-DE88-49EA-B28A-10E382BCD4AB} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {D52AAF95-DE88-49EA-B28A-10E382BCD4AB} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{094E65D5-585E-4898-B465-97A47CD44380} = {1AFB6476-670D-4E80-A464-657E01DFF482} {094E65D5-585E-4898-B465-97A47CD44380} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{2141FF78-5F51-ED6B-E11B-C7079CCA1456} = {127F38E0-40AA-4594-B955-5616BF206882}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\PowerToys.ModuleContracts\PowerToys.ModuleContracts.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using Common.UI;
using ManagedCommon;
using PowerToys.ModuleContracts;
namespace Awake.ModuleServices;
/// <summary>
/// Provides CLI-based Awake control for reuse across hosts.
/// </summary>
public sealed class AwakeService : ModuleServiceBase, IAwakeService
{
public static AwakeService Instance { get; } = new();
public override string Key => SettingsDeepLink.SettingsWindow.Awake.ToString();
protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.Awake;
public override Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default)
{
// Default launch -> indefinite, honoring Awake's own settings for display behavior.
return SetIndefiniteAsync(cancellationToken);
}
public Task<OperationResult> SetIndefiniteAsync(CancellationToken cancellationToken = default)
{
return InvokeCliAsync("-m indefinite");
}
public Task<OperationResult> SetTimedAsync(int minutes, CancellationToken cancellationToken = default)
{
if (minutes <= 0)
{
return Task.FromResult(OperationResult.Fail("Minutes must be greater than zero."));
}
return InvokeCliAsync($"-m timed -t {minutes}");
}
public Task<OperationResult> SetOffAsync(CancellationToken cancellationToken = default)
{
return InvokeCliAsync("-m passive");
}
private static Task<OperationResult> InvokeCliAsync(string arguments)
{
try
{
var basePath = PowerToysPathResolver.GetPowerToysInstallPath();
if (string.IsNullOrWhiteSpace(basePath))
{
return Task.FromResult(OperationResult.Fail("PowerToys install path not found."));
}
var exePath = Path.Combine(basePath, "PowerToys.Awake.exe");
if (!File.Exists(exePath))
{
return Task.FromResult(OperationResult.Fail("Unable to locate PowerToys.Awake.exe."));
}
var startInfo = new ProcessStartInfo(exePath, arguments)
{
UseShellExecute = false,
CreateNoWindow = true,
};
Process.Start(startInfo);
return Task.FromResult(OperationResult.Ok());
}
catch (Exception ex)
{
return Task.FromResult(OperationResult.Fail($"Failed to invoke Awake: {ex.Message}"));
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using PowerToys.ModuleContracts;
namespace Awake.ModuleServices;
public interface IAwakeService : IModuleService
{
Task<OperationResult> SetIndefiniteAsync(CancellationToken cancellationToken = default);
Task<OperationResult> SetTimedAsync(int minutes, CancellationToken cancellationToken = default);
Task<OperationResult> SetOffAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions.Toolkit;
using Awake.ModuleServices;
namespace PowerToysExtension.Commands;
internal sealed partial class StartAwakeCommand : InvokableCommand
{
private readonly Func<Task<OperationResult>> _action;
private readonly string? _successToast;
internal StartAwakeCommand(string title, Func<Task<OperationResult>> action, string? successToast = null)
{
ArgumentNullException.ThrowIfNull(action);
ArgumentException.ThrowIfNullOrWhiteSpace(title);
_action = action;
_successToast = successToast;
Name = title;
}
public override CommandResult Invoke()
{
try
{
var result = _action().GetAwaiter().GetResult();
if (!result.Success)
{
return CommandResult.ShowToast(result.Error ?? "Failed to start Awake.");
}
return string.IsNullOrWhiteSpace(_successToast) ? CommandResult.Hide() : CommandResult.ShowToast(_successToast);
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Launching Awake failed: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions.Toolkit;
using Awake.ModuleServices;
namespace PowerToysExtension.Commands;
internal sealed partial class StopAwakeCommand : InvokableCommand
{
internal StopAwakeCommand()
{
Name = "Set Awake to Off";
}
public override CommandResult Invoke()
{
try
{
var result = AwakeService.Instance.SetOffAsync().GetAwaiter().GetResult();
if (result.Success)
{
return CommandResult.ShowToast("Awake switched to Off.");
}
return CommandResult.ShowToast(result.Error ?? "Awake does not appear to be running.");
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Failed to switch Awake off: {ex.Message}");
}
}
}

View File

@@ -1,59 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
namespace PowerToysExtension.Commands;
internal sealed partial class StartAwakeCommand : InvokableCommand
{
private readonly Func<string> _argumentsProvider;
private readonly string? _successToast;
internal StartAwakeCommand(string title, Func<string> argumentsProvider, string? successToast = null)
{
ArgumentNullException.ThrowIfNull(argumentsProvider);
ArgumentException.ThrowIfNullOrWhiteSpace(title);
_argumentsProvider = argumentsProvider;
_successToast = successToast;
Name = title;
}
public override CommandResult Invoke()
{
try
{
var executablePath = PowerToysPathResolver.TryResolveExecutable("PowerToys.Awake.exe");
if (string.IsNullOrEmpty(executablePath))
{
return CommandResult.ShowToast("Unable to locate PowerToys.Awake.exe.");
}
var arguments = _argumentsProvider()?.Trim() ?? string.Empty;
var startInfo = new ProcessStartInfo(executablePath)
{
UseShellExecute = string.IsNullOrWhiteSpace(arguments),
CreateNoWindow = true,
};
if (!string.IsNullOrWhiteSpace(arguments))
{
startInfo.Arguments = arguments;
startInfo.UseShellExecute = false;
}
Process.Start(startInfo);
return string.IsNullOrWhiteSpace(_successToast) ? CommandResult.Hide() : CommandResult.ShowToast(_successToast);
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Launching Awake failed: {ex.Message}");
}
}
}

View File

@@ -1,66 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace PowerToysExtension.Commands;
internal sealed partial class StopAwakeCommand : InvokableCommand
{
private const string AwakeWindowClass = "Awake.MessageWindow";
private const uint WmCommand = 0x0111;
private const int PassiveCommand = 0x0400 + 0x3;
internal StopAwakeCommand()
{
Name = "Set Awake to Off";
}
public override CommandResult Invoke()
{
try
{
if (TrySendPassiveCommand())
{
return CommandResult.ShowToast("Awake switched to Off.");
}
return CommandResult.ShowToast("Awake does not appear to be running.");
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Failed to switch Awake off: {ex.Message}");
}
}
private static bool TrySendPassiveCommand()
{
var handle = IntPtr.Zero;
var sent = false;
while (true)
{
handle = FindWindowEx(IntPtr.Zero, handle, AwakeWindowClass, null);
if (handle == IntPtr.Zero)
{
break;
}
if (PostMessage(handle, WmCommand, new IntPtr(PassiveCommand), IntPtr.Zero))
{
sent = true;
}
}
return sent;
}
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string? className, string? windowTitle);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
}

View File

@@ -7,6 +7,7 @@ using Common.UI;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands; using PowerToysExtension.Commands;
using PowerToysExtension.Helpers; using PowerToysExtension.Helpers;
using Awake.ModuleServices;
namespace PowerToysExtension.Modules; namespace PowerToysExtension.Modules;
@@ -19,9 +20,9 @@ internal sealed class AwakeModuleCommandProvider : ModuleCommandProvider
var more = new List<CommandContextItem> var more = new List<CommandContextItem>
{ {
new CommandContextItem(new StartAwakeCommand("Awake: Keep awake indefinitely", () => "-m indefinite", "Awake set to indefinite")), new CommandContextItem(new StartAwakeCommand("Awake: Keep awake indefinitely", () => AwakeService.Instance.SetIndefiniteAsync(), "Awake set to indefinite")),
new CommandContextItem(new StartAwakeCommand("Awake: Keep awake for 30 minutes", () => "-m timed -t 30", "Awake set for 30 minutes")), new CommandContextItem(new StartAwakeCommand("Awake: Keep awake for 30 minutes", () => AwakeService.Instance.SetTimedAsync(30), "Awake set for 30 minutes")),
new CommandContextItem(new StartAwakeCommand("Awake: Keep awake for 2 hours", () => "-m timed -t 120", "Awake set for 2 hours")), new CommandContextItem(new StartAwakeCommand("Awake: Keep awake for 2 hours", () => AwakeService.Instance.SetTimedAsync(120), "Awake set for 2 hours")),
new CommandContextItem(new StopAwakeCommand()), new CommandContextItem(new StopAwakeCommand()),
}; };