Compare commits

..

20 Commits

Author SHA1 Message Date
Leilei Zhang
3c55166414 add process 2025-09-22 11:24:17 +08:00
Leilei Zhang
ec136d36ff update description 2025-09-18 16:29:47 +08:00
Leilei Zhang
68e98d62aa update description 2025-09-18 15:40:22 +08:00
Leilei Zhang
6e8142929e add status 2025-09-18 15:21:47 +08:00
Shawn Yuan
55593e5b99 Merge branch 'shawn/comServer' of https://github.com/microsoft/PowerToys into shawn/comServer 2025-09-18 14:20:12 +08:00
Shawn Yuan
8d8d451811 update
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-18 14:19:41 +08:00
leileizhang
d06555db93 Leilzh/rot (#41865)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-18 11:39:54 +08:00
Shawn Yuan
baf42abba4 add power management
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-17 17:09:01 +08:00
Shawn Yuan
c818859103 update
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-17 14:56:40 +08:00
Shawn Yuan
34a06715a8 Merge branch 'shawn/mcpserver' into shawn/comServer 2025-09-17 14:32:55 +08:00
Shawn Yuan
01e7b61efb transit to sqlite
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-17 13:56:44 +08:00
Shawn Yuan
be334fa0df added usage status record in awake
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-17 10:58:30 +08:00
Leilei Zhang
e4a48b85a1 add cpu 2025-09-16 21:29:50 +08:00
Shawn Yuan
1aeed1699e init
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-16 16:36:22 +08:00
Shawn Yuan
804de07b20 init
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-15 22:28:24 +08:00
chakrik73
3f9ff66a0e zoomit bug fixes (#41773)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #41040, #41041, #41043
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Manually tested and ensured that the issues are
resolved.
- [ ] **Localization:** N/A
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** No new binaries added
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
This PR includes code-changes to restore some of the features of ZoomIt
that existed in older versions and lost in later versions, such as when
in Draw mode, after drawing, if an area is snipped, it doesn't clear the
drawing immediately, giving the user an option to cancel snip and update
the drawing. Also, in draw mode, when left mouse is clicked, it results
in a dot, as it was in previous versions. This PR also addresses some
race conditions during Recording that results in error when the
Recording is saved.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually tested and validated the draw and snip modes.
2025-09-14 22:59:48 -07:00
Guilherme
05d621a121 CmdPal: Fix EmptyContent not rendering (#41788)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
While testing the recent GridPages changes #40832 , I noticed that the
`EmptyContent` case was removed from the `SwitchPresenter`, which caused
EmptyContent to stop rendering.

This PR restores the missing case so that `EmptyContent` is correctly
displayed again.

### Before
<img width="790" height="480" alt="BeforeFix"
src="https://github.com/user-attachments/assets/71e43da9-41c4-4c1a-bbb1-bc3f6b4da1d0"
/>

### After
<img width="793" height="483" alt="AfterFix"
src="https://github.com/user-attachments/assets/ebec6b44-ebfd-4497-9e69-99b00ce44117"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #41786 
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-12 18:02:25 -05:00
Davide Giacometti
64cbf222e1 [Settings] Search Results Page Improvements (#41719)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

- Fix tab navigation issues #41717
- Minor improvement:
  - Show module group only if there are modules to show #41526
  - Add line break in the "no results" message

0.94
<img width="769" height="422" alt="image"
src="https://github.com/user-attachments/assets/111fc200-5811-43aa-9ea0-3f8d80543560"
/>

PR
<img width="812" height="524" alt="image"
src="https://github.com/user-attachments/assets/65070862-ff3f-4294-8aad-2ade4e6d4e90"
/>

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41717 #41526
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

- Verified that tab navigation is smooth between search groups
- Verified that narrator announces group names
- Tested empty search results page
2025-09-12 13:34:10 +08:00
Kai Tao
dd25769a96 Dev doc: Work in vscode (#41704)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Doc and debugging setting in vscode.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
This pull request adds support for developing and debugging PowerToys
using Visual Studio Code by introducing a new launch configuration and
comprehensive developer documentation. These changes make it easier for
contributors to build, debug, and iterate on both native and managed
components of PowerToys within VS Code.

**VS Code integration and developer workflow:**

* Added `.vscode/launch.json` with configurations for launching and
attaching to native (`PowerToys.exe`) and managed
(`PowerToys.Settings.exe`) processes, supporting both C++ and .NET
debugging scenarios.
* Introduced `doc/devdocs/development/dev-with-vscode.md`, a detailed
guide covering VS Code setup, building, debugging, and common developer
workflows for the PowerToys project. This includes extension
recommendations, shell integration, sample build commands, and
troubleshooting tips.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Can debug locally in vscode

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-11 16:12:53 +08:00
leileizhang
0c2f6bf376 [UI Tests] Stabilize Mouse Utils UI tests by switching to AccessibilityId (#41755)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
The Mouse Utils UI tests were failing because the Name values changed.

This PR updates the tests to use AccessibilityId instead, which provides
more stable element identification.

try to replace all findbyname

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-11 15:16:12 +08:00
106 changed files with 4557 additions and 1030 deletions

12
.vscode/mcp.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"servers": {
"powertoys-mcp": {
"command": "D:/work/PowerToys/x64/Debug/PowerToys.MCPServer.exe",
"transport": "stdio",
"args": [],
"env": {
"NODE_ENV": "production"
}
}
}
}

View File

@@ -21,7 +21,8 @@
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250910-build.2249" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250703-build.2173" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
@@ -60,6 +61,7 @@
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.4" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
<PackageVersion Include="Moq" Version="4.18.4" />

View File

@@ -1509,6 +1509,7 @@ SOFTWARE.
- CommunityToolkit.WinUI.Converters
- CommunityToolkit.WinUI.Extensions
- CommunityToolkit.WinUI.UI.Controls.DataGrid
- CommunityToolkit.WinUI.UI.Controls.Markdown
- ControlzEx
- HelixToolkit
- HelixToolkit.Core.Wpf

View File

@@ -805,6 +805,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MCPServer", "MCPServer", "{B637E6DD-FB81-4595-BB9C-01168556EA9E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPServer", "src\modules\MCPServer\MCPServer\MCPServer.csproj", "{20CBF173-9E8D-3236-6664-5B9C303794A3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2923,6 +2927,14 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|ARM64.ActiveCfg = Debug|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|ARM64.Build.0 = Debug|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|x64.ActiveCfg = Debug|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|x64.Build.0 = Debug|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|ARM64.ActiveCfg = Release|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|ARM64.Build.0 = Release|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|x64.ActiveCfg = Release|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2932,6 +2944,7 @@ Global
{D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {264B412F-DB8B-4CF8-A74B-96998B183045}
{1AFB6476-670D-4E80-A464-657E01DFF482} = {557C4636-D7E1-4838-A504-7D19B725EE95}
{1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
@@ -3242,6 +3255,8 @@ Global
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{B637E6DD-FB81-4595-BB9C-01168556EA9E} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{20CBF173-9E8D-3236-6664-5B9C303794A3} = {B637E6DD-FB81-4595-BB9C-01168556EA9E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -0,0 +1,24 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
namespace ManagedCommon
{
public sealed class ErrorResponse
{
public string Error { get; set; } = string.Empty;
public IEnumerable<string>? RegisteredModules { get; set; }
public string[]? AvailableEndpoints { get; set; }
public int? StatusCode { get; set; }
public DateTimeOffset Timestamp { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
namespace ManagedCommon
{
public sealed class GlobalStatusResponse
{
public string Application { get; set; } = string.Empty;
public string? Version { get; set; }
public string Status { get; set; } = string.Empty;
public int RegisteredModules { get; set; }
public Dictionary<string, ModuleStatusResponse> Modules { get; set; } = [];
public DateTimeOffset Timestamp { get; set; }
}
}

View File

@@ -0,0 +1,358 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace ManagedCommon
{
public sealed class HttpServer : IDisposable
{
private readonly HttpListener _listener;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly Dictionary<string, IHttpRequestHandler> _requestHandlers;
private readonly JsonSerializerOptions _fallbackJsonOptions;
private Task? _listenerTask;
private bool _disposed;
public HttpServer(string prefix = "http://localhost:8080/")
{
_listener = new HttpListener();
_listener.Prefixes.Add(prefix);
_cancellationTokenSource = new CancellationTokenSource();
_requestHandlers = new Dictionary<string, IHttpRequestHandler>(StringComparer.OrdinalIgnoreCase);
// Cached fallback options for generic deserialization
_fallbackJsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
Logger.LogInfo($"HTTP server configured to listen on: {prefix}");
}
/// <summary>
/// Register a request handler for a specific module.
/// </summary>
/// <param name="handler">The request handler to register.</param>
public void RegisterHandler(IHttpRequestHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
if (string.IsNullOrWhiteSpace(handler.ModuleName))
{
throw new ArgumentException("Module name cannot be null or empty", nameof(handler));
}
_requestHandlers[handler.ModuleName] = handler;
Logger.LogInfo($"Registered HTTP handler for module: {handler.ModuleName}");
}
/// <summary>
/// Unregister a request handler for a specific module.
/// </summary>
/// <param name="moduleName">The name of the module to unregister.</param>
public void UnregisterHandler(string moduleName)
{
if (_requestHandlers.Remove(moduleName))
{
Logger.LogInfo($"Unregistered HTTP handler for module: {moduleName}");
}
}
public void Start()
{
try
{
_listener.Start();
Logger.LogInfo("HTTP server started successfully");
_listenerTask = Task.Run(async () => await ListenAsync(_cancellationTokenSource.Token));
}
catch (Exception ex)
{
Logger.LogError($"Failed to start HTTP server: {ex.Message}");
throw;
}
}
public void Stop()
{
try
{
_cancellationTokenSource.Cancel();
_listener.Stop();
_listenerTask?.Wait(TimeSpan.FromSeconds(5));
Logger.LogInfo("HTTP server stopped");
}
catch (Exception ex)
{
Logger.LogError($"Error stopping HTTP server: {ex.Message}");
}
}
/// <summary>
/// Utility method for modules to write JSON responses.
/// </summary>
/// <param name="response">The HTTP response to write to.</param>
/// <param name="data">The object to serialize as JSON.</param>
public async Task WriteJsonResponseAsync(HttpListenerResponse response, object data)
{
response.ContentType = "application/json";
response.ContentEncoding = Encoding.UTF8;
string json = data switch
{
ServerStatusResponse serverStatus => JsonSerializer.Serialize(serverStatus, HttpServerJsonContext.Default.ServerStatusResponse),
GlobalStatusResponse globalStatus => JsonSerializer.Serialize(globalStatus, HttpServerJsonContext.Default.GlobalStatusResponse),
ErrorResponse error => JsonSerializer.Serialize(error, HttpServerJsonContext.Default.ErrorResponse),
_ => JsonSerializer.Serialize(data, HttpServerJsonContext.Default.Object),
};
var buffer = Encoding.UTF8.GetBytes(json);
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer.AsMemory(), _cancellationTokenSource.Token);
response.Close();
}
/// <summary>
/// Utility method for modules to read JSON request bodies as string.
/// Modules should handle their own deserialization to maintain AOT compatibility.
/// </summary>
/// <param name="request">The HTTP request to read from.</param>
/// <returns>The JSON string, or null if no body.</returns>
public async Task<string?> ReadJsonRequestBodyAsync(HttpListenerRequest request)
{
if (!request.HasEntityBody)
{
return null;
}
using var reader = new StreamReader(request.InputStream, Encoding.UTF8);
var body = await reader.ReadToEndAsync();
return string.IsNullOrWhiteSpace(body) ? null : body;
}
/// <summary>
/// Legacy utility method for modules to read JSON request bodies.
/// Warning: This method uses reflection-based deserialization and is not AOT-compatible.
/// Consider using ReadJsonRequestBodyAsync and handling deserialization in your module.
/// </summary>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <param name="request">The HTTP request to read from.</param>
/// <returns>The deserialized object, or null if no body or invalid JSON.</returns>
[Obsolete("Use ReadJsonRequestBodyAsync and handle deserialization in your module for AOT compatibility")]
public async Task<T?> ReadJsonRequestAsync<T>(HttpListenerRequest request)
where T : class
{
var body = await ReadJsonRequestBodyAsync(request);
if (body == null)
{
return null;
}
try
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling
return JsonSerializer.Deserialize<T>(body, _fallbackJsonOptions);
#pragma warning restore IL3050
#pragma warning restore IL2026
}
catch (JsonException ex)
{
Logger.LogError($"Error deserializing request body: {ex.Message}");
return null;
}
}
public void Dispose()
{
if (!_disposed)
{
Stop();
_cancellationTokenSource?.Dispose();
_listener?.Close();
_disposed = true;
}
}
private async Task ListenAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested && _listener.IsListening)
{
try
{
var contextTask = _listener.GetContextAsync();
var context = await contextTask.ConfigureAwait(false);
// Handle request asynchronously without blocking the listener
_ = Task.Run(async () => await HandleRequestAsync(context), cancellationToken);
}
catch (ObjectDisposedException)
{
// Expected when listener is stopped
break;
}
catch (HttpListenerException ex) when (ex.ErrorCode == 995)
{
// Expected when listener is stopped
break;
}
catch (Exception ex)
{
Logger.LogError($"Error in HTTP listener: {ex.Message}");
}
}
}
private async Task HandleRequestAsync(HttpListenerContext context)
{
try
{
var request = context.Request;
var response = context.Response;
Logger.LogInfo($"HTTP Request: {request.HttpMethod} {request.Url?.AbsolutePath}");
// Set CORS headers
response.Headers.Add("Access-Control-Allow-Origin", "*");
response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
if (request.HttpMethod == "OPTIONS")
{
response.StatusCode = 200;
response.Close();
return;
}
var path = request.Url?.AbsolutePath?.TrimStart('/');
if (string.IsNullOrEmpty(path))
{
await HandleRootRequestAsync(response);
return;
}
// Parse the path to extract module name and sub-path
var segments = path.Split('/', 2);
var moduleName = segments[0];
var subPath = segments.Length > 1 ? segments[1] : string.Empty;
// Check for global endpoints
if (string.Equals(moduleName, "status", StringComparison.OrdinalIgnoreCase))
{
await HandleGlobalStatusAsync(response);
return;
}
// Route to module-specific handler
if (_requestHandlers.TryGetValue(moduleName, out var handler))
{
try
{
await handler.HandleRequestAsync(context, subPath);
}
catch (Exception ex)
{
Logger.LogError($"Error in module handler for {moduleName}: {ex.Message}");
await HandleErrorAsync(response, 500, $"Internal server error in {moduleName} module: {ex.Message}");
}
}
else
{
await HandleNotFoundAsync(response, moduleName);
}
}
catch (Exception ex)
{
Logger.LogError($"Unhandled error in HTTP request handler: {ex.Message}");
}
}
private async Task HandleRootRequestAsync(HttpListenerResponse response)
{
var rootInfo = new ServerStatusResponse
{
Application = "PowerToys HTTP Server",
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
Status = "Running",
RegisteredModules = _requestHandlers.Keys,
AvailableEndpoints = [
"GET /status - Get server status",
"GET /{module}/... - Module-specific endpoints",
],
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, rootInfo);
}
private async Task HandleGlobalStatusAsync(HttpListenerResponse response)
{
var moduleStatuses = new Dictionary<string, ModuleStatusResponse>();
foreach (var kvp in _requestHandlers)
{
moduleStatuses[kvp.Key] = new ModuleStatusResponse
{
ModuleName = kvp.Value.ModuleName,
AvailableEndpoints = kvp.Value.GetAvailableEndpoints(),
};
}
var status = new GlobalStatusResponse
{
Application = "PowerToys HTTP Server",
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
Status = "Running",
RegisteredModules = _requestHandlers.Count,
Modules = moduleStatuses,
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, status);
}
private async Task HandleNotFoundAsync(HttpListenerResponse response, string requestedModule)
{
var errorResponse = new ErrorResponse
{
Error = $"Module '{requestedModule}' not found",
RegisteredModules = _requestHandlers.Keys,
AvailableEndpoints = [
"GET /status - Get server status and available modules",
],
Timestamp = DateTimeOffset.Now,
};
response.StatusCode = 404;
await WriteJsonResponseAsync(response, errorResponse);
}
private async Task HandleErrorAsync(HttpListenerResponse response, int statusCode, string message)
{
response.StatusCode = statusCode;
var errorResponse = new ErrorResponse
{
Error = message,
StatusCode = statusCode,
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, errorResponse);
}
}
}

View File

@@ -0,0 +1,23 @@
// 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.
#nullable enable
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace ManagedCommon
{
[JsonSerializable(typeof(ServerStatusResponse))]
[JsonSerializable(typeof(ModuleStatusResponse))]
[JsonSerializable(typeof(GlobalStatusResponse))]
[JsonSerializable(typeof(ErrorResponse))]
[JsonSerializable(typeof(Dictionary<string, ModuleStatusResponse>))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(IEnumerable<string>))]
[JsonSerializable(typeof(object))]
internal sealed partial class HttpServerJsonContext : JsonSerializerContext
{
}
}

View File

@@ -0,0 +1,36 @@
// 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.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace ManagedCommon
{
public interface IHttpRequestHandler
{
/// <summary>
/// Handle an HTTP request for this module.
/// </summary>
/// <param name="context">The HTTP context containing request and response.</param>
/// <param name="path">The requested path (after module prefix).</param>
/// <returns>Task representing the async operation.</returns>
Task HandleRequestAsync(HttpListenerContext context, string path);
/// <summary>
/// Get the module name used for URL routing (e.g., "awake", "fancyzones").
/// </summary>
string ModuleName { get; }
/// <summary>
/// Get the available endpoints for this module (for documentation).
/// </summary>
string[] GetAvailableEndpoints();
}
}

View File

@@ -0,0 +1,15 @@
// 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.
#nullable enable
namespace ManagedCommon
{
public sealed class ModuleStatusResponse
{
public string ModuleName { get; set; } = string.Empty;
public string[] AvailableEndpoints { get; set; } = [];
}
}

View File

@@ -0,0 +1,225 @@
// 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 System.Threading;
#nullable enable
#pragma warning disable IL2050 // Suppress COM interop trimming warnings for ROT hosting P/Invokes (desktop only scenario)
namespace ManagedCommon
{
/// <summary>
/// Generic helper to host a single COM-visible automation object in the Running Object Table (ROT)
/// without registry/CLSID class factory registration. Used for lightweight cross-process automation.
/// Pattern: create instance -> register with moniker -> wait until Stop.
/// Threading: spins up a dedicated STA thread so objects needing STA semantics are safe.
/// </summary>
public sealed class RotSingletonHost : IDisposable
{
private readonly Lock _sync = new();
private readonly Func<object> _factory;
private readonly string _monikerName;
private readonly string _threadName;
private readonly ManualResetEvent _shutdown = new(false);
private Thread? _thread;
private int _rotCookie;
private object? _instance; // keep alive
private IMoniker? _moniker;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="RotSingletonHost"/> class.
/// </summary>
/// <param name="monikerName">Moniker name (logical unique id), e.g. "Awake.Automation".</param>
/// <param name="factory">Factory that creates the object to expose. Should return a COM-visible object.</param>
/// <param name="threadName">Optional thread name for diagnostics.</param>
public RotSingletonHost(string monikerName, Func<object> factory, string? threadName = null)
{
_monikerName = string.IsNullOrWhiteSpace(monikerName) ? throw new ArgumentException("Moniker required", nameof(monikerName)) : monikerName;
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_threadName = threadName ?? $"RotHost:{_monikerName}";
}
public bool IsRunning => _thread != null;
public string MonikerName => _monikerName;
public void Start()
{
lock (_sync)
{
if (_disposed)
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
if (_thread != null)
{
return; // already running
}
_thread = new Thread(ThreadMain)
{
IsBackground = true,
Name = _threadName,
};
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
Logger.LogInfo($"ROT host starting for moniker '{_monikerName}'");
}
}
public void Stop()
{
lock (_sync)
{
if (_thread == null)
{
return;
}
_shutdown.Set();
}
_thread?.Join(3000);
_thread = null;
_shutdown.Reset();
}
private void ThreadMain()
{
int hr = Ole32.CoInitializeEx(IntPtr.Zero, Ole32.CoinitApartmentThreaded);
if (hr < 0)
{
Logger.LogError($"CoInitializeEx failed: 0x{hr:X8}");
return;
}
try
{
hr = Ole32.GetRunningObjectTable(0, out var rot);
if (hr < 0 || rot == null)
{
Logger.LogError($"GetRunningObjectTable failed: 0x{hr:X8}");
return;
}
hr = Ole32.CreateItemMoniker("!", _monikerName, out _moniker);
if (hr < 0 || _moniker == null)
{
Logger.LogError($"CreateItemMoniker failed: 0x{hr:X8}");
return;
}
_instance = _factory();
var unk = Marshal.GetIUnknownForObject(_instance);
try
{
hr = rot.Register(0x1 /* ROTFLAGS_REGISTRATIONKEEPSALIVE */, _instance, _moniker, out _rotCookie);
if (hr < 0)
{
Logger.LogError($"IRunningObjectTable.Register failed: 0x{hr:X8}");
return;
}
}
finally
{
Marshal.Release(unk);
}
Logger.LogInfo($"ROT registered: '{_monikerName}'");
WaitHandle.WaitAny(new WaitHandle[] { _shutdown });
}
catch (Exception ex)
{
Logger.LogError($"ROT host exception: {ex}");
}
finally
{
try
{
if (_rotCookie != 0 && Ole32.GetRunningObjectTable(0, out var rot2) == 0 && rot2 != null)
{
rot2.Revoke(_rotCookie);
_rotCookie = 0;
}
}
catch (Exception ex)
{
Logger.LogWarning($"Exception revoking ROT registration: {ex.Message}");
}
Ole32.CoUninitialize();
Logger.LogInfo($"ROT host stopped: '{_monikerName}'");
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
Stop();
_disposed = true;
}
private static class Ole32
{
internal const int CoinitApartmentThreaded = 0x2;
#pragma warning disable IL2050 // Suppress trimming warnings for COM interop P/Invokes; ROT hosting not used in trimmed scenarios.
[DllImport("ole32.dll")]
internal static extern int CoInitializeEx(IntPtr pvReserved, int dwCoInit);
[DllImport("ole32.dll")]
internal static extern void CoUninitialize();
[DllImport("ole32.dll")]
internal static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable? prot);
[DllImport("ole32.dll")]
internal static extern int CreateItemMoniker([MarshalAs(UnmanagedType.LPWStr)] string lpszDelim, [MarshalAs(UnmanagedType.LPWStr)] string lpszItem, out IMoniker? ppmk);
#pragma warning restore IL2050
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000010-0000-0000-C000-000000000046")]
private interface IRunningObjectTable
{
int Register(int grfFlags, [MarshalAs(UnmanagedType.IUnknown)] object punkObject, IMoniker pmkObjectName, out int pdwRegister);
int Revoke(int dwRegister);
void IsRunning(IMoniker pmkObjectName);
int GetObject(IMoniker pmkObjectName, [MarshalAs(UnmanagedType.IUnknown)] out object? ppunkObject);
void NoteChangeTime(int dwRegister, ref FileTime pfiletime);
int GetTimeOfLastChange(IMoniker pmkObjectName, ref FileTime pfiletime);
int EnumRunning(out object ppenumMoniker);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("0000000f-0000-0000-C000-000000000046")]
private interface IMoniker
{
}
[StructLayout(LayoutKind.Sequential)]
private struct FileTime
{
public uint DwLowDateTime;
public uint DwHighDateTime;
}
}
}

View File

@@ -0,0 +1,26 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
namespace ManagedCommon
{
public sealed class ServerStatusResponse
{
public string Application { get; set; } = string.Empty;
public string? Version { get; set; }
public string Status { get; set; } = string.Empty;
public IEnumerable<string> RegisteredModules { get; set; } = [];
public string[] AvailableEndpoints { get; set; } = [];
public DateTimeOffset Timestamp { get; set; }
}
}

View File

@@ -0,0 +1,47 @@
<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.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>false</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>PowerToys.MCPServer</AssemblyName>
<AssemblyDescription>PowerToys MCP Server for Model Context Protocol</AssemblyDescription>
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<NoWin32Manifest>true</NoWin32Manifest>
<RootNamespace>PowerToys.MCPServer</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- CsWinRT configuration -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<ItemGroup>
<!-- Core dependencies -->
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="ModelContextProtocol " />
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>
<ItemGroup>
<!-- PowerToys project references -->
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

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 System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using PowerToys.MCPServer.Tools;
namespace MCPServer
{
internal sealed class Program
{
private static async Task<int> Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
// Configure all logs to go to stderr
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
return 0;
}
}
}

View File

@@ -0,0 +1,312 @@
// 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.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Data.Sqlite;
using Microsoft.PowerToys.Settings.UI.Library;
using ModelContextProtocol.Server;
namespace PowerToys.MCPServer.Tools
{
[McpServerToolType]
public static class AwakeTools
{
[McpServerTool]
[Description("Echoes the message back to the client.")]
public static string SetTimeTest(string message) => $"Hello {message}";
// =============================
// HTTP client (Awake remote control)
// =============================
private static readonly HttpClient _http = new HttpClient();
// Base URL for Awake HTTP server. Default matches Awake --http-port default (8080).
// Allow override through environment variable POWERTOYS_AWAKE_HTTP (e.g. http://localhost:9090/)
private static string BaseUrl => (Environment.GetEnvironmentVariable("POWERTOYS_AWAKE_HTTP") ?? "http://localhost:8080/").TrimEnd('/') + "/";
private static string JsonError(string msg, int? status = null) => JsonSerializer.Serialize(new { success = false, error = msg, status });
private static string JsonOk(object payload) => JsonSerializer.Serialize(payload);
private static string SendAwakeRequest(string method, string relativePath, object? body = null)
{
try
{
using var req = new HttpRequestMessage(new HttpMethod(method), BaseUrl + relativePath.TrimStart('/'));
if (body != null)
{
string json = JsonSerializer.Serialize(body);
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
using var resp = _http.Send(req);
string respText = resp.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (!resp.IsSuccessStatusCode)
{
return JsonError($"HTTP {(int)resp.StatusCode} {resp.StatusCode}", (int)resp.StatusCode) + "\n" + respText;
}
return respText;
}
catch (HttpRequestException ex)
{
return JsonError($"Connection failed: {ex.Message}. Ensure Awake is running with --http-server.");
}
catch (Exception ex)
{
return JsonError(ex.Message);
}
}
[McpServerTool]
[Description("RECOMMENDED FOR BUILDS: Intelligently keeps the system awake during build, compile, download, or process operations. Monitors CPU/memory/network activity and prevents sleep only when system is actively working. Automatically returns to sleep when build completes. PREFERRED over indefinite mode for development workflows. Params: cpuThresholdPercent (0-100), memThresholdPercent (0-100), netThresholdKBps (KB/s), sampleIntervalSeconds (>0), inactivityTimeoutSeconds (>0), keepDisplayOn=true|false")]
public static string AwakeHttpActivityBased(uint cpuThresholdPercent = 50, uint memThresholdPercent = 50, uint netThresholdKBps = 10, uint sampleIntervalSeconds = 30, uint inactivityTimeoutSeconds = 300, bool keepDisplayOn = true)
{
if (cpuThresholdPercent > 100)
{
return JsonError("cpuThresholdPercent must be 0-100");
}
if (memThresholdPercent > 100)
{
return JsonError("memThresholdPercent must be 0-100");
}
if (sampleIntervalSeconds == 0)
{
return JsonError("sampleIntervalSeconds must be > 0");
}
if (inactivityTimeoutSeconds == 0)
{
return JsonError("inactivityTimeoutSeconds must be > 0");
}
return SendAwakeRequest("POST", "awake/activity", new
{
cpuThresholdPercent,
memThresholdPercent,
netThresholdKBps,
sampleIntervalSeconds,
inactivityTimeoutSeconds,
keepDisplayOn,
});
}
[McpServerTool]
[Description("Get Awake HTTP status (GET /awake/status). Requires Awake launched with --http-server.")]
public static string AwakeHttpStatus() => SendAwakeRequest("GET", "awake/status");
[McpServerTool]
[Description("Keeps the system awake indefinitely until manually changed. WARNING: Less efficient than activity-based mode for builds. Use only when you need guaranteed continuous awake state regardless of system activity. Params: keepDisplayOn=true|false, processId=0")]
public static string AwakeHttpIndefinite(bool keepDisplayOn = true, int processId = 0)
=> SendAwakeRequest("POST", "awake/indefinite", new { keepDisplayOn, processId });
[McpServerTool]
[Description("Set timed keep-awake via HTTP. Params: seconds (>0), keepDisplayOn=true|false")]
public static string AwakeHttpTimed(uint seconds, bool keepDisplayOn = true)
{
if (seconds == 0)
{
return JsonError("seconds must be > 0");
}
return SendAwakeRequest("POST", "awake/timed", new { seconds, keepDisplayOn });
}
[McpServerTool]
[Description("Set expirable keep-awake via HTTP. Params: expireAt (ISO 8601), keepDisplayOn=true|false")]
public static string AwakeHttpExpirable(string expireAt, bool keepDisplayOn = true)
{
if (string.IsNullOrWhiteSpace(expireAt))
{
return JsonError("expireAt required (ISO 8601)");
}
return SendAwakeRequest("POST", "awake/expirable", new { expireAt, keepDisplayOn });
}
[McpServerTool]
[Description("Set passive mode via HTTP (POST /awake/passive).")]
public static string AwakeHttpPassive() => SendAwakeRequest("POST", "awake/passive");
[McpServerTool]
[Description("Toggle display keep-on via HTTP (POST /awake/display/toggle).")]
public static string AwakeHttpToggleDisplay() => SendAwakeRequest("POST", "awake/display/toggle");
[McpServerTool]
[Description("Get Awake settings via HTTP (GET /awake/settings).")]
public static string AwakeHttpSettings() => SendAwakeRequest("GET", "awake/settings");
[McpServerTool]
[Description("Check current PowerToys Awake mode and configuration. Returns active mode (indefinite, timed, activity-based, or passive), remaining time, thresholds, and display settings. Use to verify if system is being kept awake and what settings are active.")]
public static string AwakeHttpConfig() => SendAwakeRequest("GET", "awake/config");
private sealed class AppUsageRecord
{
[JsonPropertyName("process")]
public string ProcessName { get; set; } = string.Empty;
[JsonPropertyName("totalSeconds")]
public double TotalSeconds { get; set; }
[JsonPropertyName("lastUpdatedUtc")]
public DateTime LastUpdatedUtc { get; set; }
[JsonPropertyName("firstSeenUtc")]
public DateTime FirstSeenUtc { get; set; }
}
[McpServerTool]
[Description("Get top N foreground app usage entries recorded by Awake. Reads usage.sqlite if present (preferred) else legacy usage.json. Parameters: top (default 10), days (default 7). Returns JSON array.")]
public static string GetAwakeUsageSummary(int top = 10, int days = 7)
{
try
{
SettingsUtils utils = new();
string settingsPath = utils.GetSettingsFilePath("Awake");
string directory = Path.GetDirectoryName(settingsPath)!;
string sqlitePath = Path.Combine(directory, "usage.sqlite");
string legacyJson = Path.Combine(directory, "usage.json");
if (File.Exists(sqlitePath))
{
return QuerySqlite(sqlitePath, top, days);
}
if (File.Exists(legacyJson))
{
return QueryLegacyJson(legacyJson, top, days, note: "legacy-json");
}
return JsonSerializer.Serialize(new { error = "No usage data found", sqlite = sqlitePath, legacy = legacyJson });
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = ex.Message });
}
}
private static string QuerySqlite(string dbPath, int top, int days)
{
try
{
int safeDays = Math.Max(1, days);
using SqliteConnection conn = new(new SqliteConnectionStringBuilder { DataSource = dbPath, Mode = SqliteOpenMode.ReadOnly }.ToString());
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"SELECT process_name, SUM(total_seconds) AS total_seconds, MIN(first_seen_utc) AS first_seen_utc, MAX(last_updated_utc) AS last_updated_utc
FROM process_usage
WHERE day_utc >= date('now', @cutoff)
GROUP BY process_name
ORDER BY total_seconds DESC
LIMIT @top;";
cmd.Parameters.AddWithValue("@cutoff", $"-{safeDays} days");
cmd.Parameters.AddWithValue("@top", top);
var list = cmd.ExecuteReader()
.Cast<System.Data.Common.DbDataRecord>()
.Select(r => new AppUsageRecord
{
ProcessName = r.GetString(0),
TotalSeconds = r.GetDouble(1),
FirstSeenUtc = DateTime.Parse(r.GetString(2), null, System.Globalization.DateTimeStyles.RoundtripKind),
LastUpdatedUtc = DateTime.Parse(r.GetString(3), null, System.Globalization.DateTimeStyles.RoundtripKind),
})
.OrderByDescending(r => r.TotalSeconds)
.Select(r => new
{
process = r.ProcessName,
totalSeconds = Math.Round(r.TotalSeconds, 1),
totalHours = Math.Round(r.TotalSeconds / 3600.0, 2),
firstSeenUtc = r.FirstSeenUtc,
lastUpdatedUtc = r.LastUpdatedUtc,
source = "sqlite",
});
return JsonSerializer.Serialize(list);
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = "sqlite query failed", message = ex.Message, path = dbPath });
}
}
private static string QueryLegacyJson(string usageFile, int top, int days, string? note = null)
{
try
{
string json = File.ReadAllText(usageFile);
using JsonDocument doc = JsonDocument.Parse(json);
DateTime cutoff = DateTime.UtcNow.AddDays(-Math.Max(1, days));
var result = doc.RootElement
.EnumerateArray()
.Select(e => new
{
process = e.GetPropertyOrDefault("process", string.Empty),
totalSeconds = e.GetPropertyOrDefault("totalSeconds", 0.0),
lastUpdatedUtc = e.GetPropertyOrDefaultDateTime("lastUpdatedUtc"),
firstSeenUtc = e.GetPropertyOrDefaultDateTime("firstSeenUtc"),
})
.Where(r => r.lastUpdatedUtc >= cutoff)
.OrderByDescending(r => r.totalSeconds)
.Take(top)
.Select(r => new
{
r.process,
totalSeconds = Math.Round(r.totalSeconds, 1),
totalHours = Math.Round(r.totalSeconds / 3600.0, 2),
r.firstSeenUtc,
r.lastUpdatedUtc,
source = note ?? "json",
});
return JsonSerializer.Serialize(result);
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = "legacy json read failed", message = ex.Message, path = usageFile });
}
}
private static string GetPropertyOrDefault(this JsonElement element, string name, string defaultValue)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.String)
{
return value.GetString() ?? defaultValue;
}
return defaultValue;
}
private static double GetPropertyOrDefault(this JsonElement element, string name, double defaultValue)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out double d))
{
return d;
}
return defaultValue;
}
private static DateTime GetPropertyOrDefaultDateTime(this JsonElement element, string name)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value))
{
if (value.ValueKind == JsonValueKind.String && value.TryGetDateTime(out DateTime dt))
{
return dt;
}
}
return DateTime.MinValue;
}
}
}

