mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-30 00:46:20 +01:00
Compare commits
1 Commits
shawn/comS
...
cinnamon-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec6693359 |
3
.github/actions/spell-check/allow/code.txt
vendored
3
.github/actions/spell-check/allow/code.txt
vendored
@@ -94,7 +94,6 @@ onefuzzingestionpreparationtool
|
||||
OTP
|
||||
Yubi
|
||||
Yubico
|
||||
Perplexity
|
||||
svgl
|
||||
|
||||
# KEYS
|
||||
@@ -320,4 +319,4 @@ MRUINFO
|
||||
REGSTR
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
INVOKEIDLIST
|
||||
INVOKEIDLIST
|
||||
2
.github/actions/spell-check/allow/names.txt
vendored
2
.github/actions/spell-check/allow/names.txt
vendored
@@ -29,8 +29,6 @@ shortcutguide
|
||||
|
||||
# 8LWXpg is user name but user folder causes a flag
|
||||
LWXpg
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
x6f677548
|
||||
Adoumie
|
||||
Advaith
|
||||
alekhyareddy
|
||||
|
||||
15
.github/actions/spell-check/expect.txt
vendored
15
.github/actions/spell-check/expect.txt
vendored
@@ -247,7 +247,6 @@ CONFIGW
|
||||
CONFLICTINGMODIFIERKEY
|
||||
CONFLICTINGMODIFIERSHORTCUT
|
||||
CONOUT
|
||||
coreclr
|
||||
constexpr
|
||||
contentdialog
|
||||
contentfiles
|
||||
@@ -269,8 +268,6 @@ cpcontrols
|
||||
cph
|
||||
cplusplus
|
||||
CPower
|
||||
cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
createdump
|
||||
CREATEPROCESS
|
||||
@@ -282,7 +279,6 @@ CRH
|
||||
critsec
|
||||
cropandlock
|
||||
Crossdevice
|
||||
csdevkit
|
||||
CSearch
|
||||
CSettings
|
||||
cso
|
||||
@@ -310,7 +306,6 @@ CXVIRTUALSCREEN
|
||||
CYSCREEN
|
||||
CYSMICON
|
||||
CYVIRTUALSCREEN
|
||||
Czechia
|
||||
cziplib
|
||||
Dac
|
||||
dacl
|
||||
@@ -335,7 +330,6 @@ Deact
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
Deduplicator
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
DEFAULTCOLOR
|
||||
@@ -441,7 +435,6 @@ EDITSHORTCUTS
|
||||
EDITTEXT
|
||||
EFile
|
||||
ekus
|
||||
emojis
|
||||
ENABLEDELAYEDEXPANSION
|
||||
ENABLEDPOPUP
|
||||
ENABLETAB
|
||||
@@ -806,7 +799,6 @@ KEYBOARDMANAGEREDITORLIBRARYWRAPPER
|
||||
keyboardmanagerstate
|
||||
keyboardmanagerui
|
||||
keyboardtester
|
||||
keycap
|
||||
KEYEVENTF
|
||||
KEYIMAGE
|
||||
keynum
|
||||
@@ -1455,6 +1447,7 @@ rstringalnum
|
||||
rstringalpha
|
||||
rstringdigit
|
||||
rtb
|
||||
RTB
|
||||
RTLREADING
|
||||
rtm
|
||||
runas
|
||||
@@ -1787,13 +1780,10 @@ UACUI
|
||||
UAL
|
||||
uap
|
||||
UBR
|
||||
UBreak
|
||||
ubrk
|
||||
UCallback
|
||||
ucrt
|
||||
ucrtd
|
||||
uefi
|
||||
UError
|
||||
uesc
|
||||
UFlags
|
||||
UHash
|
||||
@@ -1801,7 +1791,6 @@ UIA
|
||||
UIEx
|
||||
uild
|
||||
uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
ums
|
||||
uncompilable
|
||||
@@ -1864,7 +1853,6 @@ VFT
|
||||
vget
|
||||
vgetq
|
||||
viewmodels
|
||||
virama
|
||||
VIRTKEY
|
||||
VIRTUALDESK
|
||||
VISEGRADRELAY
|
||||
@@ -2016,7 +2004,6 @@ XButton
|
||||
xclip
|
||||
xcopy
|
||||
XDeployment
|
||||
xdf
|
||||
XDocument
|
||||
XElement
|
||||
xfd
|
||||
|
||||
43
.github/copilot-instructions.md
vendored
43
.github/copilot-instructions.md
vendored
@@ -1,43 +0,0 @@
|
||||
# PowerToys – Copilot guide (concise)
|
||||
|
||||
This is the top-level guide for AI changes. Keep edits small, follow existing patterns, and cite exact paths in PRs.
|
||||
|
||||
Repo map (1‑line per area)
|
||||
- Core apps: `src/runner/**` (tray/loader), `src/settings-ui/**` (Settings app)
|
||||
- Shared libs: `src/common/**`
|
||||
- Modules: `src/modules/*` (one per utility; Command Palette in `src/modules/cmdpal/**`)
|
||||
- Build tools/docs: `tools/**`, `doc/devdocs/**`
|
||||
|
||||
Build and test (defaults)
|
||||
- Prerequisites: Visual Studio 2022 17.4+, minimal Windows 10 1803+.
|
||||
- Build discipline:
|
||||
- One terminal per operation (build → test). Don’t switch/open new ones mid-flow.
|
||||
- After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`).
|
||||
- Use script(s) to build, synchronously block and wait in foreground for it to finish: `tools/build/build.ps1|.cmd` (current folder), `build-essentials.*` (once per brand new build for missing nuget packages)
|
||||
- Treat build **exit code 0** as success; any non-zero exit code is a failure, have Copilot read the errors log in the build folder (e.g., `build.*.*.errors.log`) and surface problems.
|
||||
- Don’t start tests or launch Runner until the previous step succeeded.
|
||||
- Tests (fast + targeted):
|
||||
- Find the test project by product code prefix (e.g., FancyZones, AdvancedPaste). Look for a sibling folder or 1–2 levels up named like `<Product>*UnitTests` or `<Product>*UITests`.
|
||||
- Build the test project, wait for **exit**, then run only those tests via VS Test Explorer or `vstest.console.exe` with filters. Avoid `dotnet test` in this repo.
|
||||
- Add/adjust tests when changing behavior; if skipped, state why (e.g., comment-only, string rename).
|
||||
|
||||
Pull requests (expectations)
|
||||
- Atomic: one logical change; no drive‑by refactors.
|
||||
- Describe: problem / approach / risk / test evidence.
|
||||
- List: touched paths if not obvious.
|
||||
|
||||
When to ask for clarification
|
||||
- Ambiguous spec after scanning relevant docs (see below).
|
||||
- Cross-module impact (shared enum/struct) not clear.
|
||||
- Security / elevation / installer changes.
|
||||
|
||||
Logging (use existing stacks)
|
||||
- C++: `src/common/logger/**` (`Logger::info|warn|error|debug`). Keep hot paths quiet (hooks, tight loops).
|
||||
- C#: `ManagedCommon.Logger` (`LogInfo|LogWarning|LogError|LogDebug|LogTrace`). Some UIs use injected `ILogger` via `LoggerInstance.Logger`.
|
||||
|
||||
Docs to consult
|
||||
- `tools/build/BUILD-GUIDELINES.md`
|
||||
- `doc/devdocs/core/architecture.md`, `doc/devdocs/core/runner.md`, `doc/devdocs/core/settings/readme.md`, `doc/devdocs/modules/readme.md`
|
||||
|
||||
Done checklist (self review before finishing)
|
||||
- Build clean? Tests updated/passed? No unintended formatting? Any new dependency? Documented skips?
|
||||
@@ -1,38 +1,20 @@
|
||||
name: Manual Batch Issue Deduplication
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_numbers:
|
||||
description: "JSON array of issue numbers to deduplicate (e.g. [101,102,103])"
|
||||
required: true
|
||||
since:
|
||||
description: "Only compare against issues created after this date (ISO 8601, e.g. 2019-05-05T00:00:00Z)"
|
||||
required: false
|
||||
default: "2019-05-05T00:00:00Z"
|
||||
label_as_duplicate:
|
||||
description: "Apply duplicate label if duplicates are found (true/false)"
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
permissions:
|
||||
models: read
|
||||
issues: write
|
||||
workflow_dispatch: # Only runs when manually triggered
|
||||
|
||||
jobs:
|
||||
deduplicate:
|
||||
batch-deduplicate:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run GenAI Issue Deduplicator
|
||||
- name: Batch Deduplicate Issues
|
||||
uses: pelikhan/action-genai-issue-dedup@v0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_issue: ${{ matrix.issue }}
|
||||
label_as_duplicate: ${{ github.event.inputs.label_as_duplicate }}
|
||||
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
label-duplicate: "potential duplicate"
|
||||
comment-duplicate: true
|
||||
close-duplicate: false
|
||||
batch-size: 100
|
||||
since: '2019-05-05T00:00:00Z' # Process issues dating back to 2019
|
||||
duplicate-comment-template: "This issue appears to be a duplicate of #{duplicate_issue_number}."
|
||||
# Add other action-specific inputs if needed
|
||||
|
||||
43
.vscode/launch.json
vendored
43
.vscode/launch.json
vendored
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"inputs": [
|
||||
{
|
||||
"id": "arch",
|
||||
"type": "pickString",
|
||||
"description": "Select target architecture",
|
||||
"options": ["x64", "arm64"],
|
||||
"default": "x64"
|
||||
}
|
||||
],
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run native executable (no build)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\${input:arch}\\Debug\\PowerToys.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"environment": [],
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "C/C++ Attach to PowerToys Process (native)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}",
|
||||
"symbolSearchPath": "${workspaceFolder}\\${input:arch}\\Debug;${workspaceFolder}\\Debug;${workspaceFolder}\\symbols"
|
||||
},
|
||||
{
|
||||
"name": "Run managed code (managed, no build, ARCH configurable)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\${input:arch}\\Debug\\WinUI3Apps\\PowerToys.Settings.exe",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
]
|
||||
}
|
||||
12
.vscode/mcp.json
vendored
12
.vscode/mcp.json
vendored
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"servers": {
|
||||
"powertoys-mcp": {
|
||||
"command": "D:/work/PowerToys/x64/Debug/PowerToys.MCPServer.exe",
|
||||
"transport": "stdio",
|
||||
"args": [],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,6 @@
|
||||
<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" />
|
||||
|
||||
@@ -262,6 +262,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
|
||||
src\common\utils\EventLocker.h = src\common\utils\EventLocker.h
|
||||
src\common\utils\EventWaiter.h = src\common\utils\EventWaiter.h
|
||||
src\common\utils\excluded_apps.h = src\common\utils\excluded_apps.h
|
||||
src\common\utils\shell_ext_registration.h = src\common\utils\shell_ext_registration.h
|
||||
src\common\utils\exec.h = src\common\utils\exec.h
|
||||
src\common\utils\game_mode.h = src\common\utils\game_mode.h
|
||||
src\common\utils\gpo.h = src\common\utils\gpo.h
|
||||
@@ -281,7 +282,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
|
||||
src\common\utils\registry.h = src\common\utils\registry.h
|
||||
src\common\utils\resources.h = src\common\utils\resources.h
|
||||
src\common\utils\serialized.h = src\common\utils\serialized.h
|
||||
src\common\utils\shell_ext_registration.h = src\common\utils\shell_ext_registration.h
|
||||
src\common\utils\string_utils.h = src\common\utils\string_utils.h
|
||||
src\common\utils\timeutil.h = src\common\utils\timeutil.h
|
||||
src\common\utils\UnhandledExceptionHandler.h = src\common\utils\UnhandledExceptionHandler.h
|
||||
@@ -793,10 +793,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1-76DF-42AC-985C-164CC2EE57A1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
|
||||
@@ -805,10 +801,6 @@ 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
|
||||
@@ -2703,22 +2695,6 @@ Global
|
||||
{61CBF221-9452-4934-B685-146285E080D7}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{61CBF221-9452-4934-B685-146285E080D7}.Release|x64.ActiveCfg = Release|x64
|
||||
{61CBF221-9452-4934-B685-146285E080D7}.Release|x64.Build.0 = Release|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.Build.0 = Debug|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.ActiveCfg = Release|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.Build.0 = Release|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.Build.0 = Debug|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.ActiveCfg = Release|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.Build.0 = Release|x64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -2735,6 +2711,30 @@ Global
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.ActiveCfg = Release|x64
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.Build.0 = Release|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.Build.0 = Debug|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.ActiveCfg = Release|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.Build.0 = Release|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.Build.0 = Debug|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.ActiveCfg = Release|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.Build.0 = Release|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.Build.0 = Debug|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.ActiveCfg = Release|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.Build.0 = Release|x64
|
||||
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -2887,14 +2887,6 @@ Global
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.Build.0 = Debug|x64
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.ActiveCfg = Release|x64
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Build.0 = Release|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -2927,14 +2919,6 @@ 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
|
||||
@@ -3210,10 +3194,10 @@ Global
|
||||
{9BC1C986-1E97-4D07-A7B1-CE226C239EFA} = {2F305555-C296-497E-AC20-5FA1B237996A}
|
||||
{99CA1509-FB73-456E-AFAF-AB89C017BD72} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704}
|
||||
{61CBF221-9452-4934-B685-146285E080D7} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704}
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE} = {C3081D9A-1586-441A-B5F4-ED815B3719C1}
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {2C318EC3-BA86-4372-B1BC-DB0F33C208B2}
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {68328142-5B31-4715-BCBB-7B6345EE0971}
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE} = {C3081D9A-1586-441A-B5F4-ED815B3719C1}
|
||||
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{14AFD976-B4D2-49D0-9E6C-AA93CC061B8A} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{9D3F3793-EFE3-4525-8782-238015DABA62} = {66E1534A-1587-42B2-912F-45C994D32904}
|
||||
@@ -3249,14 +3233,10 @@ Global
|
||||
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B}
|
||||
{66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1}
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{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}
|
||||
|
||||
@@ -18,8 +18,8 @@ You can build the entire solution from the command line, which is sometimes fast
|
||||
1. Open Developer Command Prompt for VS 2022
|
||||
2. Navigate to the repository root directory
|
||||
3. Run the following command(don't forget to set the correct platform):
|
||||
```pwsh
|
||||
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln /tl /p:NuGetInteractive="true"
|
||||
```
|
||||
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln
|
||||
```
|
||||
4. This process should complete in approximately 13-14 minutes for a full build
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
## Developing PowerToys with Visual Studio Code
|
||||
|
||||
This guide shows how to build, debug, and contribute to PowerToys using VS Code instead of (or alongside) full Visual Studio. It focuses on common inner‑loop tasks for C++, .NET, and mixed scenarios present in the solution.
|
||||
|
||||
> PowerToys is a large mixed C++ / C# / WinAppSDK solution. VS Code works well for incremental development and quick module iterations, but occasionally you may still prefer full Visual Studio for designer tooling or specialized diagnostics.
|
||||
|
||||
---
|
||||
VS Code extensions Needed:
|
||||
|
||||
| Area | Extension | Notes |
|
||||
|------|-----------|-------|
|
||||
| C++ | ms-vscode.cpptools | IntelliSense, debugging (cppvsdbg) |
|
||||
| C# | ms-dotnettools.csdevkit (or C#) | Language service / test explorer |
|
||||
|
||||
---
|
||||
|
||||
## Building in VS Code
|
||||
### Configure developer powershell for vs2022 for more convenient dev in vscode.
|
||||
1. Configure profile in in settings, entry: "terminal.integrated.profiles.windows"
|
||||
2. Add below config as entry:
|
||||
```json
|
||||
"Developer PowerShell for VS 2022": {
|
||||
// Configure based on your preference
|
||||
"path": "C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.5.2.0_arm64__8wekyb3d8bbwe\\pwsh.exe",
|
||||
"args": [
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"& {",
|
||||
"$orig = Get-Location;",
|
||||
// Configure based on your environment
|
||||
"& 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\Common7\\Tools\\Launch-VsDevShell.ps1';",
|
||||
"Set-Location $orig",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
```
|
||||
3. [Optional] Set Developer PowerShell for VS 2022 as your default profile, so that you can get a deep integration with vscode coding agent.
|
||||
|
||||
4. Now You can build with plain `msbuild` or configure tasks.json in below section
|
||||
Or reach out to "tools\build\BUILD-GUIDELINES.md"
|
||||
|
||||
### Sample plain msbuild command
|
||||
```powershell
|
||||
# Restore:
|
||||
msbuild powertoys.sln -t:restore -p:configuration=debug -p:platform=x64 -m
|
||||
|
||||
# Build powertoys sln
|
||||
msbuild powertoys.sln -p:configuration=debug -p:platform=x64 -m
|
||||
|
||||
# dotnet project
|
||||
msbuild src\settings-ui\Settings.UI\PowerToys.Settings.csproj -p:Platform=x64 -p:Configuration=Debug -m
|
||||
|
||||
# native project
|
||||
msbuild "src\modules\MouseUtils\FindMyMouse\FindMyMouse.vcxproj" -p:Configuration=Debug -p:Platform=x64 -m
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
### Existing launch configuration
|
||||
|
||||
The repo provides `.vscode/launch.json` with:
|
||||
|
||||
- `Run PowerToys.exe (no build)`: Launches the already-built executable at `x64/Debug/PowerToys.exe` using `cppvsdbg`.
|
||||
|
||||
Build first, then press F5. To switch configuration (Release / ARM64) either edit the path or create additional launch entries.
|
||||
|
||||
### Attaching to a running instance
|
||||
|
||||
If PowerToys is already running, you can attach to that process:
|
||||
|
||||
2. VS Code command palette: “C/C++: (Windows) Attach to Process”.
|
||||
3. Filter for `PowerToys.exe` / module-specific processes.
|
||||
|
||||
### Debugging managed components
|
||||
|
||||
Many modules have a managed component loaded into the PowerToys process. `cppvsdbg` can debug mixed mode, but if you need richer .NET inspection you can create a second configuration using `type: coreclr` and `processId` attachment after the native launch, or just attach separately:
|
||||
|
||||
Similar for attach to managed code.
|
||||
> Note: In arm64 machine, can only debug arm64 code.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run native executable (no build)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\x64\\Debug\\PowerToys.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"environment": [],
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "C/C++ Attach to PowerToys Process (native)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}",
|
||||
"symbolSearchPath": "${workspaceFolder}\\x64\\Debug;${workspaceFolder}\\Debug;${workspaceFolder}\\symbols"
|
||||
},
|
||||
{
|
||||
"name": "Run managed code (managed, no build)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\arm64\\Debug\\WinUI3Apps\\PowerToys.Settings.exe",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
---
|
||||
|
||||
## 6. Common tasks & tips
|
||||
|
||||
| Task | Command / Action | Notes |
|
||||
|------|------------------|-------|
|
||||
| Clean | `git clean -xdf` (careful) or `msbuild /t:Clean PowerToys.sln` | Deep clean removes packages & build outputs |
|
||||
| Rebuild single project | `msbuild path\to\proj.vcxproj /t:Rebuild -p:Platform=x64 -p:Configuration=Debug` | Faster than whole solution |
|
||||
| Generate installer (rare in inner loop) | See `tools\build\build-installer.ps1` | Usually not needed for local debug |
|
||||
| Resource conversion errors | Re-run restore + build | Triggers custom PowerShell targets |
|
||||
@@ -76,7 +76,6 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
|
||||
1. Windows 10 April 2018 Update (version 1803) or newer
|
||||
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer
|
||||
1. A local clone of the PowerToys repository
|
||||
1. Enable long paths in Windows (see [Enable Long Paths](https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation#enabling-long-paths-in-windows-10-version-1607-and-later) for details)
|
||||
|
||||
### Install Visual Studio dependencies
|
||||
|
||||
|
||||
@@ -73,5 +73,4 @@ Below are community created plugins that target a website or software. They are
|
||||
| [YubicoOauthOTP](https://github.com/dlnilsson/Community.PowerToys.Run.Plugin.YubicoOauthOTP) | [dlnilsson](https://github.com/dlnilsson) | Display generated codes from OATH accounts stored on the YubiKey in powerToys Run |
|
||||
| [Firefox Bookmark](https://github.com/8LWXpg/PowerToysRun-FirefoxBookmark) | [8LWXpg](https://github.com/8LWXpg) | Open bookmarks in Firefox based browser |
|
||||
| [Linear](https://github.com/vednig/powertoys-linear) | [vednig](https://github.com/vednig) | Create Linear Issues directly from Powertoys Run |
|
||||
| [PerplexitySearchShortcut](https://github.com/0x6f677548/PowerToys-Run-PerplexitySearchShortcut) | [0x6f677548](https://github.com/0x6f677548) | Search Perplexity |
|
||||
| [SpeedTest](https://github.com/ruslanlap/PowerToysRun-SpeedTest) | [ruslanlap](https://github.com/ruslanlap) | One-command internet speed tests with real-time results, modern UI, and shareable links. |
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#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; }
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#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; }
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public sealed class ModuleStatusResponse
|
||||
{
|
||||
public string ModuleName { get; set; } = string.Empty;
|
||||
|
||||
public string[] AvailableEndpoints { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#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; }
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ namespace Microsoft.PowerToys.UITest
|
||||
Workspaces,
|
||||
PowerRename,
|
||||
CommandPalette,
|
||||
ScreenRuler,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -105,7 +104,6 @@ namespace Microsoft.PowerToys.UITest
|
||||
[PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor"),
|
||||
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
|
||||
[PowerToysModule.CommandPalette] = new ModuleInfo("Microsoft.CmdPal.UI.exe", "PowerToys Command Palette", "WinUI3Apps\\CmdPal"),
|
||||
[PowerToysModule.ScreenRuler] = new ModuleInfo("PowerToys.MeasureToolUI.exe", "PowerToys.ScreenRuler", "WinUI3Apps"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@@ -95,7 +94,6 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
Task.Delay(1000).Wait();
|
||||
AddScreenShotsToTestResultsDirectory();
|
||||
AddLogFilesToTestResultsDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,92 +598,6 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies PowerToys log files to test results directory when test fails.
|
||||
/// Renames files to include the directory structure after \PowerToys.
|
||||
/// </summary>
|
||||
protected void AddLogFilesToTestResultsDirectory()
|
||||
{
|
||||
try
|
||||
{
|
||||
var localAppDataLow = Path.Combine(
|
||||
Environment.GetEnvironmentVariable("USERPROFILE") ?? string.Empty,
|
||||
"AppData",
|
||||
"LocalLow",
|
||||
"Microsoft",
|
||||
"PowerToys");
|
||||
|
||||
if (Directory.Exists(localAppDataLow))
|
||||
{
|
||||
CopyLogFilesFromDirectory(localAppDataLow, string.Empty);
|
||||
}
|
||||
|
||||
var localAppData = Path.Combine(
|
||||
Environment.GetEnvironmentVariable("LOCALAPPDATA") ?? string.Empty,
|
||||
"Microsoft",
|
||||
"PowerToys");
|
||||
|
||||
if (Directory.Exists(localAppData))
|
||||
{
|
||||
CopyLogFilesFromDirectory(localAppData, string.Empty);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Don't fail the test if log file copying fails
|
||||
Console.WriteLine($"Failed to copy log files: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively copies log files from a directory and renames them with directory structure.
|
||||
/// </summary>
|
||||
/// <param name="sourceDir">Source directory to copy from</param>
|
||||
/// <param name="relativePath">Relative path from PowerToys folder</param>
|
||||
private void CopyLogFilesFromDirectory(string sourceDir, string relativePath)
|
||||
{
|
||||
if (!Directory.Exists(sourceDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Process log files in current directory
|
||||
var logFiles = Directory.GetFiles(sourceDir, "*.log");
|
||||
foreach (var logFile in logFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileName = Path.GetFileName(logFile);
|
||||
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
var extension = Path.GetExtension(fileName);
|
||||
|
||||
// Create new filename with directory structure
|
||||
var directoryPart = string.IsNullOrEmpty(relativePath) ? string.Empty : relativePath.Replace("\\", "-") + "-";
|
||||
var newFileName = $"{directoryPart}{fileNameWithoutExt}{extension}";
|
||||
|
||||
// Copy file to test results directory with new name
|
||||
var testResultsDir = TestContext.TestResultsDirectory ?? Path.GetTempPath();
|
||||
var destinationPath = Path.Combine(testResultsDir, newFileName);
|
||||
|
||||
File.Copy(logFile, destinationPath, true);
|
||||
TestContext.AddResultFile(destinationPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to copy log file {logFile}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process subdirectories
|
||||
var subdirectories = Directory.GetDirectories(sourceDir);
|
||||
foreach (var subdir in subdirectories)
|
||||
{
|
||||
var dirName = Path.GetFileName(subdir);
|
||||
var newRelativePath = string.IsNullOrEmpty(relativePath) ? dirName : Path.Combine(relativePath, dirName);
|
||||
CopyLogFilesFromDirectory(subdir, newRelativePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restart scope exe.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.cs,**/*.cpp,**/*.c,**/*.h,**/*.hpp"
|
||||
---
|
||||
# Common – shared libraries guidance (concise)
|
||||
|
||||
Scope
|
||||
- Logging, IPC, settings, DPI, telemetry, utilities consumed by multiple modules.
|
||||
|
||||
Guidelines
|
||||
- Avoid breaking public headers/APIs; if changed, search & update all callers.
|
||||
- Coordinate ABI-impacting struct/class layout changes; keep binary compatibility.
|
||||
- Watch perf in hot paths (hooks, timers, serialization); avoid avoidable allocations.
|
||||
- Ask before adding third‑party deps or changing serialization formats.
|
||||
|
||||
Acceptance
|
||||
- No unintended ABI breaks, no noisy logs, new non-obvious symbols briefly commented.
|
||||
@@ -31,11 +31,11 @@
|
||||
<!-- The following sections assume that the machine we're building on is always x64. That means we won't be able to run/inspect arm64 executables, therefore we must always execute x64 generator. -->
|
||||
|
||||
<Target Name="PostBuildAction" AfterTargets="Build" Outputs="$(GeneratedDSCModule)" Condition="'$(Platform)'!='ARM64'">
|
||||
<Exec Command=""$(OutDir)$(AssemblyName).exe" "..\..\..\x64\$(Configuration)\WinUI3Apps\PowerToys.Settings.UI.Lib.dll" $(GeneratedDSCModule) $(GeneratedDSCManifest)" />
|
||||
<Exec Command=""$(OutDir)$(AssemblyName).exe" "$(SolutionDir)x64\$(Configuration)\WinUI3Apps\PowerToys.Settings.UI.Lib.dll" $(GeneratedDSCModule) $(GeneratedDSCManifest)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent" Condition="'$(Platform)'=='ARM64'">
|
||||
<Exec Command=""$(MSBuildToolsPath)\msbuild.exe" PowerToys.sln -p:Configuration="$(Configuration)" -p:Platform="x64" -verbosity:m -t:DSC\PowerToys_Settings_DSC_Schema_Generator" WorkingDirectory="..\..\..\" />
|
||||
<Exec Command=""$(MSBuildToolsPath)\msbuild.exe" PowerToys.sln -p:Configuration="$(Configuration)" -p:Platform="x64" -verbosity:m -t:DSC\PowerToys_Settings_DSC_Schema_Generator" WorkingDirectory="$(SolutionDir)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<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>
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.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}";
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
EXPORTS
|
||||
powertoy_create
|
||||
@@ -1,42 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,298 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
#include "pch.h"
|
||||
@@ -1,14 +0,0 @@
|
||||
#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>
|
||||
@@ -1,8 +0,0 @@
|
||||
#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>
|
||||
@@ -31,11 +31,7 @@ struct CommonState
|
||||
|
||||
Measurement::Unit units = Measurement::Unit::Pixel;
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4324)
|
||||
alignas(8) POINT cursorPosSystemSpace = {}; // updated atomically
|
||||
#pragma warning(pop)
|
||||
|
||||
POINT cursorPosSystemSpace = {}; // updated atomically
|
||||
std::atomic_bool closeOnOtherMonitors = false;
|
||||
|
||||
float GetPhysicalPx2MmRatio(HWND window) const
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="PowerToys.ScreenRuler"
|
||||
IsAlwaysOnTop="True"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
@@ -251,7 +250,6 @@
|
||||
<ToggleButton
|
||||
Name="btnBounds"
|
||||
x:Uid="BtnBounds"
|
||||
AutomationProperties.AutomationId="Button_Bounds"
|
||||
Click="BoundsTool_Click"
|
||||
Content=""
|
||||
KeyboardAcceleratorPlacementMode="Auto"
|
||||
@@ -269,7 +267,6 @@
|
||||
<ToggleButton
|
||||
Name="btnSpacing"
|
||||
x:Uid="BtnSpacing"
|
||||
AutomationProperties.AutomationId="Button_Spacing"
|
||||
Click="MeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -287,7 +284,6 @@
|
||||
<ToggleButton
|
||||
Name="btnHorizontalSpacing"
|
||||
x:Uid="BtnHorizontalSpacing"
|
||||
AutomationProperties.AutomationId="Button_SpacingHorizontal"
|
||||
Click="HorizontalMeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -308,7 +304,6 @@
|
||||
<ToggleButton
|
||||
Name="btnVerticalSpacing"
|
||||
x:Uid="BtnVerticalSpacing"
|
||||
AutomationProperties.AutomationId="Button_SpacingVertical"
|
||||
Click="VerticalMeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -329,7 +324,6 @@
|
||||
<AppBarSeparator />
|
||||
<Button
|
||||
x:Uid="BtnClosePanel"
|
||||
AutomationProperties.AutomationId="Button_Close"
|
||||
Click="ClosePanelTool_Click"
|
||||
Content=""
|
||||
Foreground="{StaticResource CloseButtonBackgroundPointerOver}">
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="d4d0f157-5c12-4390-9689-152b0c86a582"
|
||||
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="d4d0f157-5c12-4390-9689-152b0c86a582" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>ScreenRuler.UITests</DisplayName>
|
||||
<PublisherDisplayName>Microsoft</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="ScreenRuler.UITests"
|
||||
Description="ScreenRuler.UITests"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
@@ -1,23 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>PowerToys.ScreenRuler.UITests</RootNamespace>
|
||||
<AssemblyName>ScreenRuler.UITests</AssemblyName>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
|
||||
<!-- This is a UI test, so don't run as part of MSBuild -->
|
||||
<RunVSTest>false</RunVSTest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\ScreenRuler.UITests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestBounds : UITestBase
|
||||
{
|
||||
public TestBounds()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.BoundsTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerBoundsTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "bounds test");
|
||||
TestHelper.PerformBoundsToolTest(this);
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,466 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
public static class TestHelper
|
||||
{
|
||||
private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
|
||||
|
||||
// Button automation names from Resources.resw
|
||||
public const string BoundsButtonId = "Button_Bounds";
|
||||
public const string SpacingButtonName = "Button_Spacing";
|
||||
public const string HorizontalSpacingButtonName = "Button_SpacingHorizontal";
|
||||
public const string VerticalSpacingButtonName = "Button_SpacingVertical";
|
||||
public const string CloseButtonId = "Button_Close";
|
||||
|
||||
/// <summary>
|
||||
/// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
/// <param name="testName">Name of the test for assertions</param>
|
||||
/// <returns>The activation keys for the test</returns>
|
||||
public static Key[] InitializeTest(UITestBase testBase, string testName)
|
||||
{
|
||||
LaunchFromSetting(testBase);
|
||||
|
||||
var toggleSwitch = SetScreenRulerToggle(testBase, enable: true);
|
||||
Assert.IsTrue(
|
||||
toggleSwitch.IsOn,
|
||||
$"Screen Ruler toggle switch should be ON for {testName}");
|
||||
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
Assert.IsNotNull(activationKeys, "Should be able to read activation shortcut");
|
||||
Assert.IsTrue(activationKeys.Length > 0, "Activation shortcut should contain at least one key");
|
||||
|
||||
return activationKeys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs common test cleanup: close ScreenRuler UI
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
public static void CleanupTest(UITestBase testBase)
|
||||
{
|
||||
CloseScreenRulerUI(testBase);
|
||||
|
||||
// Ensure we're attached to settings after cleanup
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors - this is just cleanup
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the Screen Ruler (Measure Tool) settings page
|
||||
/// </summary>
|
||||
public static void LaunchFromSetting(UITestBase testBase)
|
||||
{
|
||||
var screenRulers = testBase.Session.FindAll<NavigationViewItem>(By.AccessibilityId("ScreenRulerNavItem"));
|
||||
|
||||
if (screenRulers.Count == 0)
|
||||
{
|
||||
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("SystemToolsNavItem"), 5000).Click(msPostAction: 500);
|
||||
}
|
||||
|
||||
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("ScreenRulerNavItem"), 5000).Click(msPostAction: 500);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Screen Ruler toggle switch to the specified state
|
||||
/// </summary>
|
||||
public static ToggleSwitch SetScreenRulerToggle(UITestBase testBase, bool enable)
|
||||
{
|
||||
var toggleSwitch = testBase.Session.Find<ToggleSwitch>(By.AccessibilityId("Toggle_ScreenRuler"), 5000);
|
||||
|
||||
if (toggleSwitch.IsOn != enable)
|
||||
{
|
||||
toggleSwitch.Click(msPreAction: 1000, msPostAction: 2000);
|
||||
}
|
||||
|
||||
if (toggleSwitch.IsOn != enable)
|
||||
{
|
||||
testBase.Session.SendKey(Key.Space, msPreAction: 0, msPostAction: 2000);
|
||||
}
|
||||
|
||||
return toggleSwitch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Screen Ruler toggle and verify its state
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
/// <param name="enable">True to enable, false to disable</param>
|
||||
/// <param name="testName">Name of the test for assertion messages</param>
|
||||
public static void SetAndVerifyScreenRulerToggle(UITestBase testBase, bool enable, string testName)
|
||||
{
|
||||
var toggleSwitch = SetScreenRulerToggle(testBase, enable);
|
||||
Assert.AreEqual(
|
||||
enable,
|
||||
toggleSwitch.IsOn,
|
||||
$"Screen Ruler toggle switch should be {(enable ? "ON" : "OFF")} for {testName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the current activation shortcut from the ShortcutControl
|
||||
/// </summary>
|
||||
public static Key[] ReadActivationShortcut(UITestBase testBase)
|
||||
{
|
||||
var shortcutCard = testBase.Session.Find<Element>(By.AccessibilityId("Shortcut_ScreenRuler"), 5000);
|
||||
var shortcutButton = shortcutCard.Find<Element>(By.AccessibilityId("EditButton"), 5000);
|
||||
return ParseShortcutText(shortcutButton.HelpText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse shortcut text like "Win + Ctrl + Shift + M" into Key array
|
||||
/// </summary>
|
||||
private static Key[] ParseShortcutText(string shortcutText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(shortcutText))
|
||||
{
|
||||
return new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.M };
|
||||
}
|
||||
|
||||
var keys = new List<Key>();
|
||||
var parts = shortcutText.Split(ShortcutSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var cleanPart = part.Trim().ToLowerInvariant();
|
||||
var key = cleanPart switch
|
||||
{
|
||||
"win" or "windows" => Key.Win,
|
||||
"ctrl" or "control" => Key.Ctrl,
|
||||
"shift" => Key.Shift,
|
||||
"alt" => Key.Alt,
|
||||
_ when cleanPart.Length == 1 && char.IsLetter(cleanPart[0]) &&
|
||||
cleanPart[0] >= 'a' && cleanPart[0] <= 'z' =>
|
||||
(Key)Enum.Parse(typeof(Key), cleanPart.ToUpperInvariant()),
|
||||
_ => (Key?)null,
|
||||
};
|
||||
|
||||
if (key.HasValue)
|
||||
{
|
||||
keys.Add(key.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return keys.Count > 0 ? keys.ToArray() : new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.M };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if ScreenRulerUI window is open
|
||||
/// </summary>
|
||||
public static bool IsScreenRulerUIOpen(UITestBase testBase) => testBase.IsWindowOpen("PowerToys.ScreenRuler");
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to reach the specified state within the timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUIState(UITestBase testBase, bool shouldBeOpen, int timeoutMs = 5000, int pollingIntervalMs = 100)
|
||||
{
|
||||
var endTime = DateTime.Now.AddMilliseconds(timeoutMs);
|
||||
|
||||
while (DateTime.Now < endTime)
|
||||
{
|
||||
if (IsScreenRulerUIOpen(testBase) == shouldBeOpen)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Task.Delay(pollingIntervalMs).Wait();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to appear within the specified timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUI(UITestBase testBase, int timeoutMs = 5000) =>
|
||||
WaitForScreenRulerUIState(testBase, shouldBeOpen: true, timeoutMs);
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to disappear within the specified timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUIToDisappear(UITestBase testBase, int timeoutMs = 5000) =>
|
||||
WaitForScreenRulerUIState(testBase, shouldBeOpen: false, timeoutMs);
|
||||
|
||||
/// <summary>
|
||||
/// Close ScreenRulerUI if it's open
|
||||
/// </summary>
|
||||
public static void CloseScreenRulerUI(UITestBase testBase)
|
||||
{
|
||||
if (IsScreenRulerUIOpen(testBase))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Attach to ScreenRuler window before trying to find and click close button
|
||||
testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var closeButton = testBase.Session.Find<Element>(By.AccessibilityId(CloseButtonId), 15000, true);
|
||||
closeButton?.Click();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't find the close button, ignore - the window might have closed already
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Attach back to settings after closing
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific ScreenRulerUI button by its automation name
|
||||
/// </summary>
|
||||
public static Element? GetScreenRulerButton(UITestBase testBase, string buttonName, int timeoutMs = 1000)
|
||||
{
|
||||
return testBase.Session.Find<Element>(By.AccessibilityId(buttonName), timeoutMs, true);
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
// Attach to ScreenRuler window before trying to find buttons
|
||||
testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
return testBase.Session.Find<Element>(By.AccessibilityId(buttonName), timeoutMs, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Attach back to settings if needed for further operations
|
||||
// This ensures we don't break the test flow
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors - the calling code will handle as needed
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the clipboard content using STA thread
|
||||
/// </summary>
|
||||
public static void ClearClipboard()
|
||||
{
|
||||
ExecuteInSTAThread(() => System.Windows.Forms.Clipboard.Clear());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get text content from clipboard using STA thread
|
||||
/// </summary>
|
||||
public static string GetClipboardText()
|
||||
{
|
||||
string result = string.Empty;
|
||||
ExecuteInSTAThread(() =>
|
||||
{
|
||||
if (System.Windows.Forms.Clipboard.ContainsText())
|
||||
{
|
||||
result = System.Windows.Forms.Clipboard.GetText();
|
||||
}
|
||||
});
|
||||
return result ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute an action in an STA thread with error handling
|
||||
/// </summary>
|
||||
private static void ExecuteInSTAThread(Action action)
|
||||
{
|
||||
try
|
||||
{
|
||||
var staThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore clipboard errors
|
||||
}
|
||||
});
|
||||
|
||||
staThread.SetApartmentState(ApartmentState.STA);
|
||||
staThread.Start();
|
||||
staThread.Join(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore clipboard errors
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate clipboard content contains valid spacing measurement for the specified type
|
||||
/// </summary>
|
||||
public static bool ValidateSpacingClipboardContent(string clipboardText, string spacingType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipboardText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return spacingType switch
|
||||
{
|
||||
"Spacing" => Regex.IsMatch(clipboardText, @"\d+\s*[<5B>x×]\s*\d+"),
|
||||
"Horizontal Spacing" or "Vertical Spacing" => Regex.IsMatch(clipboardText, @"^\d+$"),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a complete spacing tool test operation
|
||||
/// </summary>
|
||||
public static void PerformSpacingToolTest(UITestBase testBase, string buttonId, string testName)
|
||||
{
|
||||
ClearClipboard();
|
||||
|
||||
// Launch ScreenRuler UI
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
testBase.SendKeys(activationKeys);
|
||||
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUI(testBase, 2000),
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut for {testName}: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Attach to ScreenRuler window and click spacing button
|
||||
// testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var spacingButton = testBase.Session.Find<Element>(By.AccessibilityId(buttonId), 15000, true);
|
||||
Assert.IsNotNull(spacingButton, $"{testName} button should be found");
|
||||
|
||||
spacingButton!.Click();
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Perform measurement action (stay attached to ScreenRuler for this)
|
||||
PerformMeasurementAction(testBase);
|
||||
|
||||
// Validate results
|
||||
ValidateClipboardResults(testName);
|
||||
|
||||
// Cleanup - this will handle session attachment properly
|
||||
CloseScreenRulerUI(testBase);
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUIToDisappear(testBase, 2000),
|
||||
$"{testName}: ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a bounds tool test operation
|
||||
/// </summary>
|
||||
public static void PerformBoundsToolTest(UITestBase testBase)
|
||||
{
|
||||
ClearClipboard();
|
||||
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
testBase.SendKeys(activationKeys);
|
||||
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUI(testBase, 2000),
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Attach to ScreenRuler window and click bounds button
|
||||
// testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var boundsButton = testBase.Session.Find<Element>(By.AccessibilityId(BoundsButtonId), 15000, true);
|
||||
Assert.IsNotNull(boundsButton, "Bounds button should be found");
|
||||
|
||||
boundsButton.Click();
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Perform drag operation to create 100x100 box (stay attached to ScreenRuler)
|
||||
var currentPos = testBase.GetMousePosition();
|
||||
int startX = currentPos.Item1;
|
||||
int startY = currentPos.Item2 + 200;
|
||||
|
||||
testBase.MoveMouseTo(startX, startY);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
// Drag operation
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftDown);
|
||||
Task.Delay(100).Wait();
|
||||
|
||||
testBase.MoveMouseTo(startX + 99, startY + 99);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftUp);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Dismiss selection
|
||||
testBase.Session.PerformMouseAction(MouseActionType.RightClick);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Validate results
|
||||
string clipboardText = GetClipboardText();
|
||||
Assert.IsFalse(string.IsNullOrEmpty(clipboardText), "Clipboard should contain measurement data");
|
||||
Assert.IsTrue(
|
||||
clipboardText.Contains("100 × 100") || clipboardText.Contains("100 x 100"),
|
||||
$"Clipboard should contain '100 x 100', but contained: '{clipboardText}'");
|
||||
|
||||
// Cleanup - this will handle session attachment properly
|
||||
CloseScreenRulerUI(testBase);
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUIToDisappear(testBase, 2000),
|
||||
"ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a measurement action (move mouse and click)
|
||||
/// </summary>
|
||||
private static void PerformMeasurementAction(UITestBase testBase)
|
||||
{
|
||||
var currentPos = testBase.GetMousePosition();
|
||||
int startX = currentPos.Item1;
|
||||
int startY = currentPos.Item2 + 200;
|
||||
|
||||
testBase.MoveMouseTo(startX, startY);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftClick);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.RightClick);
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate clipboard results for spacing tests
|
||||
/// </summary>
|
||||
private static void ValidateClipboardResults(string testName)
|
||||
{
|
||||
string clipboardText = GetClipboardText();
|
||||
Assert.IsFalse(string.IsNullOrEmpty(clipboardText), $"{testName}: Clipboard should contain measurement data");
|
||||
|
||||
bool containsValidPattern = ValidateSpacingClipboardContent(clipboardText, testName);
|
||||
Assert.IsTrue(
|
||||
containsValidPattern,
|
||||
$"{testName}: Clipboard should contain valid spacing measurement, but contained: '{clipboardText}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestShortcutActivation : UITestBase
|
||||
{
|
||||
public TestShortcutActivation()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.ShortcutActivation")]
|
||||
[TestCategory("Activation")]
|
||||
public void TestScreenRulerShortcutActivation()
|
||||
{
|
||||
var activationKeys = TestHelper.InitializeTest(this, "activation test");
|
||||
|
||||
// Test 1: Press the activation shortcut and verify the toolbar appears
|
||||
SendKeys(activationKeys);
|
||||
bool screenRulerAppeared = TestHelper.WaitForScreenRulerUI(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerAppeared,
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 2: Press the activation shortcut again and verify the toolbar disappears
|
||||
SendKeys(activationKeys);
|
||||
bool screenRulerDisappeared = TestHelper.WaitForScreenRulerUIToDisappear(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerDisappeared,
|
||||
$"ScreenRulerUI should disappear after pressing activation shortcut again: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 3: Disable Screen Ruler and verify that the activation shortcut no longer activates the utility
|
||||
// Ensure we're attached to settings UI before toggling
|
||||
Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
TestHelper.SetAndVerifyScreenRulerToggle(this, enable: false, "disabled state test");
|
||||
|
||||
// Try to activate with shortcut while disabled
|
||||
SendKeys(activationKeys);
|
||||
Task.Delay(1000).Wait();
|
||||
Assert.IsFalse(
|
||||
TestHelper.IsScreenRulerUIOpen(this),
|
||||
"ScreenRulerUI should not appear when Screen Ruler is disabled");
|
||||
|
||||
// Test 4: Enable Screen Ruler and press the activation shortcut and verify the toolbar appears
|
||||
// Ensure we're attached to settings UI before toggling
|
||||
Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
TestHelper.SetAndVerifyScreenRulerToggle(this, enable: true, "re-enabled state test");
|
||||
|
||||
SendKeys(activationKeys);
|
||||
screenRulerAppeared = TestHelper.WaitForScreenRulerUI(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerAppeared,
|
||||
$"ScreenRulerUI should appear after re-enabling and pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 5: Verify the utility can be closed via the cleanup method
|
||||
TestHelper.CloseScreenRulerUI(this);
|
||||
bool screenRulerClosed = TestHelper.WaitForScreenRulerUIToDisappear(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerClosed,
|
||||
"ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacing : UITestBase
|
||||
{
|
||||
public TestSpacing()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.SpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.SpacingButtonName, "Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacingHorizontal : UITestBase
|
||||
{
|
||||
public TestSpacingHorizontal()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.HorizontalSpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerHorizontalSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "horizontal spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.HorizontalSpacingButtonName, "Horizontal Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacingVertical : UITestBase
|
||||
{
|
||||
public TestSpacingVertical()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.VerticalSpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerVerticalSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "vertical spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.VerticalSpacingButtonName, "Vertical Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="ScreenRuler.UITests.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -49,23 +49,23 @@ namespace MouseUtils.UITests
|
||||
settings.BackgroundColor = "000000";
|
||||
settings.SpotlightColor = "FFFFFF";
|
||||
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
Assert.IsNotNull(foundCustom);
|
||||
|
||||
if (CheckAnimationEnable(ref foundCustom))
|
||||
{
|
||||
foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
}
|
||||
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
|
||||
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
|
||||
Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
|
||||
SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
|
||||
|
||||
var excludedApps = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseExcludedApps));
|
||||
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
|
||||
if (excludedApps != null)
|
||||
{
|
||||
excludedApps.Click();
|
||||
@@ -115,23 +115,23 @@ namespace MouseUtils.UITests
|
||||
settings.BackgroundColor = "FF0000";
|
||||
settings.SpotlightColor = "0000FF";
|
||||
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
Assert.IsNotNull(foundCustom);
|
||||
|
||||
if (CheckAnimationEnable(ref foundCustom))
|
||||
{
|
||||
foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
}
|
||||
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
|
||||
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
|
||||
Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
|
||||
SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
|
||||
|
||||
var excludedApps = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseExcludedApps));
|
||||
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
|
||||
if (excludedApps != null)
|
||||
{
|
||||
excludedApps.Click();
|
||||
@@ -170,27 +170,27 @@ namespace MouseUtils.UITests
|
||||
settings.AnimationDuration = "0";
|
||||
settings.BackgroundColor = "000000";
|
||||
settings.SpotlightColor = "FFFFFF";
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
|
||||
Assert.IsNotNull(foundCustom);
|
||||
|
||||
if (CheckAnimationEnable(ref foundCustom))
|
||||
{
|
||||
foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
}
|
||||
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
|
||||
Assert.IsNotNull(foundCustom);
|
||||
SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
|
||||
|
||||
var excludedApps = foundCustom.Find<Group>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseExcludedApps));
|
||||
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
|
||||
if (excludedApps != null)
|
||||
{
|
||||
excludedApps.Click();
|
||||
@@ -212,14 +212,14 @@ namespace MouseUtils.UITests
|
||||
VerifySpotlightAppears(ref settings);
|
||||
|
||||
// [Test Case] Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
Task.Delay(1000).Wait();
|
||||
ActivateSpotlight(ref settings);
|
||||
|
||||
VerifySpotlightDisappears(ref settings);
|
||||
|
||||
// [Test Case] Press Left Ctrl twice and verify the overlay appears
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
Task.Delay(2000).Wait();
|
||||
ActivateSpotlight(ref settings);
|
||||
VerifySpotlightAppears(ref settings);
|
||||
@@ -240,27 +240,27 @@ namespace MouseUtils.UITests
|
||||
settings.AnimationDuration = "0";
|
||||
settings.BackgroundColor = "000000";
|
||||
settings.SpotlightColor = "FFFFFF";
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
|
||||
Assert.IsNotNull(foundCustom);
|
||||
|
||||
if (CheckAnimationEnable(ref foundCustom))
|
||||
{
|
||||
foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
}
|
||||
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
|
||||
Assert.IsNotNull(foundCustom);
|
||||
SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
|
||||
|
||||
var excludedApps = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseExcludedApps));
|
||||
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
|
||||
if (excludedApps != null)
|
||||
{
|
||||
excludedApps.Click();
|
||||
@@ -282,14 +282,14 @@ namespace MouseUtils.UITests
|
||||
VerifySpotlightAppears(ref settings);
|
||||
|
||||
// [Test Case] Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
Task.Delay(1000).Wait();
|
||||
ActivateSpotlight(ref settings);
|
||||
|
||||
VerifySpotlightDisappears(ref settings);
|
||||
|
||||
// [Test Case] Press Left Ctrl twice and verify the overlay appears
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
Task.Delay(2000).Wait();
|
||||
ActivateSpotlight(ref settings);
|
||||
VerifySpotlightAppears(ref settings);
|
||||
@@ -310,17 +310,17 @@ namespace MouseUtils.UITests
|
||||
settings.AnimationDuration = "0";
|
||||
settings.BackgroundColor = "000000";
|
||||
settings.SpotlightColor = "FFFFFF";
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
|
||||
// foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
// foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
|
||||
Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
|
||||
|
||||
// SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
|
||||
var excludedApps = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseExcludedApps));
|
||||
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
|
||||
if (excludedApps != null)
|
||||
{
|
||||
excludedApps.Click();
|
||||
@@ -340,7 +340,7 @@ namespace MouseUtils.UITests
|
||||
// VerifySpotlightSettings(ref settings);
|
||||
|
||||
// [Test Case] Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
Task.Delay(2000).Wait();
|
||||
Session.SendKey(Key.LCtrl, 0, 0);
|
||||
Task.Delay(100).Wait();
|
||||
@@ -382,6 +382,9 @@ namespace MouseUtils.UITests
|
||||
|
||||
var colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
|
||||
Assert.AreEqual("#" + settings.BackgroundColor, colorBackground);
|
||||
|
||||
var colorBackground2 = this.GetPixelColorString(location.Item1 + radius + 100, location.Item2 + radius + 100);
|
||||
Assert.AreEqual("#" + settings.BackgroundColor, colorBackground2);
|
||||
}
|
||||
|
||||
private void ActivateSpotlight(ref FindMyMouseSettings settings)
|
||||
@@ -424,7 +427,7 @@ namespace MouseUtils.UITests
|
||||
private void SetFindMyMouseActivationMethod(ref Custom? foundCustom, string method)
|
||||
{
|
||||
Assert.IsNotNull(foundCustom);
|
||||
var groupActivation = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseActivationMethod));
|
||||
var groupActivation = foundCustom.Find<TextBlock>("Activation method");
|
||||
if (groupActivation != null)
|
||||
{
|
||||
groupActivation.Click();
|
||||
@@ -453,17 +456,17 @@ namespace MouseUtils.UITests
|
||||
private void SetFindMyMouseAppearanceBehavior(ref Custom foundCustom, ref FindMyMouseSettings settings)
|
||||
{
|
||||
Assert.IsNotNull(foundCustom);
|
||||
var groupAppearanceBehavior = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseAppearanceBehavior));
|
||||
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
// groupAppearanceBehavior.Click();
|
||||
if (foundCustom.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseOverlayOpacity)).Count == 0)
|
||||
if (foundCustom.FindAll<Slider>("Overlay opacity (%)").Count == 0)
|
||||
{
|
||||
groupAppearanceBehavior.Click();
|
||||
}
|
||||
|
||||
// Set the BackGround color
|
||||
var backgroundColor = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseBackgroundColor));
|
||||
var backgroundColor = foundCustom.Find<Group>("Background color");
|
||||
Assert.IsNotNull(backgroundColor);
|
||||
|
||||
var button = backgroundColor.Find<Button>(By.XPath(".//Button"));
|
||||
@@ -502,7 +505,7 @@ namespace MouseUtils.UITests
|
||||
button.Click();
|
||||
|
||||
// Set the Spotlight color
|
||||
var spotlightColor = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseSpotlightColor));
|
||||
var spotlightColor = foundCustom.Find<Group>("Spotlight color");
|
||||
Assert.IsNotNull(spotlightColor);
|
||||
|
||||
var spotlightColorButton = spotlightColor.Find<Button>(By.XPath(".//Button"));
|
||||
@@ -542,7 +545,7 @@ namespace MouseUtils.UITests
|
||||
spotlightColorButton.Click(false, 500, 1500);
|
||||
|
||||
// Set the overlay opacity to overlayOpacity%
|
||||
var overlayOpacitySlider = foundCustom.Find<Slider>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseOverlayOpacity));
|
||||
var overlayOpacitySlider = foundCustom.Find<Slider>("Overlay opacity (%)");
|
||||
Assert.IsNotNull(overlayOpacitySlider);
|
||||
Assert.IsNotNull(settings.OverlayOpacity);
|
||||
int overlayOpacityValue = int.Parse(settings.OverlayOpacity, CultureInfo.InvariantCulture);
|
||||
@@ -551,7 +554,7 @@ namespace MouseUtils.UITests
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Set the Fade Initial zoom to 0
|
||||
var spotlightInitialZoomSlider = foundCustom.Find<Slider>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseSpotlightZoom));
|
||||
var spotlightInitialZoomSlider = foundCustom.Find<Slider>("Spotlight initial zoom");
|
||||
Assert.IsNotNull(spotlightInitialZoomSlider);
|
||||
Task.Delay(1000).Wait();
|
||||
spotlightInitialZoomSlider.QuickSetValue(int.Parse(settings.InitialZoom, CultureInfo.InvariantCulture));
|
||||
@@ -559,8 +562,7 @@ namespace MouseUtils.UITests
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
//// Change the edit value
|
||||
var spotlightRadius = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseSpotlightRadius));
|
||||
var spotlightRadiusEdit = spotlightRadius.Find<TextBox>(By.AccessibilityId("InputBox"));
|
||||
var spotlightRadiusEdit = foundCustom.Find<TextBox>("Spotlight radius (px) Minimum5");
|
||||
Assert.IsNotNull(spotlightRadiusEdit);
|
||||
Task.Delay(1000).Wait();
|
||||
spotlightRadiusEdit.SetText(settings.Radius);
|
||||
@@ -568,12 +570,11 @@ namespace MouseUtils.UITests
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Set the duration to 0 ms
|
||||
var spotlightAnimationDuration = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseAnimationDuration));
|
||||
var spotlightAnimationDurationEdit = spotlightAnimationDuration.Find<TextBox>(By.AccessibilityId("InputBox"));
|
||||
Assert.IsNotNull(spotlightAnimationDurationEdit);
|
||||
var spotlightAnimationDuration = foundCustom.Find<TextBox>("Animation duration (ms) Minimum0");
|
||||
Assert.IsNotNull(spotlightAnimationDuration);
|
||||
Task.Delay(1000).Wait();
|
||||
spotlightAnimationDurationEdit.SetText(settings.AnimationDuration);
|
||||
Assert.AreEqual(settings.AnimationDuration, spotlightAnimationDurationEdit.Text);
|
||||
spotlightAnimationDuration.SetText(settings.AnimationDuration);
|
||||
Assert.AreEqual(settings.AnimationDuration, spotlightAnimationDuration.Text);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// groupAppearanceBehavior.Click();
|
||||
@@ -621,19 +622,19 @@ namespace MouseUtils.UITests
|
||||
this.Session.SetMainWindowSize(WindowSize.Large);
|
||||
|
||||
// Goto Hosts File Editor setting page
|
||||
if (this.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Count == 0)
|
||||
if (this.FindAll<NavigationViewItem>("Mouse utilities", 10000).Count == 0)
|
||||
{
|
||||
// Expand Advanced list-group if needed
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.InputOutputNavItem)).Click();
|
||||
this.Find<NavigationViewItem>("Input / Output").Click();
|
||||
}
|
||||
|
||||
if (reload)
|
||||
{
|
||||
this.Find<NavigationViewItem>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.KeyboardManagerNavItem)).Click();
|
||||
this.Find<NavigationViewItem>("Keyboard Manager").Click();
|
||||
}
|
||||
|
||||
Task.Delay(1000).Wait();
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Click();
|
||||
this.Find<NavigationViewItem>("Mouse utilities").Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -24,10 +24,10 @@ namespace MouseUtils.UITests
|
||||
public void TestEnableMouseHighlighter()
|
||||
{
|
||||
LaunchFromSetting();
|
||||
var foundCustom0 = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom0 = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom0 != null)
|
||||
{
|
||||
foundCustom0.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -42,11 +42,11 @@ namespace MouseUtils.UITests
|
||||
settings.FadeDelay = "0";
|
||||
settings.FadeDuration = "90";
|
||||
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighter));
|
||||
var foundCustom = this.Find<Custom>("Mouse Highlighter");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
|
||||
|
||||
var xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
|
||||
@@ -54,7 +54,7 @@ namespace MouseUtils.UITests
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
|
||||
// Change the shortcut key for MouseHighlighter
|
||||
// [TestCase]Change activation shortcut and test it
|
||||
@@ -107,7 +107,7 @@ namespace MouseUtils.UITests
|
||||
VerifyMouseHighlighterNotAppears(ref settings, "rightClick");
|
||||
|
||||
// [Test Case] Disable Mouse Highlighter and verify that the module is not activated when you press the activation shortcut.
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
|
||||
xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1 - 100, xy.Item2);
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace MouseUtils.UITests
|
||||
|
||||
// [Test Case] With left mouse button pressed, drag the mouse and verify the highlight is dragged with the pointer.
|
||||
// [Test Case] With right mouse button pressed, drag the mouse and verify the highlight is dragged with the pointer.
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1 - 100, xy.Item2);
|
||||
|
||||
@@ -143,10 +143,10 @@ namespace MouseUtils.UITests
|
||||
public void TestMouseHighlighterDifferentSettings()
|
||||
{
|
||||
LaunchFromSetting();
|
||||
var foundCustom0 = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom0 = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom0 != null)
|
||||
{
|
||||
foundCustom0.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -161,11 +161,11 @@ namespace MouseUtils.UITests
|
||||
settings.FadeDelay = "0";
|
||||
settings.FadeDuration = "90";
|
||||
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighter));
|
||||
var foundCustom = this.Find<Custom>("Mouse Highlighter");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
|
||||
|
||||
var xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
|
||||
@@ -173,7 +173,7 @@ namespace MouseUtils.UITests
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
|
||||
// Change the shortcut key for MouseHighlighter
|
||||
// [TestCase] Test the different settings and verify they apply - Change activation shortcut and test it
|
||||
@@ -387,7 +387,7 @@ namespace MouseUtils.UITests
|
||||
private void SetColor(ref Custom foundCustom, string colorName = "Primary button highlight color", string colorValue = "000000", string opacity = "0")
|
||||
{
|
||||
Assert.IsNotNull(foundCustom);
|
||||
var groupAppearanceBehavior = foundCustom.Find<Group>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterAppearanceBehavior));
|
||||
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
if (foundCustom.FindAll<TextBox>("Fade duration (ms) Minimum0").Count == 0)
|
||||
@@ -439,7 +439,7 @@ namespace MouseUtils.UITests
|
||||
private void SetMouseHighlighterAppearanceBehavior(ref Custom foundCustom, ref MouseHighlighterSettings settings)
|
||||
{
|
||||
Assert.IsNotNull(foundCustom);
|
||||
var groupAppearanceBehavior = foundCustom.Find<Group>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseHighlighterAppearanceBehavior));
|
||||
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
// groupAppearanceBehavior.Click();
|
||||
@@ -477,7 +477,7 @@ namespace MouseUtils.UITests
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("MouseHighlighter Appearance & behavior group not found.");
|
||||
Assert.Fail("Appearance & behavior group not found.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,14 +485,14 @@ namespace MouseUtils.UITests
|
||||
{
|
||||
this.Session.SetMainWindowSize(WindowSize.Large);
|
||||
|
||||
// Goto Mouse utilities setting page
|
||||
if (this.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Count == 0)
|
||||
// Goto Hosts File Editor setting page
|
||||
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
|
||||
{
|
||||
// Expand Input / Output list-group if needed
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.InputOutputNavItem)).Click();
|
||||
// Expand Advanced list-group if needed
|
||||
this.Find<NavigationViewItem>("Input / Output").Click();
|
||||
}
|
||||
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Click();
|
||||
this.Find<NavigationViewItem>("Mouse utilities").Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -29,11 +29,11 @@ namespace MouseUtils.UITests
|
||||
public void TestEnableMouseJump2()
|
||||
{
|
||||
LaunchFromSetting();
|
||||
var foundCustom0 = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom0 = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom0 != null)
|
||||
{
|
||||
foundCustom0.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom0.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -45,10 +45,10 @@ namespace MouseUtils.UITests
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
}
|
||||
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseJump));
|
||||
var foundCustom = this.Find<Custom>("Mouse Jump");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseJumpToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(true);
|
||||
|
||||
var xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
|
||||
@@ -89,7 +89,7 @@ namespace MouseUtils.UITests
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// [TestCase] Enable Mouse Jump. Then - Disable Mouse Jump and verify that the module is not activated when you press the activation shortcut.
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseJumpToggle)).Toggle(false);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(false);
|
||||
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY - 300, 500, 1000);
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.Z);
|
||||
Task.Delay(500).Wait();
|
||||
@@ -108,11 +108,11 @@ namespace MouseUtils.UITests
|
||||
public void TestEnableMouseJump3()
|
||||
{
|
||||
LaunchFromSetting();
|
||||
var foundCustom0 = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouse));
|
||||
var foundCustom0 = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom0 != null)
|
||||
{
|
||||
foundCustom0.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(true);
|
||||
foundCustom0.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseToggle)).Toggle(false);
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -124,10 +124,10 @@ namespace MouseUtils.UITests
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
}
|
||||
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseJump));
|
||||
var foundCustom = this.Find<Custom>("Mouse Jump");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseJumpToggle)).Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(true);
|
||||
|
||||
var xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
|
||||
@@ -215,23 +215,23 @@ namespace MouseUtils.UITests
|
||||
Session.SetMainWindowSize(WindowSize.Large);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Goto Mouse utilities setting page
|
||||
if (this.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Count == 0)
|
||||
// Goto Hosts File Editor setting page
|
||||
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
|
||||
{
|
||||
// Expand Input / Output list-group if needed
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.InputOutputNavItem)).Click();
|
||||
// Expand Advanced list-group if needed
|
||||
this.Find<NavigationViewItem>("Input / Output").ClickCenter();
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
|
||||
// Goto Mouse utilities setting page
|
||||
if (this.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Count == 0)
|
||||
// Goto Hosts File Editor setting page
|
||||
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
|
||||
{
|
||||
RestartScopeExe();
|
||||
Session.SetMainWindowSize(WindowSize.Large);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Expand Input / Output list-group if needed
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.InputOutputNavItem)).Click();
|
||||
// Expand Advanced list-group if needed
|
||||
this.Find<NavigationViewItem>("Input / Output").ClickCenter();
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ namespace MouseUtils.UITests
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Click();
|
||||
this.Find<NavigationViewItem>("Mouse utilities").Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ namespace MouseUtils.UITests
|
||||
private void SetColor(ref Custom foundCustom, string colorName, string colorValue = "000000")
|
||||
{
|
||||
Assert.IsNotNull(foundCustom);
|
||||
var groupAppearanceBehavior = foundCustom.Find<Group>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MousePointerCrosshairsAppearanceBehavior));
|
||||
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
// Set primary button highlight color
|
||||
@@ -277,7 +277,7 @@ namespace MouseUtils.UITests
|
||||
private void SetMousePointerCrosshairsAppearanceBehavior(ref Custom foundCustom, ref MousePointerCrosshairsSettings settings)
|
||||
{
|
||||
Assert.IsNotNull(foundCustom);
|
||||
var groupAppearanceBehavior = foundCustom.Find<Group>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MousePointerCrosshairsAppearanceBehavior));
|
||||
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
// groupAppearanceBehavior.Click();
|
||||
@@ -337,7 +337,7 @@ namespace MouseUtils.UITests
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("MousePointerCrosshairs Appearance & behavior group not found.");
|
||||
Assert.Fail("Appearance & behavior group not found.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,16 +371,8 @@ namespace MouseUtils.UITests
|
||||
|
||||
public Custom? FindMouseUtilElement(MouseUtilsSettings.MouseUtils element)
|
||||
{
|
||||
string accessibilityId = element switch
|
||||
{
|
||||
MouseUtilsSettings.MouseUtils.FindMyMouse => MouseUtilsSettings.AccessibilityIds.FindMyMouse,
|
||||
MouseUtilsSettings.MouseUtils.MouseHighlighter => MouseUtilsSettings.AccessibilityIds.MouseHighlighter,
|
||||
MouseUtilsSettings.MouseUtils.MousePointerCrosshairs => MouseUtilsSettings.AccessibilityIds.MousePointerCrosshairs,
|
||||
MouseUtilsSettings.MouseUtils.MouseJump => MouseUtilsSettings.AccessibilityIds.MouseJump,
|
||||
_ => throw new ArgumentException($"Unknown MouseUtils element: {element}"),
|
||||
};
|
||||
|
||||
var foundCustom = this.Find<Custom>(By.AccessibilityId(accessibilityId));
|
||||
var elementName = MouseUtilsSettings.GetMouseUtilUIName(element);
|
||||
var foundCustom = this.Find<Custom>(elementName);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
if (foundCustom != null)
|
||||
@@ -389,7 +381,7 @@ namespace MouseUtils.UITests
|
||||
}
|
||||
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
foundCustom = this.Find<Custom>(By.AccessibilityId(accessibilityId));
|
||||
foundCustom = this.Find<Custom>(elementName);
|
||||
}
|
||||
|
||||
return foundCustom;
|
||||
@@ -399,14 +391,14 @@ namespace MouseUtils.UITests
|
||||
{
|
||||
Session.SetMainWindowSize(WindowSize.Large);
|
||||
|
||||
// Goto Mouse utilities setting page
|
||||
if (this.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Count == 0)
|
||||
// Goto Hosts File Editor setting page
|
||||
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
|
||||
{
|
||||
// Expand Input / Output list-group if needed
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.InputOutputNavItem)).Click();
|
||||
// Expand Advanced list-group if needed
|
||||
this.Find<NavigationViewItem>("Input / Output").Click();
|
||||
}
|
||||
|
||||
this.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.MouseUtilitiesNavItem)).Click();
|
||||
this.Find<NavigationViewItem>("Mouse utilities").Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,48 +11,6 @@ namespace MouseUtils.UITests
|
||||
{
|
||||
public class MouseUtilsSettings
|
||||
{
|
||||
// Accessibility ID constants
|
||||
public static class AccessibilityIds
|
||||
{
|
||||
// Mouse Utils module IDs
|
||||
public const string FindMyMouse = "MouseUtils_FindMyMouseTestId";
|
||||
public const string MouseHighlighter = "MouseUtils_MouseHighlighterTestId";
|
||||
public const string MousePointerCrosshairs = "MouseUtils_MousePointerCrosshairsTestId";
|
||||
public const string MouseJump = "MouseUtils_MouseJumpTestId";
|
||||
|
||||
// ToggleSwitch IDs
|
||||
public const string FindMyMouseToggle = "MouseUtils_FindMyMouseToggleId";
|
||||
public const string MouseHighlighterToggle = "MouseUtils_MouseHighlighterToggleId";
|
||||
public const string MousePointerCrosshairsToggle = "MouseUtils_MousePointerCrosshairsToggleId";
|
||||
public const string MouseJumpToggle = "MouseUtils_MouseJumpToggleId";
|
||||
|
||||
// Find My Mouse UI Element IDs
|
||||
public const string FindMyMouseActivationMethod = "MouseUtils_FindMyMouseActivationMethodId";
|
||||
public const string FindMyMouseAppearanceBehavior = "MouseUtils_FindMyMouseAppearanceBehaviorId";
|
||||
public const string FindMyMouseExcludedApps = "MouseUtils_FindMyMouseExcludedAppsId";
|
||||
public const string FindMyMouseBackgroundColor = "MouseUtils_FindMyMouseBackgroundColorId";
|
||||
public const string FindMyMouseSpotlightColor = "MouseUtils_FindMyMouseSpotlightColorId";
|
||||
public const string FindMyMouseOverlayOpacity = "MouseUtils_FindMyMouseOverlayOpacityId";
|
||||
public const string FindMyMouseSpotlightZoom = "MouseUtils_FindMyMouseSpotlightZoomId";
|
||||
public const string FindMyMouseSpotlightRadius = "MouseUtils_FindMyMouseSpotlightRadiusId";
|
||||
public const string FindMyMouseAnimationDuration = "MouseUtils_FindMyMouseAnimationDurationId";
|
||||
|
||||
// Mouse Highlighter UI Element IDs
|
||||
public const string MouseHighlighterActivationShortcut = "MouseUtils_MouseHighlighterActivationShortcutId";
|
||||
public const string MouseHighlighterAppearanceBehavior = "MouseUtils_MouseHighlighterAppearanceBehaviorId";
|
||||
|
||||
// Mouse Pointer Crosshairs UI Element IDs
|
||||
public const string MousePointerCrosshairsAppearanceBehavior = "MouseUtils_MousePointerCrosshairsAppearanceBehaviorId";
|
||||
|
||||
// Mouse Jump UI Element IDs
|
||||
public const string MouseJumpActivationShortcut = "MouseUtils_MouseJumpActivationShortcutId";
|
||||
|
||||
// Navigation IDs
|
||||
public const string InputOutputNavItem = "InputOutputNavItem";
|
||||
public const string MouseUtilitiesNavItem = "MouseUtilitiesNavItem";
|
||||
public const string KeyboardManagerNavItem = "KeyboardManagerNavItem";
|
||||
}
|
||||
|
||||
// Mouse Utils Modules
|
||||
public enum MouseUtils
|
||||
{
|
||||
|
||||
@@ -3525,10 +3525,6 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
}
|
||||
if( destFile == nullptr ) {
|
||||
|
||||
if (stream) {
|
||||
stream.Close();
|
||||
stream = nullptr;
|
||||
}
|
||||
co_await file.DeleteAsync();
|
||||
}
|
||||
else {
|
||||
@@ -3548,10 +3544,6 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
}
|
||||
else {
|
||||
|
||||
if (stream) {
|
||||
stream.Close();
|
||||
stream = nullptr;
|
||||
}
|
||||
co_await file.DeleteAsync();
|
||||
g_RecordingSession = nullptr;
|
||||
}
|
||||
@@ -4024,10 +4016,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
// Now copy crop or copy+save
|
||||
if( LOWORD( wParam ) == SNIP_SAVE_HOTKEY )
|
||||
{
|
||||
// Hide cursor for screen capture
|
||||
ShowCursor(false);
|
||||
SendMessage( hWnd, WM_COMMAND, IDC_SAVE_CROP, ( zoomed ? 0 : SHALLOW_ZOOM ) );
|
||||
ShowCursor(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -4059,6 +4048,12 @@ LRESULT APIENTRY MainWndProc(
|
||||
OutputDebug( L"Exiting liveDraw after snip\n" );
|
||||
SendMessage( hWnd, WM_KEYDOWN, VK_ESCAPE, 0 );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set wparam to 1 to exit without animation
|
||||
OutputDebug(L"Exiting zoom after snip\n" );
|
||||
SendMessage( hWnd, WM_HOTKEY, ZOOM_HOTKEY, SHALLOW_DESTROY );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -5783,26 +5778,17 @@ LRESULT APIENTRY MainWndProc(
|
||||
|
||||
if( !g_DrawingShape ) {
|
||||
|
||||
// If the point has changed, draw a line to it
|
||||
if (prevPt.x != LOWORD(lParam) || prevPt.y != HIWORD(lParam)) {
|
||||
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
|
||||
if ((GetWindowLong(g_hWndMain, GWL_EXSTYLE) & WS_EX_LAYERED) == 0)
|
||||
{
|
||||
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
||||
}
|
||||
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
|
||||
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
|
||||
Gdiplus::GraphicsPath path;
|
||||
pen.SetLineJoin(Gdiplus::LineJoinRound);
|
||||
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
|
||||
dstGraphics.DrawPath(&pen, &path);
|
||||
}
|
||||
// Draw a dot at the current point, if the point hasn't changed
|
||||
else {
|
||||
MoveToEx(hdcScreenCompat, prevPt.x, prevPt.y, NULL);
|
||||
LineTo(hdcScreenCompat, LOWORD(lParam), HIWORD(lParam));
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
|
||||
if( ( GetWindowLong( g_hWndMain, GWL_EXSTYLE ) & WS_EX_LAYERED ) == 0 )
|
||||
{
|
||||
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
||||
}
|
||||
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
|
||||
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
|
||||
Gdiplus::GraphicsPath path;
|
||||
pen.SetLineJoin(Gdiplus::LineJoinRound);
|
||||
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
|
||||
dstGraphics.DrawPath(&pen, &path);
|
||||
|
||||
prevPt.x = LOWORD( lParam );
|
||||
prevPt.y = HIWORD( lParam );
|
||||
|
||||
@@ -1,103 +1,102 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
|
||||
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
<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>
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="System.Reactive" />
|
||||
<PackageReference Include="System.Runtime.Caching" />
|
||||
</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>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using 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}";
|
||||
}
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ 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;
|
||||
@@ -18,9 +17,6 @@ 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;
|
||||
@@ -60,26 +56,9 @@ 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();
|
||||
}
|
||||
@@ -144,19 +123,6 @@ 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...");
|
||||
@@ -164,10 +130,6 @@ 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();
|
||||
@@ -212,15 +174,6 @@ 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(
|
||||
@@ -230,94 +183,6 @@ 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());
|
||||
@@ -535,22 +400,16 @@ 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);
|
||||
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}");
|
||||
// Close the message window that we used for the tray.
|
||||
Bridge.SendMessage(TrayHelper.WindowHandle, Native.Constants.WM_CLOSE, 0, 0);
|
||||
|
||||
Bridge.DestroyWindow(TrayHelper.WindowHandle);
|
||||
}
|
||||
|
||||
Bridge.PostQuitMessage(exitCode);
|
||||
@@ -641,275 +500,6 @@ 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>
|
||||
@@ -943,100 +533,6 @@ 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);
|
||||
|
||||
@@ -10,7 +10,6 @@ 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,
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,19 +306,6 @@ 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();
|
||||
@@ -468,7 +455,6 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#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
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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; }
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#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
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,6 @@ 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;
|
||||
@@ -36,22 +33,12 @@ 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;
|
||||
|
||||
@@ -84,11 +71,7 @@ namespace Awake
|
||||
}
|
||||
|
||||
await TrayHelper.InitializeTray(TrayHelper.DefaultAwakeIcon, Core.Constants.FullAppName);
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() =>
|
||||
{
|
||||
_httpServer?.Dispose();
|
||||
LockMutex?.ReleaseMutex();
|
||||
});
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() => LockMutex?.ReleaseMutex());
|
||||
AppDomain.CurrentDomain.UnhandledException += AwakeUnhandledExceptionCatcher;
|
||||
|
||||
if (!instantiated)
|
||||
@@ -113,12 +96,6 @@ 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!
|
||||
@@ -168,50 +145,6 @@ 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 _))
|
||||
@@ -242,16 +175,6 @@ 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,
|
||||
@@ -260,56 +183,10 @@ namespace Awake
|
||||
pidOption,
|
||||
expireAtOption,
|
||||
parentPidOption,
|
||||
httpServerOption,
|
||||
httpPortOption,
|
||||
activityOption,
|
||||
activityCpuOption,
|
||||
activityMemOption,
|
||||
activityNetOption,
|
||||
activitySampleOption,
|
||||
activityTimeoutOption,
|
||||
];
|
||||
|
||||
rootCommand.Description = Core.Constants.AppName;
|
||||
|
||||
// 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);
|
||||
});
|
||||
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
|
||||
|
||||
return rootCommand.InvokeAsync(args).Result;
|
||||
}
|
||||
@@ -334,13 +211,12 @@ 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, bool enableHttpServer, int httpPort, bool activityMode, uint actCpu, uint actMem, uint actNet, uint actSample, uint actTimeout)
|
||||
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt, bool useParentPid)
|
||||
{
|
||||
if (pid == 0 && !useParentPid)
|
||||
{
|
||||
@@ -359,35 +235,6 @@ 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();
|
||||
@@ -438,11 +285,6 @@ 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
|
||||
@@ -500,33 +342,6 @@ 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()
|
||||
@@ -546,7 +361,6 @@ namespace Awake
|
||||
SetupFileSystemWatcher(settingsPath);
|
||||
InitializeSettings();
|
||||
ProcessSettings();
|
||||
InitializeUsageTracking(); // after initial settings load
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -592,7 +406,6 @@ namespace Awake
|
||||
{
|
||||
Logger.LogInfo("Detected a settings file change. Updating configuration...");
|
||||
ProcessSettings();
|
||||
InitializeUsageTracking(); // re-evaluate usage tracking on config change
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -635,22 +448,6 @@ 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.");
|
||||
|
||||
@@ -213,15 +213,6 @@ 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>
|
||||
@@ -348,15 +339,6 @@ 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>
|
||||
|
||||
@@ -146,10 +146,6 @@
|
||||
<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>
|
||||
@@ -201,9 +197,6 @@
|
||||
<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>
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<#
|
||||
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 dependency‑light 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 }
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
<#
|
||||
Minimal ROT test script for the runtime‑registered 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 self‑contained; 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) }
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Commands;
|
||||
|
||||
public sealed partial class ConfirmableCommand : InvokableCommand
|
||||
{
|
||||
private readonly IInvokableCommand? _command;
|
||||
|
||||
public Func<bool>? IsConfirmationRequired { get; init; }
|
||||
|
||||
public required string ConfirmationTitle { get; init; }
|
||||
|
||||
public required string ConfirmationMessage { get; init; }
|
||||
|
||||
public required IInvokableCommand Command
|
||||
{
|
||||
get => _command!;
|
||||
init
|
||||
{
|
||||
if (_command is INotifyPropChanged oldNotifier)
|
||||
{
|
||||
oldNotifier.PropChanged -= InnerCommand_PropChanged;
|
||||
}
|
||||
|
||||
_command = value;
|
||||
|
||||
if (_command is INotifyPropChanged notifier)
|
||||
{
|
||||
notifier.PropChanged += InnerCommand_PropChanged;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Name));
|
||||
OnPropertyChanged(nameof(Id));
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get => (_command as Command)?.Name ?? base.Name;
|
||||
set
|
||||
{
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Name = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Name = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string Id
|
||||
{
|
||||
get => (_command as Command)?.Id ?? base.Id;
|
||||
set
|
||||
{
|
||||
var previous = Id;
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Id = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Id = value;
|
||||
}
|
||||
|
||||
if (previous != Id)
|
||||
{
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override IconInfo Icon
|
||||
{
|
||||
get => (_command as Command)?.Icon ?? base.Icon;
|
||||
set
|
||||
{
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Icon = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Icon = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConfirmableCommand()
|
||||
{
|
||||
// Allow init-only construction
|
||||
}
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public ConfirmableCommand(IInvokableCommand command, string confirmationTitle, string confirmationMessage, Func<bool>? isConfirmationRequired = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(confirmationMessage);
|
||||
ArgumentNullException.ThrowIfNull(confirmationMessage);
|
||||
|
||||
IsConfirmationRequired = isConfirmationRequired;
|
||||
ConfirmationTitle = confirmationTitle;
|
||||
ConfirmationMessage = confirmationMessage;
|
||||
Command = command;
|
||||
}
|
||||
|
||||
private void InnerCommand_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
var property = args.PropertyName;
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Name))
|
||||
{
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Id))
|
||||
{
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Icon))
|
||||
{
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
var showConfirmationDialog = IsConfirmationRequired?.Invoke() ?? true;
|
||||
if (showConfirmationDialog)
|
||||
{
|
||||
return CommandResult.Confirm(new ConfirmationArgs
|
||||
{
|
||||
Title = ConfirmationTitle,
|
||||
Description = ConfirmationMessage,
|
||||
PrimaryCommand = Command,
|
||||
IsPrimaryCommandCritical = true,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return Command.Invoke(this) ?? CommandResult.Dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known key chords used in the Command Palette and extensions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Assigned key chords should not conflict with system or application shortcuts.
|
||||
/// However, the key chords in this class are not guaranteed to be unique and may conflict
|
||||
/// with each other, especially when commands appear together in the same menu.
|
||||
/// </remarks>
|
||||
public static class WellKnownKeyChords
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for opening the file location. Shortcut: Ctrl+Shift+E.
|
||||
/// </summary>
|
||||
public static KeyChord OpenFileLocation { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.E);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for copying the file path. Shortcut: Ctrl+Shift+C.
|
||||
/// </summary>
|
||||
public static KeyChord CopyFilePath { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.C);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for opening the current location in a console. Shortcut: Ctrl+Shift+R.
|
||||
/// </summary>
|
||||
public static KeyChord OpenInConsole { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.R);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for running the selected item as administrator. Shortcut: Ctrl+Shift+Enter.
|
||||
/// </summary>
|
||||
public static KeyChord RunAsAdministrator { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.Enter);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for running the selected item as a different user. Shortcut: Ctrl+Shift+U.
|
||||
/// </summary>
|
||||
public static KeyChord RunAsDifferentUser { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.U);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for toggling the pin state. Shortcut: Ctrl+P.
|
||||
/// </summary>
|
||||
public static KeyChord TogglePin { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: (int)VirtualKey.P);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public class GalleryGridPropertiesViewModel : IGridPropertiesViewModel
|
||||
{
|
||||
private readonly ExtensionObject<IGalleryGridLayout> _model;
|
||||
|
||||
public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout)
|
||||
{
|
||||
_model = new(galleryGridLayout);
|
||||
}
|
||||
|
||||
public bool ShowTitle { get; set; }
|
||||
|
||||
public bool ShowSubtitle { get; set; }
|
||||
|
||||
public void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
ShowTitle = model.ShowTitle;
|
||||
ShowSubtitle = model.ShowSubtitle;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public interface IGridPropertiesViewModel
|
||||
{
|
||||
void InitializeProperties();
|
||||
}
|
||||
@@ -45,10 +45,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
(!_isFetching) &&
|
||||
IsLoading == false;
|
||||
|
||||
public bool IsGridView { get; private set; }
|
||||
|
||||
public IGridPropertiesViewModel? GridProperties { get; private set; }
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public bool ShowDetails { get; private set; }
|
||||
@@ -520,13 +516,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
_isDynamic = model is IDynamicListPage;
|
||||
|
||||
IsGridView = model.GridProperties is not null;
|
||||
UpdateProperty(nameof(IsGridView));
|
||||
|
||||
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
|
||||
GridProperties?.InitializeProperties();
|
||||
UpdateProperty(nameof(GridProperties));
|
||||
|
||||
ShowDetails = model.ShowDetails;
|
||||
UpdateProperty(nameof(ShowDetails));
|
||||
|
||||
@@ -548,27 +537,9 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
model.ItemsChanged += Model_ItemsChanged;
|
||||
}
|
||||
|
||||
private IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
|
||||
{
|
||||
if (gridProperties is IMediumGridLayout mediumGridLayout)
|
||||
{
|
||||
return new MediumGridPropertiesViewModel(mediumGridLayout);
|
||||
}
|
||||
else if (gridProperties is IGalleryGridLayout galleryGridLayout)
|
||||
{
|
||||
return new GalleryGridPropertiesViewModel(galleryGridLayout);
|
||||
}
|
||||
else if (gridProperties is ISmallGridLayout smallGridLayout)
|
||||
{
|
||||
return new SmallGridPropertiesViewModel(smallGridLayout);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void LoadMoreIfNeeded()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
var model = this._model.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
@@ -612,7 +583,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
{
|
||||
base.FetchProperty(propertyName);
|
||||
|
||||
var model = _model.Unsafe;
|
||||
var model = this._model.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
@@ -620,20 +591,14 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(GridProperties):
|
||||
IsGridView = model.GridProperties is not null;
|
||||
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
|
||||
GridProperties?.InitializeProperties();
|
||||
UpdateProperty(nameof(IsGridView));
|
||||
break;
|
||||
case nameof(ShowDetails):
|
||||
ShowDetails = model.ShowDetails;
|
||||
this.ShowDetails = model.ShowDetails;
|
||||
break;
|
||||
case nameof(PlaceholderText):
|
||||
_modelPlaceholderText = model.PlaceholderText;
|
||||
this._modelPlaceholderText = model.PlaceholderText;
|
||||
break;
|
||||
case nameof(SearchText):
|
||||
SearchText = model.SearchText;
|
||||
this.SearchText = model.SearchText;
|
||||
break;
|
||||
case nameof(EmptyContent):
|
||||
EmptyContent = new(new(model.EmptyContent), PageContext);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public class MediumGridPropertiesViewModel : IGridPropertiesViewModel
|
||||
{
|
||||
private readonly ExtensionObject<IMediumGridLayout> _model;
|
||||
|
||||
public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout)
|
||||
{
|
||||
_model = new(mediumGridLayout);
|
||||
}
|
||||
|
||||
public bool ShowTitle { get; set; }
|
||||
|
||||
public void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
ShowTitle = model.ShowTitle;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public class SmallGridPropertiesViewModel : IGridPropertiesViewModel
|
||||
{
|
||||
private readonly ExtensionObject<ISmallGridLayout> _model;
|
||||
|
||||
public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout)
|
||||
{
|
||||
_model = new(smallGridLayout);
|
||||
}
|
||||
|
||||
public void InitializeProperties()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,6 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public bool IgnoreShortcutWhenFullscreen { get; set; }
|
||||
|
||||
public bool AllowExternalReload { get; set; }
|
||||
|
||||
public Dictionary<string, ProviderSettings> ProviderSettings { get; set; } = [];
|
||||
|
||||
public Dictionary<string, CommandAlias> Aliases { get; set; } = [];
|
||||
|
||||
@@ -38,16 +38,6 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public bool AllowExternalReload
|
||||
{
|
||||
get => _settings.AllowExternalReload;
|
||||
set
|
||||
{
|
||||
_settings.AllowExternalReload = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAppDetails
|
||||
{
|
||||
get => _settings.ShowAppDetails;
|
||||
|
||||
@@ -85,7 +85,7 @@ public partial class App : Application
|
||||
AppWindow = new MainWindow();
|
||||
|
||||
var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
((MainWindow)AppWindow).HandleLaunchNonUI(activatedEventArgs);
|
||||
((MainWindow)AppWindow).HandleLaunch(activatedEventArgs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -134,15 +134,6 @@ public sealed partial class CommandBar : UserControl,
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets focus to the "More" button after closing the context menu,
|
||||
/// keeping keyboard navigation intuitive.
|
||||
/// </summary>
|
||||
public void FocusMoreCommandsButton()
|
||||
{
|
||||
MoreCommandsButton?.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||
{
|
||||
// We need to wait until our flyout is opened to try and toss focus
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
Background="Transparent"
|
||||
PreviewKeyDown="UserControl_PreviewKeyDown"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
@@ -116,24 +115,6 @@ public sealed partial class ContextMenu : UserControl,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles Escape to close the context menu and return focus to the "More" button.
|
||||
/// </summary>
|
||||
private void UserControl_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
// Close the context menu (if not already handled)
|
||||
WeakReferenceMessenger.Default.Send(new CloseContextMenuMessage());
|
||||
|
||||
// Find the parent CommandBar and set focus to MoreCommandsButton
|
||||
var parent = this.FindParent<CommandBar>();
|
||||
parent?.FocusMoreCommandsButton();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var prop = e.PropertyName;
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Deferred;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -57,8 +55,6 @@ public partial class IconBox : ContentControl
|
||||
{
|
||||
TabFocusNavigation = KeyboardNavigationMode.Once;
|
||||
IsTabStop = false;
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center;
|
||||
VerticalContentAlignment = VerticalAlignment.Center;
|
||||
}
|
||||
|
||||
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
@@ -79,8 +75,6 @@ public partial class IconBox : ContentControl
|
||||
IconSourceElement elem = new()
|
||||
{
|
||||
IconSource = fontIco,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
@this.Content = elem;
|
||||
break;
|
||||
@@ -104,20 +98,14 @@ public partial class IconBox : ContentControl
|
||||
else
|
||||
{
|
||||
// TODO GH #239 switch back when using the new MD text block
|
||||
// Switching back to EnqueueAsync has broken icons in tags (they don't show)
|
||||
// _ = @this._queue.EnqueueAsync(() =>
|
||||
@this._queue.TryEnqueue(async void () =>
|
||||
@this._queue.TryEnqueue(new(async () =>
|
||||
{
|
||||
try
|
||||
var requestedTheme = @this.ActualTheme;
|
||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||
|
||||
if (@this.SourceRequested is not null)
|
||||
{
|
||||
if (@this.SourceRequested is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestedTheme = @this.ActualTheme;
|
||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||
|
||||
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
|
||||
|
||||
// After the await:
|
||||
@@ -142,35 +130,37 @@ public partial class IconBox : ContentControl
|
||||
// So, if the icon we get back was a font icon,
|
||||
// and the glyph for that icon is NOT in the range of
|
||||
// Segoe icons, then let's give the icon some extra space
|
||||
var iconData = eventArgs.Key switch
|
||||
{
|
||||
IconDataViewModel key => key,
|
||||
IconInfoViewModel info => requestedTheme == ElementTheme.Light ? info.Light : info.Dark,
|
||||
_ => null,
|
||||
};
|
||||
@this.Padding = new Thickness(0);
|
||||
|
||||
if (iconData?.Icon is not null && @this.Source is FontIconSource)
|
||||
IconDataViewModel? iconData = null;
|
||||
if (eventArgs.Key is IconDataViewModel)
|
||||
{
|
||||
var iconSize =
|
||||
!double.IsNaN(@this.Width) ? @this.Width :
|
||||
!double.IsNaN(@this.Height) ? @this.Height :
|
||||
@this.ActualWidth > 0 ? @this.ActualWidth :
|
||||
@this.ActualHeight;
|
||||
|
||||
@this.Padding = new Thickness(Math.Round(iconSize * -0.2));
|
||||
iconData = eventArgs.Key as IconDataViewModel;
|
||||
}
|
||||
else
|
||||
else if (eventArgs.Key is IconInfoViewModel info)
|
||||
{
|
||||
@this.Padding = default;
|
||||
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark;
|
||||
}
|
||||
|
||||
if (iconData is not null &&
|
||||
@this.Source is FontIconSource)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2)
|
||||
{
|
||||
var ch = iconData.Icon[0];
|
||||
|
||||
// The range of MDL2 Icons isn't explicitly defined, but
|
||||
// we're using this based off the table on:
|
||||
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
|
||||
var isMDL2Icon = ch is >= '\uE700' and <= '\uF8FF';
|
||||
if (!isMDL2Icon)
|
||||
{
|
||||
@this.Padding = new Thickness(-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Exception from TryEnqueue bypasses the global error handler,
|
||||
// and crashes the app.
|
||||
Logger.LogError("Failed to set icon", ex);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public IGridPropertiesViewModel? GridProperties { get; set; }
|
||||
|
||||
public DataTemplate? Small { get; set; }
|
||||
|
||||
public DataTemplate? Medium { get; set; }
|
||||
|
||||
public DataTemplate? Gallery { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
||||
{
|
||||
DataTemplate? dataTemplate = Medium;
|
||||
|
||||
if (GridProperties is SmallGridPropertiesViewModel)
|
||||
{
|
||||
dataTemplate = Small;
|
||||
}
|
||||
else if (GridProperties is MediumGridPropertiesViewModel)
|
||||
{
|
||||
dataTemplate = Medium;
|
||||
}
|
||||
else if (GridProperties is GalleryGridPropertiesViewModel)
|
||||
{
|
||||
dataTemplate = Gallery;
|
||||
}
|
||||
|
||||
return dataTemplate;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
x:Class="Microsoft.CmdPal.UI.ListPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
@@ -12,11 +11,8 @@
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
x:Name="PageRoot"
|
||||
Background="Transparent"
|
||||
DataContext="{x:Bind ViewModel, Mode=OneWay}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
@@ -27,7 +23,6 @@
|
||||
IsSourceGrouped="True"
|
||||
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />-->
|
||||
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
@@ -44,14 +39,6 @@
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<cmdpalUI:GridItemTemplateSelector
|
||||
x:Key="GridItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties}"
|
||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||
|
||||
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
|
||||
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
@@ -115,145 +102,6 @@
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Grid item templates for visual grid representation -->
|
||||
<DataTemplate x:Key="SmallGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<StackPanel
|
||||
Width="60"
|
||||
Height="60"
|
||||
Padding="8,16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Title}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="8"
|
||||
Orientation="Vertical"
|
||||
ToolTipService.ToolTip="{x:Bind Title}">
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="GridIconBorder"
|
||||
Width="28"
|
||||
Height="28"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<StackPanel
|
||||
Width="100"
|
||||
Height="100"
|
||||
Padding="8,16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Title}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="8"
|
||||
Orientation="Vertical"
|
||||
ToolTipService.ToolTip="{x:Bind Title}">
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="GridIconBorder"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
MaxHeight="40"
|
||||
Margin="0,8,0,4"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="12"
|
||||
Text="{x:Bind Title}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{Binding ElementName=PageRoot, Path=DataContext.GridProperties.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<StackPanel
|
||||
Width="160"
|
||||
Margin="4"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Title}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="4"
|
||||
Orientation="Vertical"
|
||||
ToolTipService.ToolTip="{x:Bind Title}">
|
||||
|
||||
<Grid
|
||||
Width="160"
|
||||
Height="160"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Viewbox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
Stretch="UniformToFill"
|
||||
StretchDirection="Both">
|
||||
<cpcontrols:IconBox
|
||||
CornerRadius="4"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Padding="4" Orientation="Vertical">
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
MaxWidth="152"
|
||||
MaxHeight="40"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
Text="{x:Bind Title}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
x:Name="SubTitleTextBlock"
|
||||
MaxWidth="152"
|
||||
MaxHeight="40"
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="11"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Text="{x:Bind Subtitle}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
@@ -262,49 +110,34 @@
|
||||
TargetType="x:Boolean"
|
||||
Value="{x:Bind ViewModel.ShowEmptyContent, Mode=OneWay}">
|
||||
<controls:Case Value="False">
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
TargetType="x:Boolean"
|
||||
Value="{x:Bind ViewModel.IsGridView, Mode=OneWay}">
|
||||
<controls:Case Value="False">
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
Padding="0,2,0,0"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</controls:Case>
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="8"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
DoubleTapped="Items_DoubleTapped"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<GridView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</GridView.ItemContainerTransitions>
|
||||
</GridView>
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
Padding="0,2,0,0"
|
||||
ContextCanceled="ItemsList_OnContextCanceled"
|
||||
ContextRequested="ItemsList_OnContextRequested"
|
||||
DoubleTapped="ItemsList_DoubleTapped"
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ItemsList_ItemClick"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
SelectionChanged="ItemsList_SelectionChanged">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
<!--<ListView.GroupStyle>
|
||||
<GroupStyle HidesIfEmpty="True">
|
||||
<GroupStyle.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding Key, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
</GroupStyle.HeaderTemplate>
|
||||
</GroupStyle>
|
||||
</ListView.GroupStyle>-->
|
||||
</ListView>
|
||||
</controls:Case>
|
||||
<controls:Case Value="True">
|
||||
<StackPanel
|
||||
@@ -337,5 +170,6 @@
|
||||
</StackPanel>
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -13,7 +13,6 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
@@ -39,21 +38,13 @@ public sealed partial class ListPage : Page,
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged));
|
||||
|
||||
private ListViewBase ItemView
|
||||
{
|
||||
get
|
||||
{
|
||||
return ViewModel?.IsGridView == true ? ItemsGrid : ItemsList;
|
||||
}
|
||||
}
|
||||
|
||||
public ListPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.NavigationCacheMode = NavigationCacheMode.Disabled;
|
||||
this.ItemView.Loaded += Items_Loaded;
|
||||
this.ItemView.PreviewKeyDown += Items_PreviewKeyDown;
|
||||
this.ItemView.PointerPressed += Items_PointerPressed;
|
||||
this.ItemsList.Loaded += ItemsList_Loaded;
|
||||
this.ItemsList.PreviewKeyDown += ItemsList_PreviewKeyDown;
|
||||
this.ItemsList.PointerPressed += ItemsList_PointerPressed;
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
@@ -64,11 +55,11 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
|
||||
if (e.NavigationMode == NavigationMode.Back
|
||||
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
|
||||
|| (e.NavigationMode == NavigationMode.New && ItemsList.Items.Count > 0))
|
||||
{
|
||||
// Upon navigating _back_ to this page, immediately select the
|
||||
// first item in the list
|
||||
ItemView.SelectedIndex = 0;
|
||||
ItemsList.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
@@ -99,6 +90,7 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
ViewModel?.SafeCleanup();
|
||||
CleanupHelper.Cleanup(this);
|
||||
Bindings.StopTracking();
|
||||
}
|
||||
|
||||
// Clean-up event listeners
|
||||
@@ -108,7 +100,7 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void Items_ItemClick(object sender, ItemClickEventArgs e)
|
||||
private void ItemsList_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is ListItemViewModel item)
|
||||
{
|
||||
@@ -131,9 +123,9 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
||||
private void ItemsList_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
||||
{
|
||||
if (ItemView.SelectedItem is ListItemViewModel vm)
|
||||
if (ItemsList.SelectedItem is ListItemViewModel vm)
|
||||
{
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
if (!settings.SingleClickActivates)
|
||||
@@ -144,10 +136,10 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void Items_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
private void ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var vm = ViewModel;
|
||||
var li = ItemView.SelectedItem as ListItemViewModel;
|
||||
var li = ItemsList.SelectedItem as ListItemViewModel;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
vm?.UpdateSelectedItemCommand.Execute(li);
|
||||
@@ -162,12 +154,12 @@ public sealed partial class ListPage : Page,
|
||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||
// it's in the list (otherwise, clear the cache), but that seems
|
||||
// aggressively BODGY for something that mostly just works today.
|
||||
if (ItemView.SelectedItem is not null)
|
||||
if (ItemsList.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
ItemsList.ScrollIntoView(ItemsList.SelectedItem);
|
||||
|
||||
// Automation notification for screen readers
|
||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemsList);
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
var notificationText = li.Title;
|
||||
@@ -180,37 +172,10 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
||||
private void ItemsList_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is FrameworkElement element &&
|
||||
element.DataContext is ListItemViewModel item)
|
||||
{
|
||||
if (ItemView.SelectedItem != item)
|
||||
{
|
||||
ItemView.SelectedItem = item;
|
||||
}
|
||||
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
|
||||
var pos = e.GetPosition(element);
|
||||
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
|
||||
new OpenContextMenuMessage(
|
||||
element,
|
||||
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
pos,
|
||||
ContextMenuFilterLocation.Top));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void Items_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Find the ScrollViewer in the ItemView (ItemsList or ItemsGrid)
|
||||
var listViewScrollViewer = FindScrollViewer(this.ItemView);
|
||||
// Find the ScrollViewer in the ListView
|
||||
var listViewScrollViewer = FindScrollViewer(this.ItemsList);
|
||||
|
||||
if (listViewScrollViewer is not null)
|
||||
{
|
||||
@@ -242,25 +207,25 @@ public sealed partial class ListPage : Page,
|
||||
// And then have these commands manipulate that state being bound to the UI instead
|
||||
// We may want to see how other non-list UIs need to behave to make this decision
|
||||
// At least it's decoupled from the SearchBox now :)
|
||||
if (ItemView.SelectedIndex < ItemView.Items.Count - 1)
|
||||
if (ItemsList.SelectedIndex < ItemsList.Items.Count - 1)
|
||||
{
|
||||
ItemView.SelectedIndex++;
|
||||
ItemsList.SelectedIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
ItemsList.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigatePreviousCommand message)
|
||||
{
|
||||
if (ItemView.SelectedIndex > 0)
|
||||
if (ItemsList.SelectedIndex > 0)
|
||||
{
|
||||
ItemView.SelectedIndex--;
|
||||
ItemsList.SelectedIndex--;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemView.SelectedIndex = ItemView.Items.Count - 1;
|
||||
ItemsList.SelectedIndex = ItemsList.Items.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +235,7 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(null);
|
||||
}
|
||||
else if (ItemView.SelectedItem is ListItemViewModel item)
|
||||
else if (ItemsList.SelectedItem is ListItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
}
|
||||
@@ -282,7 +247,7 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
ViewModel?.InvokeSecondaryCommandCommand.Execute(null);
|
||||
}
|
||||
else if (ItemView.SelectedItem is ListItemViewModel item)
|
||||
else if (ItemsList.SelectedItem is ListItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeSecondaryCommandCommand.Execute(item);
|
||||
}
|
||||
@@ -318,19 +283,19 @@ public sealed partial class ListPage : Page,
|
||||
//
|
||||
// It's important to do this here, because once there's no selection
|
||||
// (which can happen as the list updates) we won't get an
|
||||
// ItemView_SelectionChanged again to give us another chance to change
|
||||
// ItemsList_SelectionChanged again to give us another chance to change
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
if (ItemView.SelectedItem is null)
|
||||
if (ItemsList.SelectedItem is null)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
ItemsList.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// Always reset the selected item when the top-level list page changes
|
||||
// its items
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
ItemView.SelectedIndex = 0;
|
||||
ItemsList.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +304,7 @@ public sealed partial class ListPage : Page,
|
||||
var prop = e.PropertyName;
|
||||
if (prop == nameof(ViewModel.FilteredItems))
|
||||
{
|
||||
Debug.WriteLine($"ViewModel.FilteredItems {ItemView.SelectedItem}");
|
||||
Debug.WriteLine($"ViewModel.FilteredItems {ItemsList.SelectedItem}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,12 +328,12 @@ public sealed partial class ListPage : Page,
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
|
||||
private void ItemsList_OnContextRequested(UIElement sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
var (item, element) = e.OriginalSource switch
|
||||
{
|
||||
// caused by keyboard shortcut (e.g. Context menu key or Shift+F10)
|
||||
SelectorItem selectorItem => (ItemView.ItemFromContainer(selectorItem) as ListItemViewModel, selectorItem),
|
||||
ListViewItem listViewItem => (ItemsList.ItemFromContainer(listViewItem) as ListItemViewModel, listViewItem),
|
||||
|
||||
// caused by right-click on the ListViewItem
|
||||
FrameworkElement { DataContext: ListItemViewModel itemViewModel } frameworkElement => (itemViewModel, frameworkElement),
|
||||
@@ -381,9 +346,9 @@ public sealed partial class ListPage : Page,
|
||||
return;
|
||||
}
|
||||
|
||||
if (ItemView.SelectedItem != item)
|
||||
if (ItemsList.SelectedItem != item)
|
||||
{
|
||||
ItemView.SelectedItem = item;
|
||||
ItemsList.SelectedItem = item;
|
||||
}
|
||||
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
@@ -406,14 +371,14 @@ public sealed partial class ListPage : Page,
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Items_OnContextCanceled(UIElement sender, RoutedEventArgs e)
|
||||
private void ItemsList_OnContextCanceled(UIElement sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
|
||||
}
|
||||
|
||||
private void Items_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;
|
||||
private void ItemsList_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;
|
||||
|
||||
private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
private void ItemsList_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key is VirtualKey.Enter or VirtualKey.Space)
|
||||
{
|
||||
|
||||
@@ -123,9 +123,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
_localKeyboardListener = new LocalKeyboardListener();
|
||||
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
|
||||
_localKeyboardListener.Start();
|
||||
|
||||
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||
@@ -236,6 +233,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
|
||||
|
||||
// Make sure our HWND is cloaked before any possible window manipulations
|
||||
Cloak();
|
||||
|
||||
// Remember, IsIconic == "minimized", which is entirely different state
|
||||
// from "show/hide"
|
||||
// If we're currently minimized, restore us first, before we reveal
|
||||
@@ -243,9 +243,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// which would remain not visible to the user.
|
||||
if (PInvoke.IsIconic(hwnd))
|
||||
{
|
||||
// Make sure our HWND is cloaked before any possible window manipulations
|
||||
Cloak();
|
||||
|
||||
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
|
||||
}
|
||||
|
||||
@@ -484,13 +481,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleLaunchNonUI(AppActivationArguments? activatedEventArgs)
|
||||
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
|
||||
{
|
||||
// LOAD BEARING
|
||||
// Any reading and processing of the activation arguments must be done
|
||||
// synchronously in this method, before it returns. The sending instance
|
||||
// remains blocked until this returns; afterward it may quit, causing
|
||||
// the activation arguments to be lost.
|
||||
if (activatedEventArgs is null)
|
||||
{
|
||||
Summon(string.Empty);
|
||||
@@ -521,47 +513,15 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
return;
|
||||
}
|
||||
else if (uri.StartsWith("x-cmdpal://reload", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var settings = App.Current.Services.GetService<SettingsModel>();
|
||||
if (settings?.AllowExternalReload == true)
|
||||
{
|
||||
Logger.LogInfo("External Reload triggered");
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("External Reload is disabled");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
// https://learn.microsoft.com/en-us/windows/win32/rpc/rpc-return-values
|
||||
const int RPC_S_SERVER_UNAVAILABLE = -2147023174;
|
||||
const int RPC_S_CALL_FAILED = 2147023170;
|
||||
|
||||
// Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException
|
||||
// if the args are not valid or not passed correctly.
|
||||
if (ex.HResult is RPC_S_SERVER_UNAVAILABLE or RPC_S_CALL_FAILED)
|
||||
{
|
||||
Logger.LogWarning(
|
||||
$"COM exception (HRESULT {ex.HResult}) when accessing activation arguments. " +
|
||||
$"This might be due to the calling application not passing them correctly or exiting before we could read them. " +
|
||||
$"The application will continue running and fall back to showing the Command Palette window.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError(
|
||||
$"COM exception (HRESULT {ex.HResult}) when activating the application. " +
|
||||
$"The application will continue running and fall back to showing the Command Palette window.",
|
||||
ex);
|
||||
}
|
||||
Logger.LogError("COM exception when activating the application", ex);
|
||||
}
|
||||
|
||||
Summon(string.Empty);
|
||||
@@ -650,20 +610,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
|
||||
private void HandleSummon(string commandId)
|
||||
{
|
||||
if (_ignoreHotKeyWhenFullScreen)
|
||||
{
|
||||
// If we're in full screen mode, ignore the hotkey
|
||||
if (WindowHelper.IsWindowFullscreen())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HandleSummonCore(commandId);
|
||||
}
|
||||
|
||||
private void HandleSummonCore(string commandId)
|
||||
{
|
||||
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
|
||||
@@ -688,6 +634,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// so that we can bind hotkeys to individual commands
|
||||
if (!isVisible || !isRootHotkey)
|
||||
{
|
||||
Activate();
|
||||
|
||||
Summon(commandId);
|
||||
}
|
||||
else if (isRootHotkey)
|
||||
@@ -723,6 +671,15 @@ public sealed partial class MainWindow : WindowEx,
|
||||
var hotkeyIndex = (int)wParam.Value;
|
||||
if (hotkeyIndex < _hotkeys.Count)
|
||||
{
|
||||
if (_ignoreHotKeyWhenFullScreen)
|
||||
{
|
||||
// If we're in full screen mode, ignore the hotkey
|
||||
if (WindowHelper.IsWindowFullscreen())
|
||||
{
|
||||
return (LRESULT)IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
var hotkey = _hotkeys[hotkeyIndex];
|
||||
HandleSummon(hotkey.CommandId);
|
||||
}
|
||||
|
||||
@@ -107,33 +107,12 @@ internal sealed class Program
|
||||
{
|
||||
// Do the redirection on another thread, and use a non-blocking
|
||||
// wait method to wait for the redirection to complete.
|
||||
using var redirectSemaphore = new Semaphore(0, 1);
|
||||
var redirectTimeout = TimeSpan.FromSeconds(32);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
var redirectSemaphore = new Semaphore(0, 1);
|
||||
Task.Run(() =>
|
||||
{
|
||||
using var cts = new CancellationTokenSource(redirectTimeout);
|
||||
try
|
||||
{
|
||||
keyInstance.RedirectActivationToAsync(args)
|
||||
.AsTask(cts.Token)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogError($"Failed to activate existing instance; timed out after {redirectTimeout}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to activate existing instance", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
redirectSemaphore.Release();
|
||||
}
|
||||
keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
|
||||
redirectSemaphore.Release();
|
||||
});
|
||||
|
||||
_ = PInvoke.CoWaitForMultipleObjects(
|
||||
(uint)CWMO_FLAGS.CWMO_DEFAULT,
|
||||
PInvoke.INFINITE,
|
||||
@@ -145,14 +124,13 @@ internal sealed class Program
|
||||
{
|
||||
// If we already have a form, display the message now.
|
||||
// Otherwise, add it to the collection for displaying later.
|
||||
if (App.Current?.AppWindow is MainWindow mainWindow)
|
||||
if (App.Current is App thisApp)
|
||||
{
|
||||
// LOAD BEARING
|
||||
// This must be synchronous to ensure the method does not return
|
||||
// before the activation is fully handled and the parameters are processed.
|
||||
// The sending instance remains blocked until this returns; afterward it may quit,
|
||||
// causing the activation arguments to be lost.
|
||||
mainWindow.HandleLaunchNonUI(args);
|
||||
if (thisApp.AppWindow is not null and
|
||||
MainWindow mainWindow)
|
||||
{
|
||||
uiContext?.Post(_ => mainWindow.HandleLaunch(args), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
<RepositionThemeTransition IsStaggeringEnabled="False" />
|
||||
</StackPanel.ChildrenTransitions>-->
|
||||
|
||||
<!-- 'Activation' section -->
|
||||
|
||||
<TextBlock x:Uid="ActivationSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsExpander
|
||||
@@ -68,8 +66,6 @@
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- 'Behavior' section -->
|
||||
|
||||
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowAppDetails_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
@@ -88,16 +84,7 @@
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- 'For Developers' section -->
|
||||
|
||||
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AllowExternalReload_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.AllowExternalReload, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- 'About' section -->
|
||||
|
||||
<!-- Example 'About' section -->
|
||||
<TextBlock x:Uid="AboutSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsExpander x:Uid="Settings_GeneralPage_About_SettingsExpander" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/StoreLogo.png}">
|
||||
|
||||
@@ -24,15 +24,23 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar x:Name="TitleBar">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="ms-appx:///Assets/icon.svg" />
|
||||
</TitleBar.LeftHeader>
|
||||
</TitleBar>
|
||||
<!-- TO DO: Replace this with WinUI TitleBar once that ships. -->
|
||||
<StackPanel
|
||||
x:Name="AppTitleBar"
|
||||
Grid.Row="0"
|
||||
Height="48"
|
||||
Margin="16,0,0,0"
|
||||
Orientation="Horizontal">
|
||||
<Image
|
||||
Width="16"
|
||||
Height="16"
|
||||
Source="ms-appx:///Assets/icon.svg" />
|
||||
<TextBlock
|
||||
x:Uid="CmdPalSettingsHeader"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}" />
|
||||
</StackPanel>
|
||||
<NavigationView
|
||||
x:Name="NavView"
|
||||
Grid.Row="1"
|
||||
@@ -69,6 +77,7 @@
|
||||
x:Name="NavigationBreadcrumbBar"
|
||||
Grid.Row="0"
|
||||
MaxWidth="1000"
|
||||
Margin="16,0,0,0"
|
||||
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
|
||||
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
|
||||
<BreadcrumbBar.ItemTemplate>
|
||||
|
||||
@@ -31,10 +31,8 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
this.InitializeComponent();
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.SetIcon();
|
||||
var title = RS_.GetString("SettingsWindowTitle");
|
||||
this.AppWindow.Title = title;
|
||||
this.AppWindow.Title = RS_.GetString("SettingsWindowTitle");
|
||||
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
|
||||
this.TitleBar.Title = title;
|
||||
PositionCentered();
|
||||
|
||||
WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);
|
||||
|
||||
@@ -441,13 +441,4 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="NavigationPaneOpened" xml:space="preserve">
|
||||
<value>Navigation page opened</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AllowExternalReload_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Enable external reload</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AllowExternalReload_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Trigger reload of the extension externally with the x-cmdpal://reload command</value>
|
||||
</data>
|
||||
<data name="ForDevelopersSettingsHeader.Text" xml:space="preserve">
|
||||
<value>For Developers</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,177 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "FontIconGlyphClassifier.h"
|
||||
#include "FontIconGlyphClassifier.g.cpp"
|
||||
|
||||
#include <icu.h>
|
||||
#include <utility>
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Check if the code point is in the Private Use Area range used by Fluent UI icons.
|
||||
[[nodiscard]] constexpr bool _isFluentIconPua(const UChar32 cp) noexcept
|
||||
{
|
||||
static constexpr UChar32 _fluentIconsPrivateUseAreaStart = 0xE700;
|
||||
static constexpr UChar32 _fluentIconsPrivateUseAreaEnd = 0xF8FF;
|
||||
return cp >= _fluentIconsPrivateUseAreaStart && cp <= _fluentIconsPrivateUseAreaEnd;
|
||||
}
|
||||
|
||||
// Determine if the given text (as a sequence of UChar code units) is emoji
|
||||
[[nodiscard]] bool _isEmoji(const UChar* p, const int32_t length) noexcept
|
||||
{
|
||||
if (!p || length < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://www.unicode.org/reports/tr51/#Emoji_Variation_Selector_Notes
|
||||
constexpr UChar32 vs15CodePoint = 0xFE0E; // Variation Selectors 15: text variation selector
|
||||
constexpr UChar32 vs16CodePoint = 0xFE0F; // Variation Selectors: 16 emoji variation selector
|
||||
|
||||
// Decode the first code point correctly (surrogate-safe)
|
||||
int32_t i0{ 0 };
|
||||
UChar32 first{ 0 };
|
||||
U16_NEXT(p, i0, length, first);
|
||||
|
||||
for (int32_t i = 0; i < length;)
|
||||
{
|
||||
UChar32 cp{ 0 };
|
||||
U16_NEXT(p, i, length, cp);
|
||||
|
||||
if (cp == vs16CodePoint) { return true; }
|
||||
if (cp == vs15CodePoint) { return false; }
|
||||
}
|
||||
|
||||
return !U_IS_SURROGATE(first) && u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION);
|
||||
}
|
||||
}
|
||||
|
||||
bool FontIconGlyphClassifier::IsLikelyToBeEmojiOrSymbolIcon(const hstring& text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.size() == 1 && !IS_HIGH_SURROGATE(text[0]))
|
||||
{
|
||||
// If it's a single code unit, it's definitely either zero or one grapheme clusters.
|
||||
// If it turns out to be illegal Unicode, we don't really care.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (text.size() >= 2 && text[0] <= 0x7F && text[1] <= 0x7F)
|
||||
{
|
||||
// Two adjacent ASCII characters (as seen in most file paths) aren't a single
|
||||
// grapheme cluster.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use ICU to determine whether text is composed of a single grapheme cluster.
|
||||
int32_t off{ 0 };
|
||||
UErrorCode status{ U_ZERO_ERROR };
|
||||
|
||||
UBreakIterator* const bi{ ubrk_open(UBRK_CHARACTER,
|
||||
nullptr,
|
||||
reinterpret_cast<const UChar*>(text.data()),
|
||||
static_cast<int>(text.size()),
|
||||
&status) };
|
||||
if (bi)
|
||||
{
|
||||
if (U_SUCCESS(status))
|
||||
{
|
||||
off = ubrk_next(bi);
|
||||
}
|
||||
ubrk_close(bi);
|
||||
}
|
||||
return std::cmp_equal(off, text.size());
|
||||
}
|
||||
|
||||
FontIconGlyphKind FontIconGlyphClassifier::Classify(hstring const& text) noexcept
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return FontIconGlyphKind::None;
|
||||
}
|
||||
|
||||
const size_t textSize{ text.size() };
|
||||
const auto* buffer{ reinterpret_cast<const UChar*>(text.c_str()) };
|
||||
|
||||
// Fast path 1: Single UTF-16 code unit (most common case)
|
||||
if (textSize == 1)
|
||||
{
|
||||
const UChar ch{ buffer[0] };
|
||||
|
||||
if (IS_HIGH_SURROGATE(ch))
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
if (_isFluentIconPua(ch))
|
||||
{
|
||||
return FontIconGlyphKind::FluentSymbol;
|
||||
}
|
||||
|
||||
if (_isEmoji(&ch, 1))
|
||||
{
|
||||
return FontIconGlyphKind::Emoji;
|
||||
}
|
||||
|
||||
return FontIconGlyphKind::Other;
|
||||
}
|
||||
|
||||
// Fast path 2: Common file path pattern - two ASCII printable characters
|
||||
if (textSize >= 2 && buffer[0] <= 0x7F && buffer[1] <= 0x7F)
|
||||
{
|
||||
// Definitely multiple graphemes
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
// Expensive path: Use ICU to determine grapheme boundaries
|
||||
UErrorCode status{ U_ZERO_ERROR };
|
||||
|
||||
UBreakIterator* bi{ ubrk_open(UBRK_CHARACTER,
|
||||
nullptr,
|
||||
buffer,
|
||||
static_cast<int32_t>(textSize),
|
||||
&status) };
|
||||
|
||||
if (U_FAILURE(status) || !bi)
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
const int32_t start{ ubrk_first(bi) };
|
||||
const int32_t end{ ubrk_next(bi) }; // end of first grapheme
|
||||
ubrk_close(bi);
|
||||
|
||||
// No graphemes found
|
||||
if (end == UBRK_DONE || end <= start)
|
||||
{
|
||||
return FontIconGlyphKind::None;
|
||||
}
|
||||
|
||||
// If there's more than one grapheme, it's not a valid icon glyph
|
||||
if (std::cmp_not_equal(end, textSize))
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
// Exactly one grapheme: classify
|
||||
const UChar* grapheme = buffer + start;
|
||||
const int32_t graphemeLength = end - start;
|
||||
|
||||
if (graphemeLength == 1 && _isFluentIconPua(grapheme[0]))
|
||||
{
|
||||
return FontIconGlyphKind::FluentSymbol;
|
||||
}
|
||||
|
||||
if (_isEmoji(grapheme, graphemeLength))
|
||||
{
|
||||
return FontIconGlyphKind::Emoji;
|
||||
}
|
||||
|
||||
return FontIconGlyphKind::Other;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "FontIconGlyphClassifier.g.h"
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
struct FontIconGlyphClassifier
|
||||
{
|
||||
[[nodiscard]] static bool IsLikelyToBeEmojiOrSymbolIcon(const winrt::hstring& text);
|
||||
|
||||
[[nodiscard]] static FontIconGlyphKind Classify(winrt::hstring const& text) noexcept;
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::factory_implementation
|
||||
{
|
||||
BASIC_FACTORY(FontIconGlyphClassifier);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft.Terminal.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Categorizes the type of a single grapheme cluster or input text.
|
||||
/// Used to determine how the input should be handled or rendered (for example,
|
||||
/// whether it should be treated as an emoji, an icon from a symbol font, plain text, etc.).
|
||||
/// </summary>
|
||||
enum FontIconGlyphKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Input is invalid or contains more than one grapheme cluster and therefore cannot be
|
||||
/// treated as a single symbol. Typical for multi-character text like file paths
|
||||
/// or composed strings that include separators.
|
||||
/// </summary>
|
||||
Invalid = -1,
|
||||
|
||||
/// <summary>
|
||||
/// No grapheme present (empty string). Indicates absence of a symbol.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A single emoji grapheme cluster. This may consist of multiple Unicode code
|
||||
/// points combined into one visible glyph (e.g., emoji with modifiers or ZWJ sequences).
|
||||
/// </summary>
|
||||
Emoji = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A single glyph from the Segoe Fluent Icons / MDL2 Assets Private Use Area (PUA),
|
||||
/// typically in the Unicode range U+E700–U+F8FF. These are font-based icons (Fluent/MDL2).
|
||||
/// </summary>
|
||||
FluentSymbol = 2,
|
||||
|
||||
/// <summary>
|
||||
/// A single non-emoji grapheme that is not a Fluent/MDL2 PUA symbol.
|
||||
/// Covers ordinary characters, letters, numbers, or other single glyph symbols.
|
||||
/// </summary>
|
||||
Other = 3,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Static utility class for text and icon analysis
|
||||
/// </summary>
|
||||
static runtimeclass FontIconGlyphClassifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if text represents a single grapheme cluster (emoji/symbol icon).
|
||||
/// Uses ICU for Unicode boundary detection to distinguish icons from file paths.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to analyze</param>
|
||||
/// <returns>True if single grapheme cluster, false for multi-character text or paths</returns>
|
||||
static Boolean IsLikelyToBeEmojiOrSymbolIcon(String text);
|
||||
|
||||
/// <summary>
|
||||
/// Classifies the input into a glyph kind suitable for icon or text rendering.
|
||||
/// </summary>
|
||||
static FontIconGlyphKind Classify(String text);
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "IconPathConverter.h"
|
||||
#include "IconPathConverter.g.cpp"
|
||||
|
||||
#include "FontIconGlyphClassifier.h"
|
||||
// #include "Utils.h"
|
||||
|
||||
#include <Shlobj.h>
|
||||
#include <Shlobj_core.h>
|
||||
@@ -110,7 +110,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
|
||||
{
|
||||
typename ImageIconSource<TIconSource>::type iconSource;
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||
iconSource.ImageSource(source);
|
||||
return iconSource;
|
||||
}
|
||||
@@ -169,46 +169,41 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
|
||||
// If we fail to set the icon source using the "icon" as a path,
|
||||
// let's try it as a symbol/emoji.
|
||||
if (!iconSource)
|
||||
//
|
||||
// Anything longer than 2 wchar_t's _isn't_ an emoji or symbol, so
|
||||
// don't do this if it's just an invalid path.
|
||||
if (!iconSource && iconPath.size() <= 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto glyph_kind = FontIconGlyphClassifier::Classify(iconPath);
|
||||
typename FontIconSource<TIconSource>::type icon;
|
||||
const auto ch = til::at(iconPath, 0);
|
||||
|
||||
winrt::hstring family;
|
||||
if (glyph_kind == FontIconGlyphKind::Invalid)
|
||||
// The range of MDL2 Icons isn't explicitly defined, but
|
||||
// we're using this based off the table on:
|
||||
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
|
||||
const auto isMDL2Icon = ch >= L'\uE700' && ch <= L'\uF8FF';
|
||||
if (isMDL2Icon)
|
||||
{
|
||||
family = L"Segoe UI";
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
|
||||
}
|
||||
else if (!fontFamily.empty())
|
||||
{
|
||||
family = fontFamily;
|
||||
}
|
||||
else if (glyph_kind == FontIconGlyphKind::FluentSymbol)
|
||||
{
|
||||
family = L"Segoe Fluent Icons, Segoe MDL2 Assets";
|
||||
}
|
||||
else if (glyph_kind == FontIconGlyphKind::Emoji)
|
||||
{
|
||||
// Emoji and other symbols go in the Segoe UI Emoji font.
|
||||
// Some emojis (e.g. 2️⃣) would be rendered as emoji glyphs otherwise.
|
||||
family = L"Segoe UI Emoji, Segoe UI";
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ fontFamily });
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
family = L"Segoe UI";
|
||||
// Note: you _do_ need to manually set the font here.
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe UI" });
|
||||
}
|
||||
|
||||
typename FontIconSource<TIconSource>::type icon;
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family });
|
||||
icon.FontSize(targetSize);
|
||||
icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath);
|
||||
icon.Glyph(iconPath);
|
||||
iconSource = icon;
|
||||
}
|
||||
CATCH_LOG();
|
||||
}
|
||||
}
|
||||
|
||||
if (!iconSource)
|
||||
{
|
||||
// Set the default IconSource to a BitmapIconSource with a null source
|
||||
@@ -331,7 +326,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
}
|
||||
|
||||
static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex,
|
||||
int index,
|
||||
int index,
|
||||
int targetSize)
|
||||
{
|
||||
// Try:
|
||||
|
||||
@@ -159,9 +159,6 @@
|
||||
<ClInclude Include="ResourceString.h">
|
||||
<DependentUpon>ResourceString.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FontIconGlyphClassifier.h">
|
||||
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="init.cpp" />
|
||||
@@ -181,9 +178,6 @@
|
||||
<DependentUpon>ResourceString.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
|
||||
<ClCompile Include="FontIconGlyphClassifier.cpp">
|
||||
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="Converters.idl" />
|
||||
@@ -191,7 +185,6 @@
|
||||
<Midl Include="RunHistory.idl" />
|
||||
<Midl Include="IDirectKeyListener.idl" />
|
||||
<Midl Include="ResourceString.idl" />
|
||||
<Midl Include="FontIconGlyphClassifier.idl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// 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 Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
|
||||
|
||||
@@ -12,22 +13,34 @@ public class MockSettingsInterface : ISettingsInterface
|
||||
{
|
||||
private readonly List<HistoryItem> _historyItems;
|
||||
|
||||
public event EventHandler HistoryChanged;
|
||||
|
||||
public bool GlobalIfURI { get; set; }
|
||||
|
||||
public int HistoryItemCount { get; set; }
|
||||
public uint HistoryItemCount { get; set; }
|
||||
|
||||
public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
|
||||
|
||||
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
|
||||
public MockSettingsInterface(uint historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
|
||||
{
|
||||
_historyItems = mockHistory ?? new List<HistoryItem>();
|
||||
GlobalIfURI = globalIfUri;
|
||||
HistoryItemCount = historyItemCount;
|
||||
}
|
||||
|
||||
public void AddHistoryItem(HistoryItem historyItem)
|
||||
public List<ListItem> LoadHistory()
|
||||
{
|
||||
var listItems = new List<ListItem>();
|
||||
foreach (var historyItem in _historyItems)
|
||||
{
|
||||
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
|
||||
{
|
||||
Title = historyItem.SearchString,
|
||||
Subtitle = historyItem.Timestamp.ToString("g", System.Globalization.CultureInfo.InvariantCulture),
|
||||
});
|
||||
}
|
||||
|
||||
listItems.Reverse();
|
||||
return listItems;
|
||||
}
|
||||
|
||||
public void SaveHistory(HistoryItem historyItem)
|
||||
{
|
||||
if (historyItem is null)
|
||||
{
|
||||
@@ -41,18 +54,15 @@ public class MockSettingsInterface : ISettingsInterface
|
||||
{
|
||||
while (_historyItems.Count > HistoryItemCount)
|
||||
{
|
||||
_historyItems.RemoveAt(0);
|
||||
_historyItems.RemoveAt(0); // Remove the oldest item
|
||||
}
|
||||
}
|
||||
|
||||
HistoryChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// Helper method for testing
|
||||
public void ClearHistory()
|
||||
{
|
||||
_historyItems.Clear();
|
||||
HistoryChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// Helper method for testing
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user