View File

@@ -0,0 +1,17 @@
// 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.ComponentModel;
using ModelContextProtocol.Server;
namespace PowerToys.MCPServer.Tools
{
[McpServerToolType]
public static class EchoTool
{
[McpServerTool]
[Description("Echoes the message back to the client.")]
public static string Echo(string message) => $"Hello {message}";
}
}

View File

@@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"PowerToys.MCPServer": "Debug"
}
},
"MCPServer": {
"Port": 8080,
"MaxConcurrentConnections": 100,
"RequestTimeoutSeconds": 30,
"EnableTools": true,
"EnableResources": true,
"Transport": "http"
}
}

View File

@@ -0,0 +1,2 @@
EXPORTS
powertoy_create

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\Common.Cpp.props" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{A8B8D654-8F2A-4E6C-9B4F-1234567890AB}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>MCPServerModuleInterface</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<ModuleDefinitionFile>MCPServerModuleInterface.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>
<ItemGroup>
<None Include="MCPServerModuleInterface.def" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\SettingsAPI\SettingsAPI.vcxproj" />
<ProjectReference Include="..\..\..\..\common\logger\logger.vcxproj" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -0,0 +1,298 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/logger/logger.h>
#include <common/utils/resources.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/winapi_error.h>
#include <shellapi.h>
namespace NonLocalizable
{
const wchar_t ModulePath[] = L"PowerToys.MCPServer.exe";
const wchar_t ModuleKey[] = L"MCPServer";
}
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
class MCPServerModuleInterface : public PowertoyModuleIface
{
public:
virtual PCWSTR get_name() override
{
return app_name.c_str();
}
virtual const wchar_t* get_key() override
{
return app_key.c_str();
}
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_not_configured;
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(L"MCP Server provides Model Context Protocol access to PowerToys functionality for AI assistants and tools");
settings.set_icon_key(L"pt-mcp-server");
// Port configuration
settings.add_int_spinner(
L"port",
L"Server Port",
m_port,
1024,
65535,
1);
// Auto start option
settings.add_bool_toggle(
L"auto_start",
L"Auto Start Server",
m_auto_start);
// Enable tools API
settings.add_bool_toggle(
L"enable_tools",
L"Enable Tools API",
m_enable_tools);
// Enable resources API
settings.add_bool_toggle(
L"enable_resources",
L"Enable Resources API",
m_enable_resources);
// Transport protocol
settings.add_dropdown(
L"transport",
L"Transport Protocol",
m_transport,
std::vector<std::pair<std::wstring, std::wstring>>{
{ L"http", L"HTTP" },
{ L"stdio", L"Standard I/O" },
{ L"tcp", L"TCP Socket" }
});
return settings.serialize_to_buffer(buffer, buffer_size);
}
virtual void set_config(const wchar_t* config) override
{
try
{
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
if (auto port = values.get_int_value(L"port"))
{
m_port = port.value();
}
if (auto auto_start = values.get_bool_value(L"auto_start"))
{
m_auto_start = auto_start.value();
}
if (auto enable_tools = values.get_bool_value(L"enable_tools"))
{
m_enable_tools = enable_tools.value();
}
if (auto enable_resources = values.get_bool_value(L"enable_resources"))
{
m_enable_resources = enable_resources.value();
}
if (auto transport = values.get_string_value(L"transport"))
{
m_transport = transport.value();
}
values.save_to_settings_file();
// If service is running, restart to apply new configuration
if (m_enabled && is_process_running())
{
StopMCPServer();
StartMCPServer();
}
}
catch (std::exception& e)
{
Logger::error("MCPServer configuration parsing failed: {}", std::string{ e.what() });
}
}
virtual void enable() override
{
Logger::info("MCPServer enabling");
m_enabled = true;
if (m_auto_start)
{
StartMCPServer();
}
}
virtual void disable() override
{
Logger::info("MCPServer disabling");
m_enabled = false;
StopMCPServer();
}
virtual bool is_enabled() override
{
return m_enabled;
}
virtual void destroy() override
{
StopMCPServer();
delete this;
}
MCPServerModuleInterface()
{
app_name = L"MCP Server";
app_key = NonLocalizable::ModuleKey;
m_port = 8080;
m_auto_start = true;
m_enable_tools = true;
m_enable_resources = true;
m_transport = L"http";
init_settings();
}
private:
void StartMCPServer()
{
if (m_hProcess && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT)
{
return; // Already running
}
std::wstring executable_args = L"--port=" + std::to_wstring(m_port);
if (!m_enable_tools)
{
executable_args += L" --disable-tools";
}
if (!m_enable_resources)
{
executable_args += L" --disable-resources";
}
if (!m_transport.empty())
{
executable_args += L" --transport=" + m_transport;
}
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = NonLocalizable::ModulePath;
sei.nShow = SW_HIDE;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))
{
m_hProcess = sei.hProcess;
Logger::info("MCPServer started successfully on port {} with transport {}", m_port, std::string(m_transport.begin(), m_transport.end()));
}
else
{
Logger::error("Failed to start MCPServer");
auto message = get_last_error_message(GetLastError());
if (message.has_value())
{
Logger::error(message.value());
}
}
}
void StopMCPServer()
{
if (m_hProcess)
{
TerminateProcess(m_hProcess, 0);
CloseHandle(m_hProcess);
m_hProcess = nullptr;
Logger::info("MCPServer stopped");
}
}
bool is_process_running()
{
return m_hProcess && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
void init_settings()
{
try
{
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
if (auto port = settings.get_int_value(L"port"))
{
m_port = port.value();
}
if (auto auto_start = settings.get_bool_value(L"auto_start"))
{
m_auto_start = auto_start.value();
}
if (auto enable_tools = settings.get_bool_value(L"enable_tools"))
{
m_enable_tools = enable_tools.value();
}
if (auto enable_resources = settings.get_bool_value(L"enable_resources"))
{
m_enable_resources = enable_resources.value();
}
if (auto transport = settings.get_string_value(L"transport"))
{
m_transport = transport.value();
}
}
catch (std::exception&)
{
Logger::warn(L"MCPServer settings file not found, using defaults");
}
}
std::wstring app_name;
std::wstring app_key;
bool m_enabled = false;
HANDLE m_hProcess = nullptr;
int m_port = 8080;
bool m_auto_start = true;
bool m_enable_tools = true;
bool m_enable_resources = true;
std::wstring m_transport = L"http";
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new MCPServerModuleInterface();
}

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,14 @@
#pragma once
#include "targetver.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <unknwn.h>
#include <restrictederrorinfo.h>
#include <hstring.h>
#include <string>
#include <vector>

View File

@@ -0,0 +1,8 @@
#pragma once
// Including SDKDDKVer.h defines the highest available Windows platform.
// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
#include <SDKDDKVer.h>

View File

@@ -121,8 +121,8 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
LTEXT "ZoomIt v9.01",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,166,8
LTEXT "ZoomIt v9.0",IDC_VERSION,42,7,73,10
LTEXT "Copyright <EFBFBD> 2006-2024 Mark Russinovich",IDC_COPYRIGHT,42,17,166,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9
ICON "APPICON",IDC_STATIC,12,9,20,20

View File

@@ -1,102 +1,103 @@
<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.SelfContained.props" />
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<UseWindowsForms>False</UseWindowsForms>
<!--Per documentation: https://learn.microsoft.com/dotnet/core/compatibility/windows-forms/5.0/automatically-infer-winexe-output-type#outputtype-set-to-winexe-for-wpf-and-winforms-apps -->
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<AssemblyName>PowerToys.Awake</AssemblyName>
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<UseWindowsForms>False</UseWindowsForms>
<!--Per documentation: https://learn.microsoft.com/dotnet/core/compatibility/windows-forms/5.0/automatically-infer-winexe-output-type#outputtype-set-to-winexe-for-wpf-and-winforms-apps -->
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<AssemblyName>PowerToys.Awake</AssemblyName>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\Awake\Awake.ico" />
<None Remove="Assets\Awake\disabled.ico" />
<None Remove="Assets\Awake\expirable.ico" />
<None Remove="Assets\Awake\indefinite.ico" />
<None Remove="Assets\Awake\normal.ico" />
<None Remove="Assets\Awake\scheduled.ico" />
<None Remove="Assets\Awake\timed.ico" />
</ItemGroup>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Runtime.Caching" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Awake\Awake.ico" />
<None Remove="Assets\Awake\disabled.ico" />
<None Remove="Assets\Awake\expirable.ico" />
<None Remove="Assets\Awake\indefinite.ico" />
<None Remove="Assets\Awake\normal.ico" />
<None Remove="Assets\Awake\scheduled.ico" />
<None Remove="Assets\Awake\timed.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Runtime.Caching" />
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Compile>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Compile>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Content Include="Assets\Awake\Awake.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\disabled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\expirable.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\indefinite.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\normal.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\scheduled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\timed.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Assets\Awake\Awake.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\disabled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\expirable.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\indefinite.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\normal.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\scheduled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\timed.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
<ItemGroup>
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,33 @@
// 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 ManagedCommon;
namespace Awake
{
/// <summary>
/// Automation object exposed via the Running Object Table. Intentionally minimal; methods may expand in future.
/// </summary>
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("4F1C3769-8D28-4A2D-8A6A-AB2F4C0F5F11")]
public sealed class AwakeAutomation : IAwakeAutomation
{
public string Ping() => "pong";
public void SetIndefinite() => Logger.LogInfo("Automation: SetIndefinite");
public void SetTimed(int seconds) => Logger.LogInfo($"Automation: SetTimed {seconds}s");
public void SetExpirable(int minutes) => Logger.LogInfo($"Automation: SetExpirable {minutes}m");
public void SetPassive() => Logger.LogInfo("Automation: SetPassive");
public void Cancel() => Logger.LogInfo("Automation: Cancel");
public string GetStatusJson() => "{\"ok\":true}";
}
}

View File

@@ -0,0 +1,402 @@
// 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.Globalization;
using System.Net;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Awake.Core
{
/// <summary>
/// HTTP request handler for Awake module functionality.
/// </summary>
internal sealed class AwakeHttpHandler : IHttpRequestHandler
{
public string ModuleName => "awake";
public string[] GetAvailableEndpoints()
{
return new[]
{
"GET /awake/status - Get current Awake status",
"GET /awake/config - Get current Awake configuration",
"POST /awake/indefinite - Set indefinite keep-awake (body: {\"keepDisplayOn\": true, \"processId\": 0})",
"POST /awake/timed - Set timed keep-awake (body: {\"seconds\": 3600, \"keepDisplayOn\": true})",
"POST /awake/expirable - Set expirable keep-awake (body: {\"expireAt\": \"2024-12-31T23:59:59Z\", \"keepDisplayOn\": true})",
"POST /awake/activity - Set activity-based keep-awake (body: {\"cpuThresholdPercent\": 50, \"memThresholdPercent\": 50, \"netThresholdKBps\": 10, \"sampleIntervalSeconds\": 30, \"inactivityTimeoutSeconds\": 300, \"keepDisplayOn\": true})",
"POST /awake/passive - Set passive mode (no keep-awake)",
"POST /awake/display/toggle - Toggle display setting",
"GET /awake/settings - Get current PowerToys settings",
};
}
public async Task HandleRequestAsync(HttpListenerContext context, string path)
{
var request = context.Request;
var response = context.Response;
var method = request.HttpMethod.ToUpperInvariant();
var pathLower = path.ToLowerInvariant();
try
{
switch ((method, pathLower))
{
case ("GET", "status"):
await HandleGetStatusAsync(response);
break;
case ("GET", "config"):
await HandleGetConfigAsync(response);
break;
case ("POST", "indefinite"):
await HandleSetIndefiniteAwakeAsync(request, response);
break;
case ("POST", "timed"):
await HandleSetTimedAwakeAsync(request, response);
break;
case ("POST", "expirable"):
await HandleSetExpirableAwakeAsync(request, response);
break;
case ("POST", "activity"):
await HandleSetActivityBasedAwakeAsync(request, response);
break;
case ("POST", "passive"):
await HandleSetPassiveAwakeAsync(response);
break;
case ("POST", "display/toggle"):
await HandleToggleDisplayAsync(response);
break;
case ("GET", "settings"):
await HandleGetSettingsAsync(response);
break;
default:
await HandleNotFoundAsync(response, path);
break;
}
}
catch (Exception ex)
{
Logger.LogError($"Error handling Awake request {method} /{path}: {ex.Message}");
await HandleErrorAsync(response, 500, $"Internal server error: {ex.Message}");
}
}
private async Task HandleGetStatusAsync(HttpListenerResponse response)
{
var status = new
{
Module = "Awake",
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
Status = "Running",
UsingPowerToysConfig = Manager.IsUsingPowerToysConfig,
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, status);
}
private async Task HandleGetConfigAsync(HttpListenerResponse response)
{
var config = Manager.GetCurrentConfig();
await WriteJsonResponseAsync(response, config);
}
private async Task HandleSetIndefiniteAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
{
var requestData = await ReadJsonRequestAsync<IndefiniteAwakeRequest>(request);
Manager.SetIndefiniteKeepAwake(
requestData?.KeepDisplayOn ?? true,
requestData?.ProcessId ?? 0,
"HttpServer");
var result = new
{
Success = true,
Mode = "Indefinite",
KeepDisplayOn = requestData?.KeepDisplayOn ?? true,
ProcessId = requestData?.ProcessId ?? 0,
};
await WriteJsonResponseAsync(response, result);
}
private async Task HandleSetTimedAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
{
var requestData = await ReadJsonRequestAsync<TimedAwakeRequest>(request);
if (requestData?.Seconds == null || requestData.Seconds <= 0)
{
await HandleErrorAsync(response, 400, "Invalid or missing 'seconds' parameter. Must be a positive integer.");
return;
}
Manager.SetTimedKeepAwake(
requestData.Seconds,
requestData.KeepDisplayOn ?? true,
"HttpServer");
var result = new
{
Success = true,
Mode = "Timed",
Seconds = requestData.Seconds,
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
};
await WriteJsonResponseAsync(response, result);
}
private async Task HandleSetExpirableAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
{
var requestData = await ReadJsonRequestAsync<ExpirableAwakeRequest>(request);
if (requestData?.ExpireAt == null)
{
await HandleErrorAsync(response, 400, "Missing 'expireAt' parameter. Expected ISO 8601 format (e.g., '2024-12-31T23:59:59Z').");
return;
}
if (requestData.ExpireAt <= DateTimeOffset.Now)
{
await HandleErrorAsync(response, 400, "Expiration time must be in the future.");
return;
}
Manager.SetExpirableKeepAwake(
requestData.ExpireAt,
requestData.KeepDisplayOn ?? true,
"HttpServer");
var result = new
{
Success = true,
Mode = "Expirable",
ExpireAt = requestData.ExpireAt,
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
};
await WriteJsonResponseAsync(response, result);
}
private async Task HandleSetActivityBasedAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
{
var requestData = await ReadJsonRequestAsync<ActivityBasedAwakeRequest>(request);
if (requestData == null)
{
await HandleErrorAsync(response, 400, "Invalid or missing request body. Expected JSON with cpuThresholdPercent, memThresholdPercent, netThresholdKBps, sampleIntervalSeconds, inactivityTimeoutSeconds, and optional keepDisplayOn.");
return;
}
// Validate required parameters
if (requestData.CpuThresholdPercent > 100)
{
await HandleErrorAsync(response, 400, "Invalid cpuThresholdPercent. Must be between 0 and 100.");
return;
}
if (requestData.MemThresholdPercent > 100)
{
await HandleErrorAsync(response, 400, "Invalid memThresholdPercent. Must be between 0 and 100.");
return;
}
if (requestData.SampleIntervalSeconds == 0)
{
await HandleErrorAsync(response, 400, "Invalid sampleIntervalSeconds. Must be greater than 0.");
return;
}
if (requestData.InactivityTimeoutSeconds == 0)
{
await HandleErrorAsync(response, 400, "Invalid inactivityTimeoutSeconds. Must be greater than 0.");
return;
}
Manager.SetActivityBasedKeepAwake(
requestData.CpuThresholdPercent,
requestData.MemThresholdPercent,
requestData.NetThresholdKBps,
requestData.SampleIntervalSeconds,
requestData.InactivityTimeoutSeconds,
requestData.KeepDisplayOn ?? true,
"HttpServer");
var result = new
{
Success = true,
Mode = "Activity-based",
CpuThresholdPercent = requestData.CpuThresholdPercent,
MemThresholdPercent = requestData.MemThresholdPercent,
NetThresholdKBps = requestData.NetThresholdKBps,
SampleIntervalSeconds = requestData.SampleIntervalSeconds,
InactivityTimeoutSeconds = requestData.InactivityTimeoutSeconds,
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
};
await WriteJsonResponseAsync(response, result);
}
private async Task HandleSetPassiveAwakeAsync(HttpListenerResponse response)
{
Manager.SetPassiveKeepAwake(true, "HttpServer");
var result = new { Success = true, Mode = "Passive" };
await WriteJsonResponseAsync(response, result);
}
private async Task HandleToggleDisplayAsync(HttpListenerResponse response)
{
Manager.SetDisplay("HttpServer");
var result = new { Success = true, Action = "Display setting toggled" };
await WriteJsonResponseAsync(response, result);
}
private async Task HandleGetSettingsAsync(HttpListenerResponse response)
{
try
{
if (Manager.ModuleSettings != null)
{
var settings = Manager.ModuleSettings.GetSettings<AwakeSettings>(Constants.AppName);
await WriteJsonResponseAsync(response, settings);
}
else
{
await HandleErrorAsync(response, 500, "Settings module not available");
}
}
catch (Exception ex)
{
await HandleErrorAsync(response, 500, $"Error retrieving settings: {ex.Message}");
}
}
private async Task HandleNotFoundAsync(HttpListenerResponse response, string path)
{
var errorResponse = new
{
Error = $"Awake endpoint '/{path}' not found",
AvailableEndpoints = GetAvailableEndpoints(),
Module = ModuleName,
Timestamp = DateTimeOffset.Now,
};
response.StatusCode = 404;
await WriteJsonResponseAsync(response, errorResponse);
}
private async Task HandleErrorAsync(HttpListenerResponse response, int statusCode, string message)
{
response.StatusCode = statusCode;
var errorResponse = new
{
Error = message,
StatusCode = statusCode,
Module = ModuleName,
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, errorResponse);
}
private async Task WriteJsonResponseAsync(HttpListenerResponse response, object data)
{
// Use HttpServer utility method via static access or DI - for now, replicate the functionality
response.ContentType = "application/json";
response.ContentEncoding = System.Text.Encoding.UTF8;
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
var json = System.Text.Json.JsonSerializer.Serialize(
data, new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
WriteIndented = true,
});
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
var buffer = System.Text.Encoding.UTF8.GetBytes(json);
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer);
response.Close();
}
private async Task<T?> ReadJsonRequestAsync<T>(HttpListenerRequest request)
where T : class
{
if (!request.HasEntityBody)
{
return null;
}
using var reader = new System.IO.StreamReader(request.InputStream, System.Text.Encoding.UTF8);
var body = await reader.ReadToEndAsync();
if (string.IsNullOrWhiteSpace(body))
{
return null;
}
try
{
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
return System.Text.Json.JsonSerializer.Deserialize<T>(body, new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
});
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
}
catch (System.Text.Json.JsonException ex)
{
Logger.LogError($"Error deserializing request body: {ex.Message}");
return null;
}
}
// Request DTOs
private sealed class IndefiniteAwakeRequest
{
public bool? KeepDisplayOn { get; set; }
public int? ProcessId { get; set; }
}
private sealed class TimedAwakeRequest
{
public uint Seconds { get; set; }
public bool? KeepDisplayOn { get; set; }
}
private sealed class ExpirableAwakeRequest
{
public DateTimeOffset ExpireAt { get; set; }
public bool? KeepDisplayOn { get; set; }
}
private sealed class ActivityBasedAwakeRequest
{
public uint CpuThresholdPercent { get; set; }
public uint MemThresholdPercent { get; set; }
public uint NetThresholdKBps { get; set; }
public uint SampleIntervalSeconds { get; set; }
public uint InactivityTimeoutSeconds { get; set; }
public bool? KeepDisplayOn { get; set; }
}
}
}

View File

@@ -9,6 +9,7 @@ using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -17,6 +18,9 @@ using System.Text.Json;
using System.Threading;
using Awake.Core.Models;
using Awake.Core.Native;
// New usage tracking namespace
using Awake.Core.Usage;
using Awake.Properties;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -56,9 +60,26 @@ namespace Awake.Core
private static readonly BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource;
// Foreground usage tracker instance (lifecycle managed by Program)
internal static ForegroundUsageTracker? UsageTracker { get; set; }
private static PowerSchemeManager powerSchemeManager;
// Power scheme auto-switch (activity mode)
private static string? _originalPowerSchemeGuid;
private static bool _powerSchemeSwitched;
// Process monitoring fields
private static List<string> _processMonitoringList = [];
private static bool _processMonitoringActive;
private static IDisposable? _processMonitoringSubscription;
private static uint _processCheckInterval;
private static bool _processKeepDisplay;
static Manager()
{
_tokenSource = new CancellationTokenSource();
powerSchemeManager = new PowerSchemeManager();
_stateQueue = [];
ModuleSettings = new SettingsUtils();
}
@@ -123,6 +144,19 @@ namespace Awake.Core
: ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS;
}
// Activity mode state
private static bool _activityActive;
private static DateTimeOffset _activityLastHigh;
private static uint _activityCpu;
private static uint _activityMem;
private static uint _activityNetKBps;
private static uint _activitySample;
private static uint _activityTimeout;
private static bool _activityKeepDisplay;
private static PerformanceCounter? _cpuCounter;
private static PerformanceCounter? _memCounter;
private static List<PerformanceCounter>? _netCounters;
internal static void CancelExistingThread()
{
Logger.LogInfo("Ensuring the thread is properly cleaned up...");
@@ -130,6 +164,10 @@ namespace Awake.Core
// Reset the thread state and handle cancellation.
_stateQueue.Add(ExecutionState.ES_CONTINUOUS);
// Clean up process monitoring subscription if active
_processMonitoringSubscription?.Dispose();
_processMonitoringSubscription = null;
if (_tokenSource != null)
{
_tokenSource.Cancel();
@@ -174,6 +212,15 @@ namespace Awake.Core
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}]";
icon = TrayHelper.TimedIcon;
break;
case AwakeMode.ACTIVITY:
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_ACTIVITY}][{ScreenStateString}]";
icon = TrayHelper.IndefiniteIcon; // Placeholder icon
break;
case AwakeMode.PROCESS:
string processesText = _processMonitoringList.Count > 0 ? string.Join(", ", _processMonitoringList) : "None";
iconText = $"{Constants.FullAppName} [Process Monitor][{ScreenStateString}][{processesText}]";
icon = TrayHelper.IndefiniteIcon; // Use same icon as indefinite for now
break;
}
TrayHelper.SetShellIcon(
@@ -183,6 +230,94 @@ namespace Awake.Core
forceAdd ? TrayIconAction.Add : TrayIconAction.Update);
}
private static void CaptureOriginalPowerScheme()
{
try
{
powerSchemeManager.RefreshSchemes();
_originalPowerSchemeGuid = powerSchemeManager
.GetAllSchemes()
.FirstOrDefault(s => s.IsActive)?.PSGuid;
Logger.LogInfo($"Captured original power scheme: {_originalPowerSchemeGuid ?? "UNKNOWN"}");
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to capture original power scheme: {ex.Message}");
}
}
private static void SwitchToHighestPerformanceIfNeeded()
{
if (_powerSchemeSwitched)
{
return;
}
try
{
powerSchemeManager.RefreshSchemes();
var highest = powerSchemeManager.GetHighestPerformanceScheme();
if (highest == null)
{
Logger.LogInfo("No power schemes found when attempting high performance switch.");
return;
}
if (highest.IsActive)
{
Logger.LogInfo("Already on highest performance scheme no switch needed.");
return;
}
if (_originalPowerSchemeGuid == null)
{
CaptureOriginalPowerScheme();
}
if (powerSchemeManager.SwitchScheme(highest.PSGuid))
{
_powerSchemeSwitched = true;
Logger.LogInfo($"Switched to highest performance scheme: {highest.Name} ({highest.PSGuid})");
}
else
{
Logger.LogWarning($"Failed to switch to highest performance scheme: {highest.Name} ({highest.PSGuid})");
}
}
catch (Exception ex)
{
Logger.LogWarning($"Exception while attempting to switch power scheme: {ex.Message}");
}
}
private static void RestoreOriginalPowerSchemeIfNeeded()
{
if (!_powerSchemeSwitched || string.IsNullOrWhiteSpace(_originalPowerSchemeGuid))
{
return;
}
try
{
if (powerSchemeManager.SwitchScheme(_originalPowerSchemeGuid))
{
Logger.LogInfo($"Restored original power scheme: {_originalPowerSchemeGuid}");
}
else
{
Logger.LogWarning($"Failed to restore original power scheme: {_originalPowerSchemeGuid}");
}
}
catch (Exception ex)
{
Logger.LogWarning($"Exception restoring original power scheme: {ex.Message}");
}
finally
{
_powerSchemeSwitched = false;
}
}
internal static void SetIndefiniteKeepAwake(bool keepDisplayOn = false, int processId = 0, [CallerMemberName] string callerName = "")
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeIndefinitelyKeepAwakeEvent());
@@ -400,18 +535,24 @@ namespace Awake.Core
internal static void CompleteExit(int exitCode)
{
SetPassiveKeepAwake(updateSettings: false);
RestoreOriginalPowerSchemeIfNeeded();
if (TrayHelper.WindowHandle != IntPtr.Zero)
{
// Delete the icon.
TrayHelper.SetShellIcon(TrayHelper.WindowHandle, string.Empty, null, TrayIconAction.Delete);
// Close the message window that we used for the tray.
Bridge.SendMessage(TrayHelper.WindowHandle, Native.Constants.WM_CLOSE, 0, 0);
Bridge.DestroyWindow(TrayHelper.WindowHandle);
}
// Dispose usage tracker (flushes data)
try
{
UsageTracker?.Dispose();
}
catch (Exception ex)
{
Logger.LogWarning($"Failed disposing UsageTracker: {ex.Message}");
}
Bridge.PostQuitMessage(exitCode);
Environment.Exit(exitCode);
}
@@ -500,6 +641,275 @@ namespace Awake.Core
SetModeShellIcon();
}
internal static void SetActivityBasedKeepAwake(
uint cpuThresholdPercent,
uint memThresholdPercent,
uint netThresholdKBps,
uint sampleIntervalSeconds,
uint inactivityTimeoutSeconds,
bool keepDisplayOn,
[CallerMemberName] string callerName = "")
{
Logger.LogInfo($"Activity-based keep-awake invoked by {callerName}. CPU>={cpuThresholdPercent}%, MEM>={memThresholdPercent}%, NET>={netThresholdKBps}KB/s sample={sampleIntervalSeconds}s timeout={inactivityTimeoutSeconds}s display={keepDisplayOn}.");
CancelExistingThread();
if (IsUsingPowerToysConfig)
{
try
{
AwakeSettings currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
bool settingsChanged =
currentSettings.Properties.Mode != AwakeMode.ACTIVITY ||
currentSettings.Properties.ActivityCpuThresholdPercent != cpuThresholdPercent ||
currentSettings.Properties.ActivityMemoryThresholdPercent != memThresholdPercent ||
currentSettings.Properties.ActivityNetworkThresholdKBps != netThresholdKBps ||
currentSettings.Properties.ActivitySampleIntervalSeconds != sampleIntervalSeconds ||
currentSettings.Properties.ActivityInactivityTimeoutSeconds != inactivityTimeoutSeconds ||
currentSettings.Properties.KeepDisplayOn != keepDisplayOn;
if (settingsChanged)
{
currentSettings.Properties.Mode = AwakeMode.ACTIVITY;
currentSettings.Properties.ActivityCpuThresholdPercent = cpuThresholdPercent;
currentSettings.Properties.ActivityMemoryThresholdPercent = memThresholdPercent;
currentSettings.Properties.ActivityNetworkThresholdKBps = netThresholdKBps;
currentSettings.Properties.ActivitySampleIntervalSeconds = sampleIntervalSeconds;
currentSettings.Properties.ActivityInactivityTimeoutSeconds = inactivityTimeoutSeconds;
currentSettings.Properties.KeepDisplayOn = keepDisplayOn;
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
return; // Settings will be processed triggering this again
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to persist activity mode settings: {ex.Message}");
}
}
// Initialize performance counters
try
{
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
_memCounter = new PerformanceCounter("Memory", "% Committed Bytes In Use");
_netCounters = PerformanceCounterCategory.GetCategories()
.FirstOrDefault(c => c.CategoryName == "Network Interface")?
.GetInstanceNames()
.Select(n => new PerformanceCounter("Network Interface", "Bytes Total/sec", n))
.ToList() ?? new List<PerformanceCounter>();
_cpuCounter.NextValue(); // Prime CPU counter
}
catch (Exception ex)
{
Logger.LogError($"Failed to initialize performance counters for activity mode: {ex.Message}");
return;
}
_activityCpu = cpuThresholdPercent;
_activityMem = memThresholdPercent;
_activityNetKBps = netThresholdKBps;
_activitySample = Math.Max(1, sampleIntervalSeconds);
_activityTimeout = Math.Max(5, inactivityTimeoutSeconds);
_activityKeepDisplay = keepDisplayOn;
_activityLastHigh = DateTimeOffset.Now;
_activityActive = true;
CurrentOperatingMode = AwakeMode.ACTIVITY;
IsDisplayOn = keepDisplayOn;
SetModeShellIcon();
// Capture original scheme before any switch
CaptureOriginalPowerScheme();
TimeSpan sampleInterval = TimeSpan.FromSeconds(_activitySample);
Observable.Interval(sampleInterval).Subscribe(
_ =>
{
if (!_activityActive)
{
return;
}
float cpu = 0;
float mem = 0;
double netKBps = 0;
try
{
cpu = _cpuCounter?.NextValue() ?? 0;
mem = _memCounter?.NextValue() ?? 0;
if (_netCounters != null && _netCounters.Count > 0)
{
netKBps = _netCounters.Sum(c => c.NextValue()) / 1024.0;
}
}
catch (Exception ex)
{
Logger.LogError($"Performance counter read failure: {ex.Message}");
}
bool above =
(_activityCpu == 0 || cpu >= _activityCpu) ||
(_activityMem == 0 || mem >= _activityMem) ||
(_activityNetKBps == 0 || netKBps >= _activityNetKBps);
if (above)
{
_activityLastHigh = DateTimeOffset.Now;
_stateQueue.Add(ComputeAwakeState(_activityKeepDisplay));
SwitchToHighestPerformanceIfNeeded();
}
TrayHelper.SetShellIcon(
TrayHelper.WindowHandle,
$"{Constants.FullAppName} [Activity][{ScreenStateString}][CPU {cpu:0.#}% | MEM {mem:0.#}% | NET {netKBps:0.#}KB/s]",
TrayHelper.IndefiniteIcon,
TrayIconAction.Update);
if ((DateTimeOffset.Now - _activityLastHigh).TotalSeconds >= _activityTimeout)
{
Logger.LogInfo("Activity thresholds not met within timeout window. Ending activity mode.");
_activityActive = false;
CancelExistingThread();
RestoreOriginalPowerSchemeIfNeeded();
if (IsUsingPowerToysConfig)
{
SetPassiveKeepAwake();
}
else
{
CompleteExit(Environment.ExitCode);
}
}
},
ex =>
{
Logger.LogError($"Activity mode observable failure: {ex.Message}");
},
_tokenSource.Token);
}
internal static void SetProcessBasedKeepAwake(
List<string> processNames,
uint checkIntervalSeconds,
bool keepDisplayOn,
[CallerMemberName] string callerName = "")
{
Logger.LogInfo($"Process-based keep-awake invoked by {callerName}. Processes: [{string.Join(", ", processNames)}], CheckInterval: {checkIntervalSeconds}s, Display: {keepDisplayOn}.");
CancelExistingThread();
if (IsUsingPowerToysConfig)
{
try
{
AwakeSettings currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
bool settingsChanged =
currentSettings.Properties.Mode != AwakeMode.PROCESS ||
!currentSettings.Properties.ProcessMonitoringList.SequenceEqual(processNames) ||
currentSettings.Properties.ProcessCheckIntervalSeconds != checkIntervalSeconds ||
currentSettings.Properties.KeepDisplayOn != keepDisplayOn;
if (settingsChanged)
{
currentSettings.Properties.Mode = AwakeMode.PROCESS;
currentSettings.Properties.ProcessMonitoringList = new List<string>(processNames);
currentSettings.Properties.ProcessCheckIntervalSeconds = checkIntervalSeconds;
currentSettings.Properties.KeepDisplayOn = keepDisplayOn;
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
return; // Settings will be processed triggering this again
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to persist process monitoring settings: {ex.Message}");
}
}
_processMonitoringList = [.. processNames];
_processCheckInterval = Math.Max(1, checkIntervalSeconds);
_processKeepDisplay = keepDisplayOn;
_processMonitoringActive = true;
CurrentOperatingMode = AwakeMode.PROCESS;
IsDisplayOn = keepDisplayOn;
SetModeShellIcon();
TimeSpan checkInterval = TimeSpan.FromSeconds(_processCheckInterval);
Observable.Interval(checkInterval).Subscribe(
_ =>
{
if (!_processMonitoringActive)
{
return;
}
bool anyTargetProcessRunning = false;
List<string> runningProcesses = new();
try
{
foreach (string processName in _processMonitoringList)
{
if (string.IsNullOrWhiteSpace(processName))
{
continue;
}
// Remove .exe extension if present for process name comparison
string processNameWithoutExt = processName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
? processName.Substring(0, processName.Length - 4)
: processName;
Process[] processes = Process.GetProcessesByName(processNameWithoutExt);
if (processes.Length > 0)
{
anyTargetProcessRunning = true;
runningProcesses.Add(processNameWithoutExt);
}
}
}
catch (Exception ex)
{
Logger.LogError($"Process monitoring check failure: {ex.Message}");
}
if (anyTargetProcessRunning)
{
_stateQueue.Add(ComputeAwakeState(_processKeepDisplay));
TrayHelper.SetShellIcon(
TrayHelper.WindowHandle,
$"{Constants.FullAppName} [Process Monitor][{ScreenStateString}][Running: {string.Join(", ", runningProcesses)}]",
TrayHelper.IndefiniteIcon,
TrayIconAction.Update);
}
else
{
Logger.LogInfo("No target processes running. Ending process monitoring mode.");
_processMonitoringActive = false;
CancelExistingThread();
if (IsUsingPowerToysConfig)
{
SetPassiveKeepAwake();
}
else
{
CompleteExit(Environment.ExitCode);
}
}
},
ex =>
{
Logger.LogError($"Process monitoring observable failure: {ex.Message}");
},
_tokenSource.Token);
}
/// <summary>
/// Sets the display settings.
/// </summary>
@@ -533,6 +943,100 @@ namespace Awake.Core
}
}
/// <summary>
/// Gets the current Awake configuration status.
/// </summary>
/// <returns>Current configuration status information.</returns>
public static object GetCurrentConfig()
{
try
{
AwakeSettings currentSettings = ModuleSettings?.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
var baseConfig = new
{
mode = CurrentOperatingMode.ToString(),
keepDisplayOn = IsDisplayOn,
processId = ProcessId,
isUsingPowerToysConfig = IsUsingPowerToysConfig,
};
// Return different parameters based on the current mode
return CurrentOperatingMode switch
{
AwakeMode.PASSIVE => new
{
baseConfig.mode,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.INDEFINITE => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.TIMED => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
timeRemaining = TimeRemaining,
intervalHours = currentSettings.Properties.IntervalHours,
intervalMinutes = currentSettings.Properties.IntervalMinutes,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.EXPIRABLE => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
expireAt = ExpireAt == DateTimeOffset.MinValue ? (DateTimeOffset?)null : ExpireAt,
expirationDateTime = currentSettings.Properties.ExpirationDateTime,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.ACTIVITY => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
cpuThresholdPercent = currentSettings.Properties.ActivityCpuThresholdPercent,
memoryThresholdPercent = currentSettings.Properties.ActivityMemoryThresholdPercent,
networkThresholdKBps = currentSettings.Properties.ActivityNetworkThresholdKBps,
sampleIntervalSeconds = currentSettings.Properties.ActivitySampleIntervalSeconds,
inactivityTimeoutSeconds = currentSettings.Properties.ActivityInactivityTimeoutSeconds,
activityActive = _activityActive,
lastHighActivity = _activityLastHigh,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.PROCESS => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
processMonitoringList = currentSettings.Properties.ProcessMonitoringList,
processCheckIntervalSeconds = currentSettings.Properties.ProcessCheckIntervalSeconds,
processMonitoringActive = _processMonitoringActive,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
_ => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
error = "Unknown mode",
},
};
}
catch (Exception ex)
{
Logger.LogError($"Failed to get current config: {ex.Message}");
return new { error = "Failed to get current configuration", message = ex.Message };
}
}
public static Process? GetParentProcess()
{
return GetParentProcess(Process.GetCurrentProcess().Handle);

View File

@@ -10,6 +10,7 @@ namespace Awake.Core.Models
TC_MODE_PASSIVE = Native.Constants.WM_USER + 0x3,
TC_MODE_INDEFINITE = Native.Constants.WM_USER + 0x4,
TC_MODE_EXPIRABLE = Native.Constants.WM_USER + 0x5,
TC_MODE_ACTIVITY = Native.Constants.WM_USER + 0x6,
TC_EXIT = Native.Constants.WM_USER + 0x64,
TC_TIME = Native.Constants.WM_USER + 0x65,
}

View File

@@ -0,0 +1,44 @@
// 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;
namespace Awake.Core.Native
{
internal static class IdleTime
{
// Keep original native field names but suppress StyleCop (interop requires exact names).
[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO
{
#pragma warning disable SA1307 // Interop field naming
public uint cbSize;
public uint dwTime;
#pragma warning restore SA1307
}
[DllImport("user32.dll")]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
public static TimeSpan GetIdleTime()
{
LASTINPUTINFO info = new()
{
cbSize = (uint)Marshal.SizeOf<LASTINPUTINFO>(),
};
if (!GetLastInputInfo(ref info))
{
return TimeSpan.Zero;
}
// Calculate elapsed milliseconds since last input considering Environment.TickCount wrap.
uint lastInputTicks = info.dwTime;
uint nowTicks = (uint)Environment.TickCount;
uint delta = nowTicks >= lastInputTicks ? nowTicks - lastInputTicks : (uint.MaxValue - lastInputTicks) + nowTicks + 1;
return TimeSpan.FromMilliseconds(delta);
}
}
}

View File

@@ -0,0 +1,136 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Awake.Core
{
public class PowerSchemeManager
{
private readonly List<PowerScheme> _schemes = new();
public PowerSchemeManager()
{
RefreshSchemes();
}
public void RefreshSchemes()
{
_schemes.Clear();
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "powercfg",
Arguments = "/L",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
},
};
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
var matches = Regex.Matches(output, @"Power Scheme GUID: ([a-fA-F0-9\-]+)\s+\((.+?)\)(\s+\*)?");
foreach (Match match in matches)
{
_schemes.Add(new PowerScheme
{
PSGuid = match.Groups[1].Value,
Name = match.Groups[2].Value,
IsActive = match.Groups[3].Value.Contains('*'),
});
}
// Rank schemes by performance (descending)
_schemes.Sort((a, b) => GetScore(b).CompareTo(GetScore(a)));
}
/// <summary>
/// Returns all power schemes sorted by performance (highest first).
/// </summary>
public IReadOnlyList<PowerScheme> GetAllSchemes() => _schemes;
/// <summary>
/// Returns the highest performance scheme currently available (may already be active).
/// </summary>
public PowerScheme? GetHighestPerformanceScheme()
{
if (_schemes.Count == 0)
{
RefreshSchemes();
}
return _schemes.Count == 0 ? null : _schemes[0];
}
public bool SwitchScheme(string psGuid)
{
try
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "powercfg",
Arguments = $"/setactive {psGuid}",
UseShellExecute = false,
CreateNoWindow = true,
},
};
process.Start();
process.WaitForExit();
RefreshSchemes();
return true;
}
catch
{
return false;
}
}
private static int GetScore(PowerScheme scheme)
{
// Heuristic based on name (multi-language basic keywords).
string name = scheme.Name.ToLowerInvariant();
// High performance indicators
if (name.Contains("ultimate") || name.Contains("ultra"))
{
return 380;
}
if (name.Contains("high"))
{
return 310;
}
if (name.Contains("balanced"))
{
return 200;
}
if (name.Contains("saver"))
{
return 120;
}
// Default for unknown custom plans.
return 180;
}
public class PowerScheme
{
public string PSGuid { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool IsActive { get; set; }
}
}
}

View File

@@ -306,6 +306,19 @@ namespace Awake.Core
break;
}
case (uint)TrayCommands.TC_MODE_ACTIVITY:
{
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
Manager.SetActivityBasedKeepAwake(
settings.Properties.ActivityCpuThresholdPercent,
settings.Properties.ActivityMemoryThresholdPercent,
settings.Properties.ActivityNetworkThresholdKBps,
settings.Properties.ActivitySampleIntervalSeconds,
settings.Properties.ActivityInactivityTimeoutSeconds,
settings.Properties.KeepDisplayOn);
break;
}
case (uint)TrayCommands.TC_MODE_PASSIVE:
{
Manager.SetPassiveKeepAwake();
@@ -455,6 +468,7 @@ namespace Awake.Core
InsertMenuItem(0, TrayCommands.TC_MODE_PASSIVE, Resources.AWAKE_OFF, mode == AwakeMode.PASSIVE);
InsertMenuItem(0, TrayCommands.TC_MODE_INDEFINITE, Resources.AWAKE_KEEP_INDEFINITELY, mode == AwakeMode.INDEFINITE);
InsertMenuItem(0, TrayCommands.TC_MODE_EXPIRABLE, Resources.AWAKE_KEEP_UNTIL_EXPIRATION, mode == AwakeMode.EXPIRABLE, true);
InsertMenuItem(0, TrayCommands.TC_MODE_ACTIVITY, Resources.AWAKE_KEEP_ON_ACTIVITY, mode == AwakeMode.ACTIVITY);
}
}
}

View File

@@ -0,0 +1,364 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Timers;
using Awake.Core.Native;
using Awake.Core.Usage.Models;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Awake.Core.Usage
{
internal sealed class ForegroundUsageTracker : IDisposable
{
private const uint EventSystemForeground = 0x0003;
private const uint WinEventOutOfContext = 0x0000;
private const double CommitThresholdSeconds = 0.25;
private static readonly JsonSerializerOptions LegacySerializer = new()
{
WriteIndented = true,
};
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
private readonly object _lock = new();
private readonly string _legacyJsonPath;
private readonly string _dbPath;
private readonly Timer _flushTimer;
private readonly Timer _pollTimer;
private readonly TimeSpan _idleThreshold = TimeSpan.FromSeconds(60);
private readonly Dictionary<string, AppUsageRecord> _sessionCache = new(StringComparer.OrdinalIgnoreCase);
private IUsageStore _store;
private string? _activeProcess;
private DateTime _activeSince;
private IntPtr _hook;
private WinEventDelegate? _hookDelegate;
private IntPtr _lastHwnd;
private int _retentionDays;
private bool _disposed;
internal bool Enabled { get; private set; }
public ForegroundUsageTracker(string legacyJsonPath, int retentionDays)
{
_legacyJsonPath = legacyJsonPath;
_dbPath = Path.Combine(Path.GetDirectoryName(legacyJsonPath)!, "usage.sqlite");
_retentionDays = retentionDays;
_store = new SqliteUsageStore(_dbPath);
_flushTimer = new Timer(5000)
{
AutoReset = true,
};
_flushTimer.Elapsed += (_, _) => FlushInternal();
_pollTimer = new Timer(1000)
{
AutoReset = true,
};
_pollTimer.Elapsed += (_, _) => PollForeground();
TryImportLegacy();
}
private void TryImportLegacy()
{
try
{
if (!File.Exists(_legacyJsonPath))
{
return;
}
string json = File.ReadAllText(_legacyJsonPath);
List<AppUsageRecord> list = JsonSerializer.Deserialize<List<AppUsageRecord>>(json, LegacySerializer) ?? new();
foreach (AppUsageRecord r in list)
{
_store.AddSpan(r.ProcessName, r.TotalSeconds, r.FirstSeenUtc, r.LastUpdatedUtc, _retentionDays);
}
Logger.LogInfo("[AwakeUsage] Imported legacy usage.json into SQLite. Deleting old file.");
File.Delete(_legacyJsonPath);
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Legacy import failed: " + ex.Message);
}
}
public void Configure(bool enabled, int retentionDays)
{
_retentionDays = Math.Max(1, retentionDays);
if (enabled == Enabled)
{
return;
}
Enabled = enabled;
if (Enabled)
{
_activeSince = DateTime.UtcNow;
_hookDelegate = WinEventCallback;
_hook = SetWinEventHook(EventSystemForeground, EventSystemForeground, IntPtr.Zero, _hookDelegate, 0, 0, WinEventOutOfContext);
Logger.LogInfo(_hook != IntPtr.Zero ? "[AwakeUsage] WinEvent hook installed." : "[AwakeUsage] WinEvent hook failed (poll fallback)");
CaptureInitialForeground();
_flushTimer.Start();
_pollTimer.Start();
Logger.LogInfo("[AwakeUsage] Tracking enabled (5s flush, sqlite store).");
}
else
{
_flushTimer.Stop();
_pollTimer.Stop();
if (_hook != IntPtr.Zero)
{
UnhookWinEvent(_hook);
_hook = IntPtr.Zero;
}
CommitActiveSpan();
FlushInternal(force: true);
Logger.LogInfo("[AwakeUsage] Tracking disabled.");
}
}
private void WinEventCallback(
IntPtr hWinEventHook,
uint evt,
IntPtr hwnd,
int idObj,
int idChild,
uint thread,
uint time)
{
if (_disposed || !Enabled || evt != EventSystemForeground)
{
return;
}
HandleForegroundChange(hwnd, "hook");
}
private void PollForeground()
{
if (_disposed || !Enabled)
{
return;
}
IntPtr hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero || hwnd == _lastHwnd)
{
return;
}
HandleForegroundChange(hwnd, "poll");
}
private void CaptureInitialForeground()
{
IntPtr hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero)
{
return;
}
if (TryResolveProcess(hwnd, out string? name))
{
_activeProcess = name;
_activeSince = DateTime.UtcNow;
_lastHwnd = hwnd;
}
}
private bool TryResolveProcess(IntPtr hwnd, out string? name)
{
name = null;
try
{
uint pid;
uint tid = GetWindowThreadProcessId(hwnd, out pid);
if (tid == 0 || pid == 0)
{
return false;
}
using Process p = Process.GetProcessById((int)pid);
name = SafeProcessName(p);
return !string.IsNullOrWhiteSpace(name);
}
catch
{
return false;
}
}
private static string SafeProcessName(Process p)
{
try
{
return Path.GetFileName(p.MainModule?.FileName) ?? p.ProcessName;
}
catch
{
return p.ProcessName;
}
}
private void HandleForegroundChange(IntPtr hwnd, string source)
{
try
{
CommitActiveSpan();
if (!TryResolveProcess(hwnd, out string? name))
{
_activeProcess = null;
return;
}
_activeProcess = name;
_activeSince = DateTime.UtcNow;
_lastHwnd = hwnd;
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] FG change failed: " + ex.Message);
}
}
private void CommitActiveSpan()
{
if (string.IsNullOrEmpty(_activeProcess))
{
return;
}
if (IdleTime.GetIdleTime() > _idleThreshold)
{
_activeProcess = null;
return;
}
double secs = (DateTime.UtcNow - _activeSince).TotalSeconds;
if (secs < CommitThresholdSeconds)
{
return;
}
lock (_lock)
{
if (!_sessionCache.TryGetValue(_activeProcess!, out AppUsageRecord? rec))
{
rec = new AppUsageRecord
{
ProcessName = _activeProcess!,
FirstSeenUtc = DateTime.UtcNow,
LastUpdatedUtc = DateTime.UtcNow,
TotalSeconds = 0,
};
_sessionCache[_activeProcess!] = rec;
}
rec.TotalSeconds += secs;
rec.LastUpdatedUtc = DateTime.UtcNow;
}
_activeSince = DateTime.UtcNow;
}
private void FlushInternal(bool force = false)
{
try
{
CommitActiveSpan();
Dictionary<string, AppUsageRecord> snapshot;
lock (_lock)
{
snapshot = _sessionCache.ToDictionary(k => k.Key, v => v.Value);
_sessionCache.Clear();
}
foreach (AppUsageRecord rec in snapshot.Values)
{
_store.AddSpan(rec.ProcessName, rec.TotalSeconds, rec.FirstSeenUtc, rec.LastUpdatedUtc, _retentionDays);
}
if (force)
{
_store.Prune(_retentionDays);
}
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Flush failed: " + ex.Message);
}
}
public IReadOnlyList<AppUsageRecord> GetSummary(int top, int days)
{
CommitActiveSpan();
FlushInternal();
try
{
return _store.Query(top, days);
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Query failed: " + ex.Message);
return Array.Empty<AppUsageRecord>();
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
Configure(false, _retentionDays);
_store.Dispose();
_flushTimer.Dispose();
_pollTimer.Dispose();
}
}
}

View File

@@ -0,0 +1,21 @@
// 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.
#pragma warning disable SA1516, SA1636
using System;
using System.Collections.Generic;
using Awake.Core.Usage.Models;
namespace Awake.Core.Usage
{
internal interface IUsageStore : IDisposable
{
void AddSpan(string processName, double seconds, DateTime firstSeenUtc, DateTime lastUpdatedUtc, int retentionDays);
IReadOnlyList<AppUsageRecord> Query(int top, int days);
void Prune(int retentionDays);
}
}
#pragma warning restore SA1516, SA1636

View File

@@ -0,0 +1,24 @@
// 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.Text.Json.Serialization;
namespace Awake.Core.Usage.Models
{
internal sealed class AppUsageRecord
{
[JsonPropertyName("process")]
public string ProcessName { get; set; } = string.Empty;
[JsonPropertyName("totalSeconds")]
public double TotalSeconds { get; set; }
[JsonPropertyName("lastUpdatedUtc")]
public DateTime LastUpdatedUtc { get; set; }
[JsonPropertyName("firstSeenUtc")]
public DateTime FirstSeenUtc { get; set; }
}
}

View File

@@ -0,0 +1,145 @@
// 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.
#pragma warning disable SA1516, SA1210, SA1636
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using Awake.Core.Usage.Models;
using ManagedCommon;
using Microsoft.Data.Sqlite;
namespace Awake.Core.Usage
{
internal sealed class SqliteUsageStore : IUsageStore
{
private readonly string _dbPath;
private readonly string _connectionString;
public SqliteUsageStore(string dbPath)
{
_dbPath = dbPath;
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
_connectionString = new SqliteConnectionStringBuilder
{
DataSource = _dbPath,
Mode = SqliteOpenMode.ReadWriteCreate,
}.ToString();
Initialize();
}
private void Initialize()
{
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS process_usage (
process_name TEXT NOT NULL,
day_utc TEXT NOT NULL,
total_seconds REAL NOT NULL,
first_seen_utc TEXT NOT NULL,
last_updated_utc TEXT NOT NULL,
PRIMARY KEY(process_name, day_utc)
);";
cmd.ExecuteNonQuery();
}
public void AddSpan(string processName, double seconds, DateTime firstSeenUtc, DateTime lastUpdatedUtc, int retentionDays)
{
if (seconds <= 0)
{
return;
}
string day = DateTime.UtcNow.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteTransaction tx = conn.BeginTransaction();
using (SqliteCommand cmd = conn.CreateCommand())
{
cmd.Transaction = tx;
cmd.CommandText = @"INSERT INTO process_usage(process_name, day_utc, total_seconds, first_seen_utc, last_updated_utc)
VALUES($p,$d,$s,$f,$l)
ON CONFLICT(process_name,day_utc) DO UPDATE SET
total_seconds = total_seconds + excluded.total_seconds,
last_updated_utc = excluded.last_updated_utc;";
cmd.Parameters.AddWithValue("$p", processName);
cmd.Parameters.AddWithValue("$d", day);
cmd.Parameters.AddWithValue("$s", seconds);
cmd.Parameters.AddWithValue("$f", firstSeenUtc.ToString("o"));
cmd.Parameters.AddWithValue("$l", lastUpdatedUtc.ToString("o"));
cmd.ExecuteNonQuery();
}
using (SqliteCommand prune = conn.CreateCommand())
{
prune.Transaction = tx;
prune.CommandText = @"DELETE FROM process_usage WHERE day_utc < date('now', @retention);";
prune.Parameters.AddWithValue("@retention", $"-{Math.Max(1, retentionDays)} days");
prune.ExecuteNonQuery();
}
tx.Commit();
}
public IReadOnlyList<AppUsageRecord> Query(int top, int days)
{
List<AppUsageRecord> result = new();
int safeDays = Math.Max(1, days);
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"SELECT process_name, SUM(total_seconds) AS total_seconds, MIN(first_seen_utc) AS first_seen_utc, MAX(last_updated_utc) AS last_updated_utc
FROM process_usage
WHERE day_utc >= date('now', @cutoff)
GROUP BY process_name
ORDER BY total_seconds DESC
LIMIT @top;";
cmd.Parameters.AddWithValue("@cutoff", $"-{safeDays} days");
cmd.Parameters.AddWithValue("@top", top);
using SqliteDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
while (reader.Read())
{
try
{
string name = reader.GetString(0);
double secs = reader.GetDouble(1);
DateTime first = DateTime.Parse(reader.GetString(2), null, System.Globalization.DateTimeStyles.RoundtripKind);
DateTime last = DateTime.Parse(reader.GetString(3), null, System.Globalization.DateTimeStyles.RoundtripKind);
result.Add(new AppUsageRecord
{
ProcessName = name,
TotalSeconds = secs,
FirstSeenUtc = first,
LastUpdatedUtc = last,
});
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage][SQLite] Row parse failed: " + ex.Message);
}
}
return result;
}
public void Prune(int retentionDays)
{
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM process_usage WHERE day_utc < date('now', @cutoff);";
cmd.Parameters.AddWithValue("@cutoff", $"-{Math.Max(1, retentionDays)} days");
cmd.ExecuteNonQuery();
}
public void Dispose()
{
}
}
}
#pragma warning restore SA1516, SA1210, SA1636

View File

@@ -0,0 +1,31 @@
// 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;
namespace Awake
{
/// <summary>
/// COM automation interface exposed via ROT for controlling Awake.
/// </summary>
[ComVisible(true)]
[Guid("5CA92C1D-9D7E-4F6D-9B06-5B7B28BF4E21")]
public interface IAwakeAutomation
{
string Ping();
void SetIndefinite();
void SetTimed(int seconds);
void SetExpirable(int minutes);
void SetPassive();
void Cancel();
string GetStatusJson();
}
}

View File

@@ -18,6 +18,9 @@ using System.Threading.Tasks;
using Awake.Core;
using Awake.Core.Models;
using Awake.Core.Native;
// Usage tracking
using Awake.Core.Usage;
using Awake.Properties;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -33,12 +36,22 @@ namespace Awake
private static readonly string[] _aliasesPidOption = ["--pid", "-p"];
private static readonly string[] _aliasesExpireAtOption = ["--expire-at", "-e"];
private static readonly string[] _aliasesParentPidOption = ["--use-parent-pid", "-u"];
private static readonly string[] _aliasesHttpServerOption = ["--http-server", "-s"];
private static readonly string[] _aliasesHttpPortOption = ["--http-port"];
private static readonly string[] _aliasesActivityOption = ["--activity", "-a"];
private static readonly string[] _aliasesActivityCpuOption = ["--act-cpu"];
private static readonly string[] _aliasesActivityMemOption = ["--act-mem"];
private static readonly string[] _aliasesActivityNetOption = ["--act-net"];
private static readonly string[] _aliasesActivitySampleOption = ["--act-sample"];
private static readonly string[] _aliasesActivityTimeoutOption = ["--act-timeout"];
private static readonly JsonSerializerOptions _serializerOptions = new() { IncludeFields = true };
private static readonly ETWTrace _etwTrace = new();
private static FileSystemWatcher? _watcher;
private static SettingsUtils? _settingsUtils;
private static ManagedCommon.HttpServer? _httpServer;
private static AwakeHttpHandler? _awakeHttpHandler;
private static bool _startedFromPowerToys;
@@ -71,7 +84,11 @@ namespace Awake
}
await TrayHelper.InitializeTray(TrayHelper.DefaultAwakeIcon, Core.Constants.FullAppName);
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() => LockMutex?.ReleaseMutex());
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() =>
{
_httpServer?.Dispose();
LockMutex?.ReleaseMutex();
});
AppDomain.CurrentDomain.UnhandledException += AwakeUnhandledExceptionCatcher;
if (!instantiated)
@@ -96,6 +113,12 @@ namespace Awake
Logger.LogInfo($"OS: {Environment.OSVersion}");
Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
// Start background COM automation host (ROT) so any startup path exposes the automation surface.
// Uses default moniker; could be extended with a --rotname parameter if needed later.
var rotHost = new RotSingletonHost("Awake.Automation", () => new AwakeAutomation(), "AwakeAutomationRotThread");
rotHost.Start();
AppDomain.CurrentDomain.ProcessExit += (_, _) => rotHost.Stop();
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
Trace.WriteLine($"Task scheduler error: {args.Exception.Message}"); // somebody forgot to check!
@@ -145,6 +168,50 @@ namespace Awake
IsRequired = false,
};
Option<bool> httpServerOption = new(_aliasesHttpServerOption, () => true, "Enable HTTP server for remote control")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<int> httpPortOption = new(_aliasesHttpPortOption, () => 8080, "HTTP server port (default: 8080)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
// Activity-based keep-awake options
Option<bool> activityOption = new(_aliasesActivityOption, () => false, "Enable activity-based keep awake mode (CPU / Memory / Network)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activityCpuOption = new(_aliasesActivityCpuOption, () => 20, "Activity mode CPU threshold percent (0-100, 0 to ignore)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activityMemOption = new(_aliasesActivityMemOption, () => 50, "Activity mode Memory usage threshold percent (0-100, 0 to ignore)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activityNetOption = new(_aliasesActivityNetOption, () => 100, "Activity mode total network throughput threshold KB/s (0 to ignore)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activitySampleOption = new(_aliasesActivitySampleOption, () => 5, "Activity mode sampling interval in seconds (min 1)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activityTimeoutOption = new(_aliasesActivityTimeoutOption, () => 60, "Activity mode inactivity timeout in seconds before reverting (min 5)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
timeOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _))
@@ -175,6 +242,16 @@ namespace Awake
}
});
httpPortOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && (!int.TryParse(result.Tokens[0].Value, out int port) || port < 1 || port > 65535))
{
string errorMessage = $"HTTP port value could not be parsed correctly or is out of range (1-65535). Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
RootCommand? rootCommand =
[
configOption,
@@ -183,10 +260,56 @@ namespace Awake
pidOption,
expireAtOption,
parentPidOption,
httpServerOption,
httpPortOption,
activityOption,
activityCpuOption,
activityMemOption,
activityNetOption,
activitySampleOption,
activityTimeoutOption,
];
rootCommand.Description = Core.Constants.AppName;
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
// NOTE: The strongly-typed SetHandler overloads have an upper limit on the number of symbols
// they accept. We exceed that limit with the new activity-mode parameters. Switch to a context
// based handler and manually extract option values from the ParseResult.
rootCommand.SetHandler(context =>
{
var pr = context.ParseResult;
bool usePtConfig = pr.GetValueForOption(configOption);
bool displayOn = pr.GetValueForOption(displayOption);
uint timeLimit = pr.GetValueForOption(timeOption);
int pid = pr.GetValueForOption(pidOption);
string expireAt = pr.GetValueForOption(expireAtOption) ?? string.Empty;
bool useParentPid = pr.GetValueForOption(parentPidOption);
bool enableHttpServer = pr.GetValueForOption(httpServerOption);
int httpPort = pr.GetValueForOption(httpPortOption);
bool activityMode = pr.GetValueForOption(activityOption);
uint actCpu = pr.GetValueForOption(activityCpuOption);
uint actMem = pr.GetValueForOption(activityMemOption);
uint actNet = pr.GetValueForOption(activityNetOption);
uint actSample = pr.GetValueForOption(activitySampleOption);
uint actTimeout = pr.GetValueForOption(activityTimeoutOption);
HandleCommandLineArguments(
usePtConfig,
displayOn,
timeLimit,
pid,
expireAt,
useParentPid,
enableHttpServer,
httpPort,
activityMode,
actCpu,
actMem,
actNet,
actSample,
actTimeout);
});
return rootCommand.InvokeAsync(args).Result;
}
@@ -211,12 +334,13 @@ namespace Awake
private static void Exit(string message, int exitCode)
{
_httpServer?.Dispose();
_etwTrace?.Dispose();
Logger.LogInfo(message);
Manager.CompleteExit(exitCode);
}
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt, bool useParentPid)
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt, bool useParentPid, bool enableHttpServer, int httpPort, bool activityMode, uint actCpu, uint actMem, uint actNet, uint actSample, uint actTimeout)
{
if (pid == 0 && !useParentPid)
{
@@ -235,6 +359,35 @@ namespace Awake
Logger.LogInfo($"The value for --pid is: {pid}");
Logger.LogInfo($"The value for --expire-at is: {expireAt}");
Logger.LogInfo($"The value for --use-parent-pid is: {useParentPid}");
Logger.LogInfo($"The value for --http-server is: {enableHttpServer}");
Logger.LogInfo($"The value for --http-port is: {httpPort}");
Logger.LogInfo($"The value for --activity is: {activityMode}");
if (activityMode)
{
Logger.LogInfo($"Activity thresholds CPU={actCpu}% MEM={actMem}% NET={actNet}KB/s SAMPLE={actSample}s TIMEOUT={actTimeout}s");
}
// Initialize HTTP server if requested
if (enableHttpServer)
{
try
{
string prefix = $"http://localhost:{httpPort}/";
_httpServer = new ManagedCommon.HttpServer(prefix);
// Register Awake-specific handler
_awakeHttpHandler = new AwakeHttpHandler();
_httpServer.RegisterHandler(_awakeHttpHandler);
_httpServer.Start();
Logger.LogInfo($"HTTP server started on {prefix}");
Logger.LogInfo($"Available endpoints: GET {prefix}status, GET {prefix}awake/...");
}
catch (Exception ex)
{
Logger.LogError($"Failed to start HTTP server: {ex.Message}");
}
}
// Start the monitor thread that will be used to track the current state.
Manager.StartMonitor();
@@ -285,6 +438,11 @@ namespace Awake
Logger.LogError($"There was a problem with the configuration file. Make sure it exists. {ex.Message}");
}
}
else if (!usePtConfig && activityMode)
{
Logger.LogInfo("Starting activity-based keep-awake mode (CLI override).");
Manager.SetActivityBasedKeepAwake(actCpu, actMem, actNet, actSample, actTimeout, displayOn);
}
else if (pid != 0 || useParentPid)
{
// Second, we snap to process-based execution. Because this is something that
@@ -342,6 +500,33 @@ namespace Awake
}
}
}
// Initialize usage tracking
InitializeUsageTracking();
}
private static void InitializeUsageTracking()
{
try
{
string settingsPath = _settingsUtils!.GetSettingsFilePath(Core.Constants.AppName);
string directory = Path.GetDirectoryName(settingsPath)!;
string usageFile = Path.Combine(directory, "usage.json");
AwakeSettings settings = _settingsUtils.GetSettings<AwakeSettings>(Core.Constants.AppName) ?? new AwakeSettings();
if (Manager.UsageTracker == null)
{
Manager.UsageTracker = new ForegroundUsageTracker(usageFile, settings.Properties.UsageRetentionDays);
}
Manager.UsageTracker.Configure(settings.Properties.TrackUsageEnabled, settings.Properties.UsageRetentionDays);
Logger.LogInfo($"Usage tracking configured (enabled={settings.Properties.TrackUsageEnabled}, retentionDays={settings.Properties.UsageRetentionDays}).");
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize usage tracking: {ex.Message}");
}
}
private static void AllocateLocalConsole()
@@ -361,6 +546,7 @@ namespace Awake
SetupFileSystemWatcher(settingsPath);
InitializeSettings();
ProcessSettings();
InitializeUsageTracking(); // after initial settings load
}
catch (Exception ex)
{
@@ -406,6 +592,7 @@ namespace Awake
{
Logger.LogInfo("Detected a settings file change. Updating configuration...");
ProcessSettings();
InitializeUsageTracking(); // re-evaluate usage tracking on config change
}
catch (Exception e)
{
@@ -448,6 +635,22 @@ namespace Awake
Manager.SetExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn);
break;
case AwakeMode.ACTIVITY:
Manager.SetActivityBasedKeepAwake(
settings.Properties.ActivityCpuThresholdPercent,
settings.Properties.ActivityMemoryThresholdPercent,
settings.Properties.ActivityNetworkThresholdKBps,
settings.Properties.ActivitySampleIntervalSeconds,
settings.Properties.ActivityInactivityTimeoutSeconds,
settings.Properties.KeepDisplayOn);
break;
case AwakeMode.PROCESS:
Manager.SetProcessBasedKeepAwake(
settings.Properties.ProcessMonitoringList,
settings.Properties.ProcessCheckIntervalSeconds,
settings.Properties.KeepDisplayOn);
break;
default:
Logger.LogError("Unknown mode of operation. Check config file.");

View File

@@ -213,6 +213,15 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Keep awake on activity (CPU / Memory / Network).
/// </summary>
internal static string AWAKE_KEEP_ON_ACTIVITY {
get {
return ResourceManager.GetString("AWAKE_KEEP_ON_ACTIVITY", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to d.
/// </summary>
@@ -339,6 +348,15 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Activity.
/// </summary>
internal static string AWAKE_TRAY_TEXT_ACTIVITY {
get {
return ResourceManager.GetString("AWAKE_TRAY_TEXT_ACTIVITY", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unchecked.
/// </summary>

View File

@@ -146,6 +146,10 @@
<value>Keep awake until expiration date and time</value>
<comment>Keep the system awake until expiration date and time</comment>
</data>
<data name="AWAKE_KEEP_ON_ACTIVITY" xml:space="preserve">
<value>Keep awake on activity (CPU / Memory / Network)</value>
<comment>Keep the system awake while resource activity is above configured thresholds</comment>
</data>
<data name="AWAKE_MINUTE" xml:space="preserve">
<value>{0} minute</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
@@ -197,6 +201,9 @@
<data name="AWAKE_TRAY_TEXT_TIMED" xml:space="preserve">
<value>Interval</value>
</data>
<data name="AWAKE_TRAY_TEXT_ACTIVITY" xml:space="preserve">
<value>Activity</value>
</data>
<data name="AWAKE_LABEL_DAYS" xml:space="preserve">
<value>d</value>
<comment>Used to display number of days in the system tray tooltip.</comment>

View File

@@ -0,0 +1,131 @@
<#
AwakeRotMini.ps1 — Minimal ROT client for the Awake COM automation object.
Supported actions:
ping -> Calls Ping(), prints PING=pong
status -> Calls GetStatusJson(), prints JSON
cancel -> Calls Cancel(), prints CANCEL_OK
timed:<m> -> Calls SetTimed(<m * 60 seconds>), prints TIMED_OK (minutes input)
Assumptions:
- Server registered object in ROT via CreateItemMoniker("!", logicalName)
- We only need late binding (IDispatch InvokeMember) no type library.
Exit codes:
0 = success
2 = object not found in ROT
4 = call/parse error
NOTE: This script intentionally stays dependencylight and fast to start.
#>
param(
[string]$MonikerName = 'Awake.Automation',
[string]$Action = 'ping'
)
# ---------------------------------------------------------------------------
# Inline C# (single public class) handles:
# * ROT lookup via CreateItemMoniker("!", logicalName)
# * Late bound InvokeMember calls
# * Lightweight action dispatch
# ---------------------------------------------------------------------------
if (-not ('AwakeRotMiniClient' -as [type])) {
$code = @'
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
public static class AwakeRotMiniClient
{
// P/Invoke -------------------------------------------------------------
[DllImport("ole32.dll")] private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable rot);
[DllImport("ole32.dll")] private static extern int CreateBindCtx(int reserved, out IBindCtx ctx);
[DllImport("ole32.dll", CharSet = CharSet.Unicode)] private static extern int CreateItemMoniker(string delimiter, string item, out IMoniker mk);
// Internal helpers -----------------------------------------------------
private static void Open(out IRunningObjectTable rot, out IBindCtx ctx)
{
GetRunningObjectTable(0, out rot);
CreateBindCtx(0, out ctx);
}
private static object BindLogical(string logical)
{
Open(out var rot, out var ctx);
if (CreateItemMoniker("!", logical, out var mk) == 0)
{
try
{
rot.GetObject(mk, out var obj);
return obj;
}
catch
{
// Swallow treated as not found below.
}
}
return null;
}
private static object Call(object obj, string name, params object[] args)
{
var t = obj.GetType(); // System.__ComObject
return t.InvokeMember(
name,
System.Reflection.BindingFlags.InvokeMethod |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance,
null,
obj,
(args == null || args.Length == 0) ? null : args);
}
// Public entry ---------------------------------------------------------
public static string Exec(string logical, string action)
{
var obj = BindLogical(logical);
if (obj == null)
return "__NOT_FOUND__";
if (string.IsNullOrEmpty(action) || action == "ping")
{
try { return "PING=" + Call(obj, "Ping"); } catch (Exception ex) { return Err(ex); }
}
try
{
if (action == "status")
return (string)Call(obj, "GetStatusJson");
if (action == "cancel")
{
Call(obj, "Cancel");
return "CANCEL_OK";
}
if (action.StartsWith("timed:", StringComparison.OrdinalIgnoreCase))
{
var slice = action.Substring(6);
if (!int.TryParse(slice, out var minutes) || minutes < 0)
return "__ERR=Format:Invalid minutes";
Call(obj, "SetTimed", minutes * 60);
return "TIMED_OK";
}
return "UNKNOWN_ACTION";
}
catch (Exception ex)
{
return Err(ex);
}
}
private static string Err(Exception ex) => "__ERR=" + ex.GetType().Name + ":" + ex.Message;
}
'@
Add-Type -TypeDefinition $code -ErrorAction Stop | Out-Null
}
$result = [AwakeRotMiniClient]::Exec($MonikerName, $Action)
switch ($result) {
'__NOT_FOUND__' { exit 2 }
{ $_ -like '__ERR=*' } { $host.UI.WriteErrorLine($result); exit 4 }
default { Write-Output $result; exit 0 }
}

View File

@@ -0,0 +1,187 @@
<#
Minimal ROT test script for the runtimeregistered Awake automation object.
Usage:
.\AwakeRotTest.ps1 [-MonikerName Awake.Automation] -Action <action>
Actions:
ping -> PING=pong
status -> Returns JSON from GetStatusJson (placeholder now)
cancel -> Calls Cancel
timed:<m> -> Calls SetTimed(int minutes)
pingdbg -> Diagnostic: shows type info and invocation paths
Exit codes:
0 = success
2 = moniker not found
3 = (reserved for bind failure not currently used)
4 = method invocation failure
Notes:
- The automation object is registered with display name pattern: !<MonikerName>
- We late-bind via IDispatch InvokeMember because the object is surfaced as System.__ComObject.
- Keep this script selfcontained; avoid multiple Add-Type blocks to prevent type cache issues.
#>
param(
[string]$MonikerName = 'Awake.Automation',
[string]$Action
)
# ----------------------------
# Constants (exit codes)
# ----------------------------
Set-Variable -Name EXIT_OK -Value 0 -Option Constant
Set-Variable -Name EXIT_NOT_FOUND -Value 2 -Option Constant
Set-Variable -Name EXIT_CALL_FAIL -Value 4 -Option Constant
Write-Host '[AwakeRotTest] Start' -ForegroundColor Cyan
# ----------------------------
# C# helper (enumerate + bind + invoke)
# ----------------------------
if (-not ('AwakeRot.Client' -as [type])) {
$code = @'
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Collections.Generic;
using System.Text;
public static class AwakeRotClient
{
[DllImport("ole32.dll")] private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable rot);
[DllImport("ole32.dll")] private static extern int CreateBindCtx(int reserved, out IBindCtx ctx);
private static (IRunningObjectTable rot, IBindCtx ctx) Open()
{
GetRunningObjectTable(0, out var rot);
CreateBindCtx(0, out var ctx);
return (rot, ctx);
}
// (Enumeration removed for simplification list action no longer supported.)
// Direct bind using CreateItemMoniker fast-path (avoids full enumeration).
[DllImport("ole32.dll", CharSet = CharSet.Unicode)] private static extern int CreateItemMoniker(string lpszDelim, string lpszItem, out IMoniker ppmk);
private static object Bind(string display)
{
var (rot, ctx) = Open();
if (display.Length > 1 && display[0] == '!')
{
string logical = display.Substring(1);
if (CreateItemMoniker("!", logical, out var mk) == 0 && mk != null)
{
try
{
rot.GetObject(mk, out var directObj);
return directObj; // may be null if not found
}
catch { return null; }
}
}
return null; // No fallback enumeration (intentionally removed for simplicity/perf determinism)
}
// Strong-typed interface (early binding) mirrors server's IAwakeAutomation definition.
// GUIDs copied from IAwakeAutomation / AwakeAutomation server code.
[ComImport]
[Guid("5CA92C1D-9D7E-4F6D-9B06-5B7B28BF4E21")] // interface GUID
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAwakeAutomation
{
[PreserveSig] string Ping();
void SetIndefinite();
void SetTimed(int seconds);
void SetExpirable(int minutes);
void SetPassive();
void Cancel();
[PreserveSig] string GetStatusJson();
}
// Fallback late-binding helper (kept for diagnostic / if cast fails)
private static object CallLate(object obj, string name, params object[] args)
{
var t = obj.GetType();
return t.InvokeMember(
name,
System.Reflection.BindingFlags.InvokeMethod |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance,
binder: null,
target: obj,
args: (args == null || args.Length == 0) ? null : args);
}
public static string Exec(string logical, string action)
{
string display = "!" + logical;
var obj = Bind(display);
if (obj == null)
return "__NOT_FOUND__";
var t = obj.GetType();
try
{
// Try strong-typed cast first.
IAwakeAutomation api = obj as IAwakeAutomation;
bool typed = api != null;
if (action == "pingdbg")
{
var sb = new StringBuilder();
sb.AppendLine("TYPE=" + t.FullName);
sb.AppendLine("StrongTypedCast=" + typed);
if (typed)
{
try { sb.AppendLine("TypedPing=" + api.Ping()); } catch (Exception ex) { sb.AppendLine("TypedPingErr=" + ex.Message); }
}
else
{
try { sb.AppendLine("LatePing=" + CallLate(obj, "Ping")); } catch (Exception ex) { sb.AppendLine("LatePingErr=" + ex.Message); }
}
return sb.ToString();
}
if (string.IsNullOrEmpty(action) || action == "demo")
{
var ping = typed ? api.Ping() : (string)CallLate(obj, "Ping");
return $"DEMO Ping={ping}";
}
if (action == "ping") return "PING=" + (typed ? api.Ping() : (string)CallLate(obj, "Ping"));
if (action == "status") return typed ? api.GetStatusJson() : (string)CallLate(obj, "GetStatusJson");
if (action == "cancel") { if (typed) api.Cancel(); else CallLate(obj, "Cancel"); return "CANCEL_OK"; }
if (action != null && action.StartsWith("timed:"))
{
var m = int.Parse(action.Substring(6));
// NOTE: Server SetTimed expects seconds (per interface). Action timed:<m> originally treated value as minutes -> semantic mismatch.
// For now keep behavior (treat number as minutes -> convert to seconds for strong typed call) to avoid breaking existing usage.
if (typed) api.SetTimed(m * 60); else CallLate(obj, "SetTimed", m * 60);
return "TIMED_OK";
}
return "UNKNOWN_ACTION";
}
catch (Exception ex)
{
return "__CALL_ERROR__" + ex.GetType().Name + ":" + ex.Message;
}
}
}
'@
Add-Type -TypeDefinition $code -ErrorAction Stop | Out-Null
}
# Quick list fast-path
if ($Action -eq 'list') {
[AwakeRotClient]::List() | ForEach-Object { $_ }
exit $EXIT_OK
}
$result = [AwakeRotClient]::Exec($MonikerName, $Action)
switch ($result) {
'__NOT_FOUND__' { Write-Host "Moniker !$MonikerName not found." -ForegroundColor Red; [Environment]::Exit($EXIT_NOT_FOUND) }
{ $_ -like '__CALL_ERROR__*' } { Write-Host "Call failed: $result" -ForegroundColor Red; [Environment]::Exit($EXIT_CALL_FAIL) }
default { Write-Host $result -ForegroundColor Green; [Environment]::Exit($EXIT_OK) }
}

View File

@@ -5,7 +5,6 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -118,46 +117,36 @@ public partial class ContextMenuViewModel : ObservableObject,
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed. In case there are duplicate keybindings, the first
/// one is used and the rest are ignored.
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
private Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
var result = new Dictionary<KeyChord, CommandContextItemViewModel>();
var menu = CurrentContextMenu;
if (menu is null)
if (CurrentContextMenu is null)
{
return result;
return [];
}
foreach (var item in menu)
{
if (item is CommandContextItemViewModel cmd && cmd.HasRequestedShortcut)
{
var key = cmd.RequestedShortcut ?? new KeyChord(0, 0, 0);
var added = result.TryAdd(key, cmd);
if (!added)
{
Logger.LogWarning($"Ignoring duplicate keyboard shortcut {KeyChordHelpers.FormatForDebug(key)} on command '{cmd.Title ?? cmd.Name ?? "(unknown)"}'");
}
}
}
return result;
return CurrentContextMenu
.OfType<CommandContextItemViewModel>()
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = Keybindings();
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var item))
if (keybindings is not null)
{
return InvokeCommand(item);
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var item))
{
return InvokeCommand(item);
}
}
return null;

View File

@@ -51,36 +51,6 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject
DoOnUiThread(() => OnPropertyChanged(propertyName));
}
protected void UpdateProperty(string propertyName1, string propertyName2)
{
DoOnUiThread(() =>
{
OnPropertyChanged(propertyName1);
OnPropertyChanged(propertyName2);
});
}
protected void UpdateProperty(string propertyName1, string propertyName2, string propertyName3)
{
DoOnUiThread(() =>
{
OnPropertyChanged(propertyName1);
OnPropertyChanged(propertyName2);
OnPropertyChanged(propertyName3);
});
}
protected void UpdateProperty(params string[] propertyNames)
{
DoOnUiThread(() =>
{
foreach (var propertyName in propertyNames)
{
OnPropertyChanged(propertyName);
}
});
}
protected void ShowException(Exception ex, string? extensionHint = null)
{
if (PageContext.TryGetTarget(out var pageContext))

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
@@ -9,11 +10,14 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public partial class FiltersViewModel : ExtensionObjectViewModel
{
private readonly ExtensionObject<IFilters> _filtersModel;
private readonly ExtensionObject<IFilters> _filtersModel = new(null);
public string CurrentFilterId { get; private set; } = string.Empty;
[ObservableProperty]
public partial string CurrentFilterId { get; set; } = string.Empty;
public IFilterItemViewModel[] Filters { get; private set; } = [];
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShouldShowFilters))]
public partial IFilterItemViewModel[] Filters { get; set; } = [];
public bool ShouldShowFilters => Filters.Length > 0;
@@ -30,11 +34,23 @@ public partial class FiltersViewModel : ExtensionObjectViewModel
if (_filtersModel.Unsafe is not null)
{
var filters = _filtersModel.Unsafe.GetFilters();
Filters = BuildFilters(filters ?? []);
UpdateProperty(nameof(Filters), nameof(ShouldShowFilters));
Filters = filters.Select<IFilterItem, IFilterItemViewModel>(filter =>
{
var filterItem = filter as IFilter;
if (filterItem != null)
{
var filterVM = new FilterItemViewModel(filterItem!, PageContext);
filterVM.InitializeProperties();
CurrentFilterId = _filtersModel.Unsafe.CurrentFilterId ?? string.Empty;
UpdateProperty(nameof(CurrentFilterId));
return filterVM;
}
else
{
return new SeparatorViewModel();
}
}).ToArray();
CurrentFilterId = _filtersModel.Unsafe.CurrentFilterId;
return;
}
@@ -45,27 +61,7 @@ public partial class FiltersViewModel : ExtensionObjectViewModel
}
Filters = [];
UpdateProperty(nameof(Filters), nameof(ShouldShowFilters));
CurrentFilterId = string.Empty;
UpdateProperty(nameof(CurrentFilterId));
}
private IFilterItemViewModel[] BuildFilters(IFilterItem[] filters)
{
return [..filters.Select<IFilterItem, IFilterItemViewModel>(filter =>
{
if (filter is IFilter filterItem)
{
var filterItemViewModel = new FilterItemViewModel(filterItem!, PageContext);
filterItemViewModel.InitializeProperties();
return filterItemViewModel;
}
else
{
return new SeparatorViewModel();
}
})];
}
public override void SafeCleanup()
@@ -74,9 +70,9 @@ public partial class FiltersViewModel : ExtensionObjectViewModel
foreach (var filter in Filters)
{
if (filter is FilterItemViewModel filterItemViewModel)
if (filter is FilterItemViewModel filterVM)
{
filterItemViewModel.SafeCleanup();
filterVM.SafeCleanup();
}
}

View File

@@ -3,9 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
@@ -34,28 +32,12 @@ public interface IContextMenuContext : INotifyPropertyChanged
/// that have a shortcut key set.</returns>
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
var result = new Dictionary<KeyChord, CommandContextItemViewModel>();
var menu = MoreCommands;
if (menu is null)
{
return result;
}
foreach (var item in menu)
{
if (item is CommandContextItemViewModel cmd && cmd.HasRequestedShortcut)
{
var key = cmd.RequestedShortcut ?? new KeyChord(0, 0, 0);
var added = result.TryAdd(key, cmd);
if (!added)
{
Logger.LogWarning($"Ignoring duplicate keyboard shortcut {KeyChordHelpers.FormatForDebug(key)} on command '{cmd.Title ?? cmd.Name ?? "(unknown)"}'");
}
}
}
return result;
return MoreCommands
.OfType<CommandContextItemViewModel>()
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}

View File

@@ -59,7 +59,7 @@
<Rectangle
Height="1"
Margin="-16,-12,-12,-12"
Fill="{ThemeResource MenuFlyoutSeparatorBackground}" />
Fill="{ThemeResource MenuFlyoutSeparatorThemeBrush}" />
</DataTemplate>
</ResourceDictionary>
@@ -68,14 +68,11 @@
<ComboBox
Name="FiltersComboBox"
x:Uid="FiltersComboBox"
MinWidth="200"
VerticalAlignment="Center"
ItemTemplateSelector="{StaticResource FilterTemplateSelector}"
ItemsSource="{x:Bind ViewModel.Filters, Mode=OneWay}"
PlaceholderText="Filters"
PreviewKeyDown="FiltersComboBox_PreviewKeyDown"
SelectedValue="{x:Bind ViewModel.CurrentFilterId, Mode=OneWay}"
SelectedValuePath="Id"
SelectionChanged="FiltersComboBox_SelectionChanged"
Style="{StaticResource ComboBoxStyle}"
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">

View File

@@ -131,11 +131,6 @@ public sealed partial class SearchBar : UserControl,
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
e.Handled = true;
}
else if (ctrlPressed && e.Key == VirtualKey.I)
{
// Today you learned that Ctrl+I in a TextBox will insert a tab
e.Handled = true;
}
else if (e.Key == VirtualKey.Escape)
{
if (string.IsNullOrEmpty(FilterBox.Text))

View File

@@ -4,8 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
x:Name="ShortcutContentControl"
mc:Ignorable="d">
<Grid MinWidth="498" MinHeight="220">
@@ -66,7 +66,7 @@
IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}"
Severity="Warning" />
</Grid>
<tkcontrols:MarkdownTextBlock
<labToolkit:MarkdownTextBlock
x:Uid="InvalidShortcutWarningLabel"
Background="Transparent"
FontSize="12"

View File

@@ -4,8 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
@@ -36,7 +36,7 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<tkcontrols:MarkdownTextBlock
<labToolkit:MarkdownTextBlock
Grid.Column="1"
VerticalAlignment="Center"
Background="Transparent"

View File

@@ -2,7 +2,6 @@
// 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.CodeAnalysis;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -15,7 +14,6 @@ internal sealed partial class FilterTemplateSelector : DataTemplateSelector
public DataTemplate? Separator { get; set; }
[DynamicDependency(DynamicallyAccessedMemberTypes.All, "Microsoft.UI.Xaml.Controls.ComboBoxItem", "Microsoft.WinUI")]
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{
DataTemplate? dataTemplate = Default;

View File

@@ -11,20 +11,22 @@
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:markdownTextBlockRns="using:CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:toolkit="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
Background="Transparent"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<tkcontrols:MarkdownThemes
<markdownTextBlockRns:MarkdownThemes
x:Key="DefaultMarkdownThemeConfig"
H3FontSize="12"
H3FontWeight="Normal" />
<tkcontrols:MarkdownConfig x:Key="DefaultMarkdownConfig" Themes="{StaticResource DefaultMarkdownThemeConfig}" />
<labToolkit:MarkdownConfig x:Key="DefaultMarkdownConfig" Themes="{StaticResource DefaultMarkdownThemeConfig}" />
<StackLayout
x:Name="VerticalStackLayout"
@@ -50,7 +52,10 @@
<DataTemplate x:Key="MarkdownContentTemplate" x:DataType="viewModels:ContentMarkdownViewModel">
<Grid Margin="0,4,4,4" Padding="12,8,8,8">
<tkcontrols:MarkdownTextBlock Config="{StaticResource DefaultMarkdownConfig}" Text="{x:Bind Body, Mode=OneWay}" />
<labToolkit:MarkdownTextBlock
Background="Transparent"
Config="{StaticResource DefaultMarkdownConfig}"
Text="{x:Bind Body, Mode=OneWay}" />
</Grid>
</DataTemplate>
@@ -62,7 +67,10 @@
<DataTemplate x:Key="NestedMarkdownContentTemplate" x:DataType="viewModels:ContentMarkdownViewModel">
<Grid>
<tkcontrols:MarkdownTextBlock Config="{StaticResource DefaultMarkdownConfig}" Text="{x:Bind Body, Mode=OneWay}" />
<labToolkit:MarkdownTextBlock
Background="Transparent"
Config="{StaticResource DefaultMarkdownConfig}"
Text="{x:Bind Body, Mode=OneWay}" />
</Grid>
</DataTemplate>

View File

@@ -10,8 +10,9 @@
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"
xmlns:markdownTextBlockRns="using:CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
@@ -146,11 +147,11 @@
</ItemsControl>
</StackPanel>
</DataTemplate>
<tkcontrols:MarkdownThemes
<markdownTextBlockRns:MarkdownThemes
x:Key="DefaultMarkdownThemeConfig"
H3FontSize="12"
H3FontWeight="Normal" />
<tkcontrols:MarkdownConfig x:Key="DefaultMarkdownConfig" Themes="{StaticResource DefaultMarkdownThemeConfig}" />
<labToolkit:MarkdownConfig x:Key="DefaultMarkdownConfig" Themes="{StaticResource DefaultMarkdownThemeConfig}" />
</ResourceDictionary>
</Page.Resources>
@@ -326,6 +327,11 @@
x:Name="FiltersDropDown"
HorizontalAlignment="Right"
CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
<Grid.Transitions>
<TransitionCollection>
<RepositionThemeTransition />
</TransitionCollection>
</Grid.Transitions>
</Grid>
</Grid>
@@ -421,7 +427,7 @@
TextWrapping="WrapWholeWords"
Visibility="{x:Bind ViewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" />
<tkcontrols:MarkdownTextBlock
<labToolkit:MarkdownTextBlock
Grid.Row="2"
Margin="0,4,0,24"
Background="Transparent"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -88,11 +88,11 @@ public static class ServiceHelper
];
}
IconInfo icon = Icons.PlayIcon;
IconInfo icon = Icons.GreenCircleIcon;
switch (s.Status)
{
case ServiceControllerStatus.Stopped:
icon = Icons.StopIcon;
icon = Icons.RedCircleIcon;
break;
case ServiceControllerStatus.Running:
break;

View File

@@ -10,13 +10,17 @@ internal sealed class Icons
{
internal static IconInfo ServicesIcon { get; } = IconHelpers.FromRelativePath("Assets\\Services.svg");
internal static IconInfo StopIcon { get; } = IconHelpers.FromRelativePath("Assets\\service_stopped.png");
internal static IconInfo StopIcon { get; } = new IconInfo("\xE71A"); // Stop icon
internal static IconInfo PlayIcon { get; } = IconHelpers.FromRelativePath("Assets\\service_running.png");
internal static IconInfo PlayIcon { get; } = new IconInfo("\xEDB5"); // PlayBadge12 icon
internal static IconInfo RefreshIcon { get; } = new IconInfo("\xE72C"); // Refresh icon
internal static IconInfo OpenIcon { get; } = new IconInfo("\xE8A7"); // OpenInNewWindow icon
internal static IconInfo PauseIcon { get; } = IconHelpers.FromRelativePath("Assets\\service_paused.png");
internal static IconInfo GreenCircleIcon { get; } = new("\U0001f7e2"); // unicode LARGE GREEN CIRCLE
internal static IconInfo RedCircleIcon { get; } = new("\U0001F534"); // unicode LARGE RED CIRCLE
internal static IconInfo PauseIcon { get; } = new("\u23F8"); // unicode DOUBLE VERTICAL BAR, aka, "Pause"
}

View File

@@ -35,15 +35,6 @@
<Content Update="Assets\Services.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\service_paused.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\service_running.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\service_stopped.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">

View File

@@ -18,8 +18,8 @@ public partial class ServiceFilters : Filters
return [
new Filter() { Id = "all", Name = "All Services" },
new Separator(),
new Filter() { Id = "running", Name = "Running", Icon = Icons.PlayIcon },
new Filter() { Id = "stopped", Name = "Stopped", Icon = Icons.StopIcon },
new Filter() { Id = "running", Name = "Running", Icon = Icons.GreenCircleIcon },
new Filter() { Id = "stopped", Name = "Stopped", Icon = Icons.RedCircleIcon },
new Filter() { Id = "paused", Name = "Paused", Icon = Icons.PauseIcon },
];
}

View File

@@ -219,12 +219,7 @@ public partial class EvilSamplesPage : ListPage
}
],
},
new ListItem(new EvilDuplicateRequestedShortcut())
{
Title = "Evil keyboard shortcuts",
Subtitle = "Two commands with the same shortcut and more...",
Icon = new IconInfo("\uE765"),
}
];
public EvilSamplesPage()
@@ -419,42 +414,3 @@ internal sealed partial class EvilFastUpdatesPage : DynamicListPage
}
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
internal sealed partial class EvilDuplicateRequestedShortcut : ListPage
{
private readonly IListItem[] _items =
[
new ListItem(new NoOpCommand())
{
Title = "I'm evil!",
Subtitle = "I have multiple commands sharing the same keyboard shortcut",
MoreCommands = [
new CommandContextItem(new AnonymousCommand(() => new ToastStatusMessage("Me too executed").Show())
{
Result = CommandResult.KeepOpen(),
})
{
Title = "Me too",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
new CommandContextItem(new AnonymousCommand(() => new ToastStatusMessage("Me three executed").Show())
{
Result = CommandResult.KeepOpen(),
})
{
Title = "Me three",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
],
},
];
public override IListItem[] GetItems() => _items;
public EvilDuplicateRequestedShortcut()
{
Icon = new IconInfo(string.Empty);
Name = "Open";
}
}

View File

@@ -70,7 +70,7 @@ public partial class SampleFilters : Filters
[
new Filter() { Id = "all", Name = "All" },
new Filter() { Id = "mod2", Name = "Every 2nd", Icon = new IconInfo("2") },
new Filter() { Id = "mod3", Name = "Every 3rd (and long name)", Icon = new IconInfo("3") },
new Filter() { Id = "mod3", Name = "Every 3rd", Icon = new IconInfo("3") },
];
}
}

View File

@@ -6,7 +6,7 @@ using Windows.System;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public static partial class KeyChordHelpers
public partial class KeyChordHelpers
{
public static KeyChord FromModifiers(
bool ctrl = false,
@@ -34,28 +34,4 @@ public static partial class KeyChordHelpers
{
return FromModifiers(ctrl, alt, shift, win, (int)vkey, scanCode);
}
public static string FormatForDebug(KeyChord value)
{
var result = string.Empty;
if (value.Modifiers.HasFlag(VirtualKeyModifiers.Control))
{
result += "Ctrl+";
}
if (value.Modifiers.HasFlag(VirtualKeyModifiers.Shift))
{
result += "Shift+";
}
if (value.Modifiers.HasFlag(VirtualKeyModifiers.Menu))
{
result += "Alt+";
}
result += (VirtualKey)value.Vkey;
return result;
}
}

View File

@@ -4,6 +4,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:winuiex="using:WinUIEx"
MinWidth="480"
MinHeight="320"

View File

@@ -10,5 +10,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
INDEFINITE = 1,
TIMED = 2,
EXPIRABLE = 3,
ACTIVITY = 4, // Resource activity based keep-awake
PROCESS = 5, // Process-based keep-awake
}
}

View File

@@ -20,6 +20,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
IntervalMinutes = 1;
ExpirationDateTime = DateTimeOffset.Now;
CustomTrayTimes = [];
// Defaults for activity-based mode
ActivityCpuThresholdPercent = 20;
ActivityMemoryThresholdPercent = 50;
ActivityNetworkThresholdKBps = 100;
ActivitySampleIntervalSeconds = 5;
ActivityInactivityTimeoutSeconds = 60;
// Usage tracking defaults (opt-in, disabled by default)
TrackUsageEnabled = false; // default off
UsageRetentionDays = 14; // two weeks default retention
// Process monitoring defaults
ProcessMonitoringList = new List<string>();
ProcessCheckIntervalSeconds = 5; // Check every 5 seconds
}
[JsonPropertyName("keepDisplayOn")]
@@ -40,5 +55,36 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("customTrayTimes")]
[CmdConfigureIgnore]
public Dictionary<string, uint> CustomTrayTimes { get; set; }
// Activity-based mode configuration
[JsonPropertyName("activityCpuThresholdPercent")]
public uint ActivityCpuThresholdPercent { get; set; }
[JsonPropertyName("activityMemoryThresholdPercent")]
public uint ActivityMemoryThresholdPercent { get; set; }
[JsonPropertyName("activityNetworkThresholdKBps")]
public uint ActivityNetworkThresholdKBps { get; set; }
[JsonPropertyName("activitySampleIntervalSeconds")]
public uint ActivitySampleIntervalSeconds { get; set; }
[JsonPropertyName("activityInactivityTimeoutSeconds")]
public uint ActivityInactivityTimeoutSeconds { get; set; }
// New opt-in usage tracking flag
[JsonPropertyName("trackUsageEnabled")]
public bool TrackUsageEnabled { get; set; }
// Retention window for usage data (days)
[JsonPropertyName("usageRetentionDays")]
public int UsageRetentionDays { get; set; }
// Process monitoring configuration
[JsonPropertyName("processMonitoringList")]
public List<string> ProcessMonitoringList { get; set; }
[JsonPropertyName("processCheckIntervalSeconds")]
public uint ProcessCheckIntervalSeconds { get; set; }
}
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
@@ -38,9 +39,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library
KeepDisplayOn = Properties.KeepDisplayOn,
IntervalMinutes = Properties.IntervalMinutes,
IntervalHours = Properties.IntervalHours,
ActivityCpuThresholdPercent = Properties.ActivityCpuThresholdPercent,
ActivityMemoryThresholdPercent = Properties.ActivityMemoryThresholdPercent,
ActivityNetworkThresholdKBps = Properties.ActivityNetworkThresholdKBps,
ActivitySampleIntervalSeconds = Properties.ActivitySampleIntervalSeconds,
ActivityInactivityTimeoutSeconds = Properties.ActivityInactivityTimeoutSeconds,
// Fix old buggy default value that might be saved in Settings. Some components don't deal well with negative time zones and minimum time offsets.
ExpirationDateTime = Properties.ExpirationDateTime.Year < 2 ? DateTimeOffset.Now : Properties.ExpirationDateTime,
TrackUsageEnabled = Properties.TrackUsageEnabled,
UsageRetentionDays = Properties.UsageRetentionDays,
// Process monitoring properties
ProcessMonitoringList = Properties.ProcessMonitoringList != null ? new List<string>(Properties.ProcessMonitoringList) : new List<string>(),
ProcessCheckIntervalSeconds = Properties.ProcessCheckIntervalSeconds,
},
};
}

View File

@@ -181,6 +181,8 @@ namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
// Define namespaces
XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml";
XNamespace controls = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
XNamespace labs = "using:CommunityToolkit.Labs.WinUI";
XNamespace winui = "using:CommunityToolkit.WinUI.UI.Controls";
// Extract SettingsPageControl elements
var settingsPageElements = doc.Descendants()

View File

@@ -63,7 +63,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown" />
<PackageReference Include="System.Net.Http" />
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="System.Text.RegularExpressions" />

View File

@@ -9,7 +9,7 @@
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<UserControl.Resources>
<converters:ColorFormatConverter x:Key="ColorFormatConverter" />
@@ -61,7 +61,7 @@
</DataTemplate>
<ItemsPanelTemplate x:Key="ItemPanelTemplate">
<tkcontrols:WrapPanel
<tk7controls:WrapPanel
HorizontalSpacing="20"
Orientation="Horizontal"
VerticalSpacing="4" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
@@ -35,7 +35,7 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<tkcontrols:MarkdownTextBlock
<tk7controls:MarkdownTextBlock
Grid.Column="1"
Margin="8,0,0,0"
VerticalAlignment="Center"

View File

@@ -2,48 +2,16 @@
x:Class="Microsoft.PowerToys.Settings.UI.Controls.SettingsPageControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<UserControl.Resources>
<x:Double x:Key="PageMaxWidth">1000</x:Double>
<x:Double x:Key="PageHeaderMaxWidth">1020</x:Double>
<tkconverters:DoubleToVisibilityConverter
x:Name="doubleToVisibilityConverter"
FalseValue="Collapsed"
GreaterThan="0"
TrueValue="Visible" />
<animations:ImplicitAnimationSet x:Name="ShowTransitions">
<animations:OffsetAnimation
EasingMode="EaseOut"
From="0,24,0"
To="0"
Duration="0:0:0.4" />
<animations:OpacityAnimation
EasingMode="EaseOut"
From="0"
To="1"
Duration="0:0:0.2" />
</animations:ImplicitAnimationSet>
<animations:ImplicitAnimationSet x:Name="HideTransitions">
<animations:OffsetAnimation
EasingMode="EaseOut"
From="0"
To="0,24,0"
Duration="0:0:0.2" />
<animations:OpacityAnimation
EasingMode="EaseOut"
From="1"
To="0"
Duration="0:0:0.1" />
</animations:ImplicitAnimationSet>
<converters:UriToImageSourceConverter x:Key="UriToImageSourceConverter" />
</UserControl.Resources>
<Grid Padding="20,0,0,0" RowSpacing="24">
@@ -65,16 +33,14 @@
Padding="0,0,20,48"
ChildrenTransitions="{StaticResource SettingsCardsAnimations}"
RowSpacing="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Top panel -->
<Grid MaxWidth="{StaticResource PageMaxWidth}" ColumnSpacing="16" RowSpacing="16">
<Grid MaxWidth="{StaticResource PageMaxWidth}" RowSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
@@ -82,7 +48,6 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
MaxWidth="160"
@@ -100,12 +65,13 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ModuleDescription}"
TextWrapping="Wrap" />
<ToggleSwitch IsOn="True"/>
<ItemsControl Visibility="Collapsed"
<ItemsControl
x:Name="PrimaryLinksControl"
Margin="0,8,0,0"
IsTabStop="False"
ItemsSource="{x:Bind PrimaryLinks}">
ItemsSource="{x:Bind PrimaryLinks}"
Visibility="{x:Bind PrimaryLinks.Count, Converter={StaticResource DoubleToVisibilityConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="controls:PageLink">
<HyperlinkButton NavigateUri="{x:Bind Link}" Style="{StaticResource TextButtonStyle}">
@@ -115,81 +81,25 @@
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<tkcontrols:WrapPanel HorizontalSpacing="24" Orientation="Horizontal" />
<tk7controls:WrapPanel HorizontalSpacing="24" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
</Grid>
<Grid
<!-- Content panel -->
<ContentPresenter
x:Name="ModuleContentPresenter"
Grid.Row="1"
MaxWidth="{StaticResource PageMaxWidth}"
Margin="0,-48,0,0">
<SelectorBar x:Name="PivotBar">
<SelectorBarItem
IsSelected="True"
Tag="Settings"
Text="Settings" />
<SelectorBarItem Tag="Docs" Text="Documentation" />
</SelectorBar>
</Grid>
Margin="0,12,0,0"
Content="{x:Bind ModuleContent}" />
<toolkit:SwitchPresenter
Grid.Row="2"
MaxWidth="{StaticResource PageMaxWidth}"
Value="{Binding SelectedItem.Tag, ElementName=PivotBar}">
<toolkit:Case Value="Settings">
<!-- Content panel -->
<ContentPresenter
x:Name="ModuleContentPresenter"
Margin="0,12,0,0"
Content="{x:Bind ModuleContent}" />
</toolkit:Case>
<toolkit:Case Value="Docs">
<Grid
animations:Implicit.HideAnimations="{StaticResource HideTransitions}"
animations:Implicit.ShowAnimations="{StaticResource ShowTransitions}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ListView
x:Name="TocList"
Grid.Row="1"
Grid.Column="1"
IsItemClickEnabled="True"
ItemClick="TocList_ItemClick"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock
Margin="{Binding Indent}"
Tag="{Binding Id}"
Text="{Binding Title}"
TextWrapping="WrapWholeWords" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ScrollViewer
x:Name="DocScroll"
VerticalScrollBarVisibility="Auto"
VerticalScrollMode="Auto">
<StackPanel x:Name="DocHost" Orientation="Vertical" />
</ScrollViewer>
</Grid>
</toolkit:Case>
</toolkit:SwitchPresenter>
<!-- Bottom panel -->
<StackPanel
x:Name="SecondaryLinksPanel"
Grid.Row="3"
Grid.Row="2"
MaxWidth="{StaticResource PageMaxWidth}"
AutomationProperties.Name="{x:Bind SecondaryLinksHeader}"
Orientation="Vertical"
@@ -213,7 +123,7 @@
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<tkcontrols:WrapPanel HorizontalSpacing="24" Orientation="Horizontal" />
<tk7controls:WrapPanel HorizontalSpacing="24" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
@@ -232,9 +142,9 @@
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<!--<Setter Target="DescriptionPanel.(Grid.Row)" Value="1" />
<Setter Target="DescriptionPanel.(Grid.Row)" Value="1" />
<Setter Target="DescriptionPanel.(Grid.Column)" Value="0" />
<Setter Target="DescriptionPanel.(Grid.ColumnSpan)" Value="2" />-->
<Setter Target="DescriptionPanel.(Grid.ColumnSpan)" Value="2" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>

View File

@@ -3,56 +3,26 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.WinUI.Controls;
using Markdig;
using Markdig.Syntax;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using Windows.System;
namespace Microsoft.PowerToys.Settings.UI.Controls
{
public sealed partial class SettingsPageControl : UserControl
{
private readonly Dictionary<string, FrameworkElement> _anchors = new();
private readonly MarkdownPipeline _pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build();
// For section flyouts
private string _fullMarkdown = string.Empty;
private sealed class HeadingInfo
{
#pragma warning disable SA1401 // Fields should be private
public string Id = string.Empty;
public string Title = string.Empty;
public int Level;
public int Start;
public int End;
#pragma warning restore SA1401 // Fields should be private
}
private readonly List<HeadingInfo> _allHeadings = new();
public SettingsPageControl()
{
InitializeComponent();
this.InitializeComponent();
PrimaryLinks = new ObservableCollection<PageLink>();
SecondaryLinks = new ObservableCollection<PageLink>();
}
public string ModuleTitle
{
get => (string)GetValue(ModuleTitleProperty);
set => SetValue(ModuleTitleProperty, value);
get { return (string)GetValue(ModuleTitleProperty); }
set { SetValue(ModuleTitleProperty, value); }
}
public string ModuleDescription
@@ -75,8 +45,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public string SecondaryLinksHeader
{
get => (string)GetValue(SecondaryLinksHeaderProperty);
set => SetValue(SecondaryLinksHeaderProperty, value);
get { return (string)GetValue(SecondaryLinksHeaderProperty); }
set { SetValue(SecondaryLinksHeaderProperty, value); }
}
public ObservableCollection<PageLink> SecondaryLinks
@@ -87,471 +57,21 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public object ModuleContent
{
get => GetValue(ModuleContentProperty);
set => SetValue(ModuleContentProperty, value);
get { return (object)GetValue(ModuleContentProperty); }
set { SetValue(ModuleContentProperty, value); }
}
public static readonly DependencyProperty ModuleTitleProperty =
DependencyProperty.Register(nameof(ModuleTitle), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty ModuleDescriptionProperty =
DependencyProperty.Register(nameof(ModuleDescription), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty ModuleImageSourceProperty =
DependencyProperty.Register(nameof(ModuleImageSource), typeof(Uri), typeof(SettingsPageControl), new PropertyMetadata(null));
public static readonly DependencyProperty PrimaryLinksProperty =
DependencyProperty.Register(nameof(PrimaryLinks), typeof(ObservableCollection<PageLink>), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection<PageLink>()));
public static readonly DependencyProperty SecondaryLinksHeaderProperty =
DependencyProperty.Register(nameof(SecondaryLinksHeader), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty SecondaryLinksProperty =
DependencyProperty.Register(nameof(SecondaryLinks), typeof(ObservableCollection<PageLink>), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection<PageLink>()));
public static readonly DependencyProperty ModuleContentProperty =
DependencyProperty.Register(nameof(ModuleContent), typeof(object), typeof(SettingsPageControl), new PropertyMetadata(new Grid()));
public static readonly DependencyProperty ModuleTitleProperty = DependencyProperty.Register(nameof(ModuleTitle), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(defaultValue: null));
public static readonly DependencyProperty ModuleDescriptionProperty = DependencyProperty.Register(nameof(ModuleDescription), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(defaultValue: null));
public static readonly DependencyProperty ModuleImageSourceProperty = DependencyProperty.Register(nameof(ModuleImageSource), typeof(Uri), typeof(SettingsPageControl), new PropertyMetadata(null));
public static readonly DependencyProperty PrimaryLinksProperty = DependencyProperty.Register(nameof(PrimaryLinks), typeof(ObservableCollection<PageLink>), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection<PageLink>()));
public static readonly DependencyProperty SecondaryLinksHeaderProperty = DependencyProperty.Register(nameof(SecondaryLinksHeader), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty SecondaryLinksProperty = DependencyProperty.Register(nameof(SecondaryLinks), typeof(ObservableCollection<PageLink>), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection<PageLink>()));
public static readonly DependencyProperty ModuleContentProperty = DependencyProperty.Register(nameof(ModuleContent), typeof(object), typeof(SettingsPageControl), new PropertyMetadata(new Grid()));
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
_ = LoadAndRenderAsync("https://raw.githubusercontent.com/MicrosoftDocs/windows-dev-docs/refs/heads/docs/hub/powertoys/advanced-paste.md");
}
private sealed class TocItem
{
public string Id { get; init; } = string.Empty;
public string Title { get; init; } = string.Empty;
public int Level { get; init; }
}
private async Task LoadAndRenderAsync(string requestUrl)
{
using var client = new HttpClient();
var raw = await client.GetStringAsync(requestUrl);
// Preprocess with knowledge of the file URL (so we resolve ../images/...)
var md = PreprocessMarkdown(raw, requestUrl);
var tocItems = BuildDocumentAndAnchors(md);
// Bind ToC (indent H2/H3 a bit)
TocList.ItemsSource = tocItems.Select(i => new
{
i.Id,
i.Title,
Indent = new Thickness((i.Level - 1) * 12, 6, 8, 6),
}).ToList();
}
private List<TocItem> BuildDocumentAndAnchors(string md)
{
_fullMarkdown = md;
DocHost.Children.Clear();
_anchors.Clear();
_allHeadings.Clear();
var doc = Markdig.Markdown.Parse(md, _pipeline);
// Build slugs for ALL headings (H1..H6) so section flyouts can target any level
var rawHeadings = doc.Descendants<HeadingBlock>().ToList();
var seen = new Dictionary<string, int>();
foreach (var hb in rawHeadings)
{
string title = hb.Inline?.FirstChild?.ToString() ?? "Section";
string id = MakeSlug(title);
if (seen.TryGetValue(id, out int n))
{
n++;
seen[id] = n;
id = $"{id}-{n}";
}
else
{
seen[id] = 1;
}
_allHeadings.Add(new HeadingInfo
{
Id = id,
Title = title,
Level = hb.Level,
Start = hb.Span.Start,
End = md.Length, // fixed below
});
}
// Compute section End = next heading with level <= current level (or EOF)
for (int i = 0; i < _allHeadings.Count; i++)
{
for (int j = i + 1; j < _allHeadings.Count; j++)
{
if (_allHeadings[j].Level <= _allHeadings[i].Level)
{
_allHeadings[i].End = _allHeadings[j].Start;
break;
}
}
}
// Render the document in H2/H3 chunks for this UI
var headings = _allHeadings.Where(h => h.Level is 2 or 3).ToList();
var toc = new List<TocItem>();
if (headings.Count == 0)
{
DocHost.Children.Add(new MarkdownTextBlock { Text = md });
return toc;
}
foreach (var h in headings)
{
toc.Add(new TocItem { Id = h.Id, Title = h.Title, Level = h.Level });
// Invisible anchor just before the rendered section
var anchor = new Border { Height = 0, Opacity = 0, Tag = h.Id };
DocHost.Children.Add(anchor);
_anchors[h.Id] = anchor;
// Render this sections markdown (include heading line)
string sectionMd = md.Substring(h.Start, h.End - h.Start);
var mdtb = new MarkdownTextBlock { Text = sectionMd };
// NOTE: some toolkit versions use LinkClicked; you used OnLinkClicked in your snippet.
// Keep your version:
mdtb.OnLinkClicked += Markdown_LinkClicked;
DocHost.Children.Add(mdtb);
}
return toc;
}
private void Markdown_LinkClicked(object sender, CommunityToolkit.WinUI.Controls.LinkClickedEventArgs e)
{
var uri = e.Uri;
if (uri is null)
{
return;
}
string anchorId = null;
#pragma warning disable CA1310 // Specify StringComparison for correctness
#pragma warning disable CA1866 // Use char overload
if (!uri.IsAbsoluteUri && uri.OriginalString.StartsWith("#"))
{
anchorId = uri.OriginalString.TrimStart('#');
}
else if (uri.IsAbsoluteUri && !string.IsNullOrEmpty(uri.Fragment))
{
anchorId = uri.Fragment.TrimStart('#');
}
#pragma warning restore CA1866 // Use char overload
#pragma warning restore CA1310 // Specify StringComparison for correctness
if (!string.IsNullOrEmpty(anchorId) && _anchors.TryGetValue(anchorId, out _))
{
ScrollToAnchor(anchorId, TopOffset);
return;
}
_ = Launcher.LaunchUriAsync(uri);
}
private const double TopOffset = 40; // pixels
// Click in ToC -> scroll to anchor
private void TocList_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is FrameworkElement fe && fe.Tag is string id)
{
ScrollToAnchor(id, TopOffset);
return;
}
var idProp = e.ClickedItem?.GetType().GetProperty("Id")?.GetValue(e.ClickedItem) as string;
if (!string.IsNullOrEmpty(idProp))
{
ScrollToAnchor(idProp!, TopOffset);
}
}
private void ScrollToAnchor(string id, double topOffset = 0)
{
if (_anchors.TryGetValue(id, out var target))
{
var opts = new BringIntoViewOptions
{
VerticalAlignmentRatio = 0.0,
HorizontalAlignmentRatio = 0.0,
AnimationDesired = true,
};
target.StartBringIntoView(opts);
}
}
// ----------------------------
// Public API: show a section in a flyout (for any H1..H6)
// ----------------------------
public bool TryShowSectionFlyout(FrameworkElement placementTarget, string sectionId, bool includeHeading = false, FlyoutPlacementMode placement = FlyoutPlacementMode.Bottom)
{
if (placementTarget is null || string.IsNullOrWhiteSpace(sectionId))
{
return false;
}
var h = _allHeadings.FirstOrDefault(x => string.Equals(x.Id, sectionId, StringComparison.OrdinalIgnoreCase));
if (h is null)
{
return false;
}
var slice = _fullMarkdown.Substring(h.Start, h.End - h.Start);
if (!includeHeading)
{
int nl = slice.IndexOf('\n');
slice = nl >= 0 ? slice[(nl + 1)..] : string.Empty;
}
if (string.IsNullOrWhiteSpace(slice))
{
return false;
}
var title = new TextBlock
{
Text = h.Title,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 8),
Style = (Style)Application.Current.Resources["SubtitleTextBlockStyle"],
};
var body = new MarkdownTextBlock { Text = slice };
body.OnLinkClicked += Markdown_LinkClicked;
var content = new StackPanel { MinWidth = 320, MaxWidth = 560 };
content.Children.Add(title);
content.Children.Add(new ScrollViewer
{
Content = body,
MaxHeight = 420,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
});
var flyout = new Microsoft.UI.Xaml.Controls.Flyout { Content = content, Placement = placement };
flyout.ShowAt(placementTarget);
return true;
}
// ----------------------------
// Markdown preprocessor (MS Learn → standard Markdown)
// ----------------------------
public static string PreprocessMarkdown(string markdown, string sourceFileUrl)
{
// Compute the *directory* of the md file (guaranteed trailing slash)
var baseDir = new Uri(new Uri(sourceFileUrl), ".");
string Resolve(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return url;
}
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
return url;
}
return new Uri(baseDir, url).ToString();
}
// 1) Strip YAML front matter at the very top
markdown = Regex.Replace(
markdown,
pattern: @"\A---\s*[\s\S]*?^\s*---\s*$\r?\n?",
replacement: string.Empty,
options: RegexOptions.Multiline);
// 2) Remove specific Learn notice (example you had)
markdown = Regex.Replace(
markdown,
@"^>\s*\[!IMPORTANT\]\s*> - Phi Silica is not available in China\.\s*$\r?\n?",
string.Empty,
RegexOptions.Multiline | RegexOptions.IgnoreCase);
// 3) Convert Learn admonitions to simpler blockquotes with icons
var admonitions = new (string Pattern, string Replacement)[]
{
(@"^>\s*\[!IMPORTANT\]", "> ** Important:**"),
(@"^>\s*\[!NOTE\]", "> **❗ Note:**"),
(@"^>\s*\[!TIP\]", "> **💡 Tip:**"),
(@"^>\s*\[!WARNING\]", "> **⚠️ Warning:**"),
(@"^>\s*\[!CAUTION\]", "> **⚠️ Caution:**"),
};
foreach (var (pat, rep) in admonitions)
markdown = Regex.Replace(markdown, pat, rep, RegexOptions.Multiline | RegexOptions.IgnoreCase);
// 4) Convert :::image ... ::: blocks
markdown = Regex.Replace(
markdown,
@":::image\s+(?<attrs>.*?):::",
m =>
{
string attrs = m.Groups["attrs"].Value;
static string A(string attrs, string name)
{
var mm = Regex.Match(attrs, $@"\b{name}\s*=\s*""([^""]*)""", RegexOptions.IgnoreCase);
return mm.Success ? mm.Groups[1].Value : string.Empty;
}
string src = A(attrs, "source");
string alt = A(attrs, "alt-text");
string lightbox = A(attrs, "lightbox");
string link = A(attrs, "link");
src = Resolve(src);
lightbox = Resolve(lightbox);
link = Resolve(link);
var img = $"![{alt}]";
if (!string.IsNullOrWhiteSpace(src))
{
img += $"({src})";
}
var href = !string.IsNullOrWhiteSpace(link) ? link : lightbox;
if (!string.IsNullOrWhiteSpace(href))
{
img = $"[{img}]({href})";
}
return img + "\n";
},
RegexOptions.Singleline | RegexOptions.IgnoreCase);
// 5) Resolve relative links in standard markdown
markdown = Regex.Replace(
markdown,
@"\]\((?!https?://|mailto:|data:|#)(?<rel>[^)]+)\)",
m =>
{
var rel = m.Groups["rel"].Value.Trim();
var abs = Resolve(rel);
return $"]({abs})";
});
return markdown;
}
private static string MakeSlug(string s)
{
if (string.IsNullOrWhiteSpace(s))
{
return "section";
}
var slug = s.Trim().ToLowerInvariant();
slug = Regex.Replace(slug, @"[^\p{L}\p{Nd}\s-]", string.Empty);
slug = Regex.Replace(slug, @"\s+", "-");
slug = Regex.Replace(slug, @"-+", "-");
return slug;
}
}
// ------------------------------------------------------------
// Attached property: set DocSectionFlyout.SectionId on any Button
// inside ModuleContent, and it will open a flyout for that section.
// ------------------------------------------------------------
#pragma warning disable SA1402 // File may only contain a single type
public static class DocSectionFlyout
#pragma warning restore SA1402 // File may only contain a single type
{
public static readonly DependencyProperty SectionIdProperty =
DependencyProperty.RegisterAttached(
"SectionId",
typeof(string),
typeof(DocSectionFlyout),
new PropertyMetadata(null, OnSectionIdChanged));
public static void SetSectionId(DependencyObject obj, string value) => obj.SetValue(SectionIdProperty, value);
public static string GetSectionId(DependencyObject obj) => (string)obj.GetValue(SectionIdProperty);
public static readonly DependencyProperty IncludeHeadingProperty =
DependencyProperty.RegisterAttached(
"IncludeHeading",
typeof(bool),
typeof(DocSectionFlyout),
new PropertyMetadata(false));
public static void SetIncludeHeading(DependencyObject obj, bool value) => obj.SetValue(IncludeHeadingProperty, value);
public static bool GetIncludeHeading(DependencyObject obj) => (bool)obj.GetValue(IncludeHeadingProperty);
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.RegisterAttached(
"Placement",
typeof(FlyoutPlacementMode),
typeof(DocSectionFlyout),
new PropertyMetadata(FlyoutPlacementMode.Bottom));
public static void SetPlacement(DependencyObject obj, FlyoutPlacementMode value) => obj.SetValue(PlacementProperty, value);
public static FlyoutPlacementMode GetPlacement(DependencyObject obj) => (FlyoutPlacementMode)obj.GetValue(PlacementProperty);
private static void OnSectionIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ButtonBase btn)
{
btn.Click -= Button_Click;
if (e.NewValue is string { Length: > 0 })
{
btn.Click += Button_Click;
}
}
}
private static void Button_Click(object sender, RoutedEventArgs e)
{
if (sender is not FrameworkElement fe)
{
return;
}
// Find nearest SettingsPageControl ancestor
var parent = fe as DependencyObject;
SettingsPageControl host = null;
while (parent is not null)
{
parent = VisualTreeHelper.GetParent(parent);
if (parent is SettingsPageControl spc)
{
host = spc;
break;
}
}
if (host is null)
{
return;
}
var id = GetSectionId(fe);
if (string.IsNullOrWhiteSpace(id))
{
return;
}
var includeHeading = GetIncludeHeading(fe);
var placement = GetPlacement(fe);
host.TryShowSectionFlyout(fe, id, includeHeading, placement);
PrimaryLinksControl.Focus(FocusState.Programmatic);
}
}
}

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
x:Name="ShortcutContentControl"
mc:Ignorable="d">
<Grid MinWidth="498" MinHeight="220">
@@ -71,8 +71,9 @@
Message="{Binding ElementName=ShortcutContentControl, Path=ConflictMessage, Mode=OneWay}"
Severity="Warning" />
</Grid>
<tkcontrols:MarkdownTextBlock
<tk7controls:MarkdownTextBlock
x:Uid="InvalidShortcutWarningLabel"
Background="Transparent"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
@@ -38,10 +38,11 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<tkcontrols:MarkdownTextBlock
<tk7controls:MarkdownTextBlock
x:Name="LabelControl"
Grid.Column="1"
VerticalAlignment="Center"
Background="Transparent"
Text="{x:Bind Text}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="LabelPlacementStates">

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_AlwaysOnTop" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/AlwaysOnTop.png">
@@ -17,7 +17,7 @@
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_AlwaysOnTop_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_AlwaysOnTop_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_Awake" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/Awake.png">
@@ -13,11 +13,11 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_Awake_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_Awake_HowToUse" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_Awake_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_Awake_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_CmdNotFound" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/CmdNotFound.png">
@@ -13,7 +13,7 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_CmdNotFound_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_CmdNotFound_HowToUse" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_CmdPal" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/CmdPal.png">
@@ -17,7 +17,7 @@
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_CmdPal_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_CmdPal_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_ColorPicker" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/ColorPicker.gif">
@@ -17,7 +17,7 @@
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_ColorPicker_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_ColorPicker_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_FancyZones" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/FancyZones.gif">
@@ -13,12 +13,12 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_FancyZones_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_FancyZones_HowToUse" Background="Transparent" />
<controls:ShortcutWithTextLabelControl x:Name="HotkeyControl" x:Uid="Oobe_FancyZones_HowToUse_Shortcut" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_FancyZones_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_FancyZones_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_FileExplorer" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/FileExplorer.png">
@@ -13,7 +13,7 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToEnable" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_FileExplorer_HowToEnable" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_FileExplorer_HowToEnable" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_FileLocksmith" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/FileLocksmith.gif">
@@ -13,11 +13,11 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_FileLocksmith_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_FileLocksmith_HowToUse" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_FileLocksmith_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_FileLocksmith_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_ImageResizer" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/ImageResizer.gif">
@@ -13,11 +13,11 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToLaunch" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_ImageResizer_HowToLaunch" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_ImageResizer_HowToLaunch" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_ImageResizer_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_ImageResizer_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_KBM" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/KBM.gif">
@@ -13,11 +13,11 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToCreateMappings" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_KBM_HowToCreateMappings" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_KBM_HowToCreateMappings" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_KBM_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_KBM_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_MeasureTool" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/ScreenRuler.gif">
@@ -15,7 +15,7 @@
<controls:ShortcutWithTextLabelControl x:Name="HotkeyActivation" x:Uid="Oobe_MeasureTool_Activation" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_MeasureTool_HowToLaunch" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_MeasureTool_HowToLaunch" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,23 +5,23 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_MouseUtils" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/MouseUtils.gif">
<controls:OOBEPageControl.PageContent>
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_MouseUtils_FindMyMouse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_MouseUtils_FindMyMouse_Description" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_MouseUtils_FindMyMouse_Description" Background="Transparent" />
<TextBlock x:Uid="Oobe_MouseUtils_MouseHighlighter" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_MouseUtils_MouseHighlighter_Description" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_MouseUtils_MouseHighlighter_Description" Background="Transparent" />
<TextBlock x:Uid="Oobe_MouseUtils_MousePointerCrosshairs" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_MouseUtils_MousePointerCrosshairs_Description" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_MouseUtils_MousePointerCrosshairs_Description" Background="Transparent" />
<TextBlock x:Uid="Oobe_MouseUtils_MouseJump" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_MouseUtils_MouseJump_Description" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_MouseUtils_MouseJump_Description" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_MouseWithoutBorders" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/MouseWithoutBorders.png">
@@ -14,11 +14,11 @@
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_MouseWithoutBorders_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_MouseWithoutBorders_HowToUse" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_MouseWithoutBorders_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_MouseWithoutBorders_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<!-- TODO: Create New+ overview .gif and update ref here -->
@@ -13,10 +13,10 @@
<controls:OOBEPageControl.PageContent>
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_NewPlus_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_NewPlus_HowToUse" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_NewPlus_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_NewPlus_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -7,6 +7,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
Loaded="Page_Loaded"
mc:Ignorable="d">

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_QuickAccent" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/QuickAccent.gif">
@@ -13,7 +13,7 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_QuickAccent_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_QuickAccent_HowToUse" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_TextExtractor" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/TextExtractor.gif">
@@ -17,7 +17,7 @@
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_TextExtractor_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_TextExtractor_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_PowerRename" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/PowerRename.gif">
@@ -13,11 +13,11 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_PowerRename_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_PowerRename_HowToUse" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_PowerRename_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_PowerRename_TipsAndTricks" Background="Transparent" />
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_RegistryPreview" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/RegistryPreview.png">
@@ -13,11 +13,11 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_RegistryPreview_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_RegistryPreview_HowToUse" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_RegistryPreview_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_RegistryPreview_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_Run" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/Run.gif">
@@ -17,7 +17,7 @@
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_Run_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_Run_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button

View File

@@ -5,25 +5,11 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
Loaded="Page_Loaded"
mc:Ignorable="d">
<Page.Resources>
<tkcontrols:MarkdownThemes
x:Key="ReleaseNotesMarkdownThemeConfig"
H1FontSize="22"
H1FontWeight="SemiBold"
H1Margin="0, 36, 0, 8"
H2FontSize="16"
H2FontWeight="SemiBold"
H2Margin="0, 16, 0, 4"
H3FontSize="16"
H3FontWeight="SemiBold"
H3Margin="0, 16, 0, 4"
HeadingForeground="{StaticResource TextFillColorPrimaryBrush}" />
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
</Page.Resources>
<!-- Main layout container -->
<Grid>
@@ -177,14 +163,23 @@
VerticalAlignment="Center"
IsIndeterminate="True"
Visibility="Visible" />
<tkcontrols:MarkdownTextBlock
<tk7controls:MarkdownTextBlock
x:Name="ReleaseNotesMarkdown"
Config="{StaticResource ReleaseNotesMarkdownConfig}"
UseAutoLinks="True"
UseEmphasisExtras="True"
UseListExtras="True"
UsePipeTables="True"
UseTaskLists="True" />
VerticalAlignment="Top"
Background="Transparent"
Header1FontSize="20"
Header1FontWeight="SemiBold"
Header1Margin="0,0,0,4"
Header3FontSize="16"
Header3FontWeight="SemiBold"
Header4FontSize="16"
Header4FontWeight="SemiBold"
HorizontalRuleMargin="24"
LinkClicked="ReleaseNotesMarkdown_LinkClicked"
ListMargin="-18,4,0,12"
ParagraphMargin="0,0,0,0"
TableMargin="24"
Visibility="Collapsed" />
</Grid>
</ScrollViewer>
</Grid>

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -12,7 +13,8 @@ using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.WinUI.Controls;
using CommunityToolkit.WinUI.UI.Controls;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
@@ -23,10 +25,7 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
@@ -258,6 +257,17 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
}
}
private void ReleaseNotesMarkdown_LinkClicked(object sender, LinkClickedEventArgs e)
{
if (Uri.TryCreate(e.Link, UriKind.Absolute, out Uri link))
{
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
Process.Start(new ProcessStartInfo(link.ToString()) { UseShellExecute = true });
});
}
}
private void DataDiagnostics_InfoBar_YesNo_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
string commandArg = string.Empty;

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_Workspaces" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/Workspaces.png">
@@ -13,12 +13,12 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_Workspaces_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_Workspaces_HowToUse" Background="Transparent" />
<controls:ShortcutWithTextLabelControl x:Name="HotkeyControl" x:Uid="Oobe_Workspaces_HowToUse_Shortcut" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_Workspaces_TipsAndTricks" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_Workspaces_TipsAndTricks" Background="Transparent" />
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -5,7 +5,7 @@
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_ZoomIt" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/ZoomIt.gif">
@@ -13,7 +13,7 @@
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_ZoomIt_HowToUse" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_ZoomIt_HowToUse" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -46,9 +46,6 @@
ChildrenTransitions="{StaticResource SettingsCardsAnimations}"
Orientation="Vertical"
Spacing="2">
<StackPanel Orientation="Horizontal" Spacing="8">
<!-- Include the heading line in the flyout and place it at the right -->
</StackPanel>
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="AdvancedPasteEnableToggleControlHeaderText"
@@ -89,11 +86,7 @@
<tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Vertical">
<TextBlock x:Uid="AdvancedPaste_EnableAISettingsCardDescription" />
<HyperlinkButton
x:Uid="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore"
controls:DocSectionFlyout.IncludeHeading="True"
controls:DocSectionFlyout.Placement="Right"
controls:DocSectionFlyout.SectionId="paste-text-with-ai" />
<HyperlinkButton x:Uid="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore" NavigateUri="https://learn.microsoft.com/windows/powertoys/advanced-paste" />
</StackPanel>
</tkcontrols:SettingsCard.Description>
</tkcontrols:SettingsCard>

Some files were not shown because too many files have changed in this diff Show More