Compare commits

..

64 Commits

Author SHA1 Message Date
Noraa Junker
797bd4cb35 Port Mouse Highlighter and abstract IPowerToys module into other interfaces to be more modular 2026-01-22 01:25:28 +01:00
Noraa Junker
297c7f0cbc Port Mouse Pointer Crosshairs 2026-01-22 00:14:15 +01:00
Noraa Junker
7df1c2f002 merge main 2026-01-21 22:16:52 +01:00
Jessica Dene Earley-Cha
fbabdffcfa Update DATA_AND_PRIVACY to reflect current status (#44844)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Updates the telemetry events documentation in `DATA_AND_PRIVACY.md` to
ensure completeness and improve organization.
- Added missing & new events
- Removed obsolete events (deprecated or unused event)
- Alphabetized all modules for easier navigation and maintenance
- Made all tables uniform with `table-layout:fixed`



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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-21 11:07:12 -08:00
Mike Hall
7cfa4d443c add script parameters to add user, machine, and devices names, default is to not include. (#44880)
## Summary of the Pull Request
Update the screen capture/layout powershell script to NOT include user
name, machine name, and device names - these can be added into the
output JSON file using new command line parameters - also added '-help'.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Ran the script locally with each of the new command line parameters and
validated the json output.
2026-01-21 12:58:00 +00:00
Gordon Lam
5422bc31bb docs(prompts): Enhance release notes generation and implement GitHub issue tools (#44762)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This update improves the release notes generation process by
restructuring documentation for clarity and enhancing workflows. It
introduces new tools for handling GitHub issue images and attachments,
along with automated tests to ensure functionality.

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

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
The changes include updates to the release notes summarization process
and workflow documentation for better clarity. New scripts for managing
PR diffs, grouping, and milestone assignments have been added.
Additionally, tools for downloading images and attachments from GitHub
issues have been implemented, ensuring a more streamlined process.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Automated tests were added for the new GitHub issue tools, and all tests
passed successfully. Manual validation included testing the new
workflows and ensuring the documentation accurately reflects the changes
made.
```
2026-01-21 16:18:57 +08:00
Noraa Junker
8727a72a83 Signing 2026-01-14 22:42:33 +01:00
Noraa Junker
a2e81dbc3f Merge branch 'feature/runner-v2' of https://github.com/microsoft/PowerToys into feature/runner-v2 2026-01-14 22:37:50 +01:00
Noraa Junker
7c870d48ea Fix version.h 2026-01-14 22:37:46 +01:00
Noraa Junker
3006cbf42d Fix version.h 2026-01-14 22:22:29 +01:00
Noraa Junker
a7ea40749e Remove several now unused classes/projects 2026-01-14 19:37:36 +01:00
Noraa Junker
94ea7597be Remove Hotkey functionality 2026-01-11 23:12:39 +01:00
Noraa Junker
3f6b41fc80 Fix some merge errors and port workspaces 2026-01-11 23:10:42 +01:00
Noraa Junker
aede049596 merge main 2026-01-11 22:47:28 +01:00
Noraa Junker
5440699250 Merge main 2026-01-11 22:47:10 +01:00
Noraa Junker
57957d5dbf Merge main 2026-01-11 22:46:28 +01:00
Noraa Junker
6e882b9cb2 Port Find My Mouse 2026-01-11 22:41:43 +01:00
Noraa Junker
ce24d9d951 Port cursorwrap 2026-01-11 20:38:47 +01:00
Noraa Junker
3ee8a78178 Consolidate File Explorer cpp projects in one common project 2026-01-10 23:36:17 +01:00
Noraa Junker
d0b46a98eb Port ActionRunner 2026-01-10 22:13:13 +01:00
Noraa Junker
3c08019190 Some build fixes 2025-12-22 23:37:37 +01:00
Noraa Junker
25badc45a4 Fix some stuff 2025-12-22 22:38:29 +01:00
Noraa Junker
6b8a2064ab Merge branch 'feature/runner-v2' of https://github.com/microsoft/PowerToys into feature/runner-v2 2025-12-22 01:09:22 +01:00
Noraa Junker
14d8c36768 Port LightSwitch 2025-12-22 01:09:20 +01:00
Noraa Junker
23500371b5 Port LightSwitch 2025-12-22 01:03:18 +01:00
Noraa Junker
f932679911 Port Keyboard Manager 2025-12-21 23:59:39 +01:00
Noraa Junker
81494b23bf Port PowerToys Run 2025-12-21 23:40:58 +01:00
Noraa Junker
1ad1c98c98 Port FancyZones 2025-12-21 23:16:15 +01:00
Noraa Junker
ec9071bc4f Fix File Explorer add-ons 2025-12-21 22:51:04 +01:00
Noraa Junker
180d4a21aa Port MouseJump 2025-12-21 17:21:50 +01:00
Noraa Junker
9a77b8e9cb Remove project template 2025-12-21 17:08:21 +01:00
Noraa Junker
b1935ca1eb Merge main 2025-12-21 17:07:16 +01:00
Noraa Junker
a1c34b1503 Port Screen Ruler 2025-12-21 17:03:37 +01:00
Noraa Junker
5368c82154 Port TextExtractor 2025-12-21 16:41:35 +01:00
Noraa Junker
c21ab5bc30 Merge branch 'feature/runner-v2' of https://github.com/microsoft/PowerToys into feature/runner-v2 2025-12-21 16:20:18 +01:00
Noraa Junker
b3a084db0a Port ZoomIt 2025-12-21 16:20:12 +01:00
Noraa Junker
4fe880cc1e Port ZoomIt 2025-12-21 16:19:09 +01:00
Noraa Junker
e9cf9113a4 Port File Explorer Add-Ons 2025-12-21 15:58:06 +01:00
Noraa Junker
2752054b14 Port Registry Preview and move module interface interface to models 2025-12-19 20:32:03 +01:00
Noraa Junker
f7a30212d9 Port Environment Variables module and fix launching hosts 2025-12-18 16:54:34 +01:00
Noraa Junker
9b36bb9ddd Fix merging 2025-12-18 16:18:57 +01:00
Noraa Junker
79515f0d6b merge main 2025-12-18 15:40:50 +01:00
Noraa Junker
99523fe317 Moved from custom event implementation to EventWaitHanlde 2025-12-14 22:48:34 +01:00
Noraa Junker
5398c16456 Add some documentation 2025-12-11 18:15:07 +01:00
Noraa Junker
123d318d6a Created a new way to treat process creation from module interfaces 2025-12-11 01:43:04 +01:00
Noraa Junker
09a79ab692 Port Crop and Lock 2025-12-10 23:31:41 +01:00
Noraa Junker
9cd23666d3 Port Command Palette module interface 2025-12-10 23:10:48 +01:00
Noraa Junker
bf99a2a0e5 Migrate ColorPicker and use common process killer function 2025-12-03 21:58:07 +01:00
Noraa Junker
261381f2f7 Migrate Command on Palette module 2025-12-03 20:34:50 +01:00
Noraa Junker
b971d8799a Removed dependency on PowerToysSettings by moving HotkeySettingsControlHook and NativeKeyboardHelper to ManagedCommon 2025-12-03 19:59:20 +01:00
Noraa Junker
0155bb3b63 Port Awake 2025-12-02 21:51:28 +01:00
Noraa Junker
43e9959df4 Fix ctrl alt key in Keyboard Hook and Advanced Paste custom actions 2025-12-02 21:19:19 +01:00
Noraa Junker
231f59c7a9 Remove old Advanced Paste module interface 2025-12-01 23:28:02 +01:00
Noraa Junker
1532dd5b38 Move AlwaysOnTop to keyboard hook (like old moduleinterface) 2025-12-01 23:27:13 +01:00
Noraa Junker
9afa3a1ecc Add Keyboard Hook mechanism 2025-12-01 23:25:10 +01:00
Noraa Junker
17af826408 Include update ability 2025-11-26 22:21:48 +01:00
Noraa Junker
b9c9ade9d9 Use atom variable in hotkey id 2025-11-19 23:32:31 +01:00
Noraa Junker
18c335397f Change location of moduleinterfaces and fix hotkey issues 2025-11-19 23:31:27 +01:00
Noraa Junker
6d1f533af4 merge 2025-11-19 15:23:59 +01:00
Noraa Junker
6dd9617538 Adapt Always on Top module to use right settings and GPO 2025-11-19 15:22:44 +01:00
Noraa Junker
d1564b0572 Adapt Always on Top module to use right settings and GPO 2025-11-19 14:52:37 +01:00
Noraa Junker
2ddf561f57 More things implemented 2025-11-18 22:01:20 +01:00
Noraa Junker
6fea4d9c5d Fix tray icon behaviour 2025-11-16 23:21:52 +01:00
Noraa Junker
77a8555fd4 push 2025-11-16 22:50:40 +01:00
703 changed files with 10750 additions and 32254 deletions

View File

@@ -32,6 +32,7 @@ advfirewall
AFeature
affordances
AFX
agentskills
AGGREGATABLE
AHK
AHybrid
@@ -219,6 +220,7 @@ CIELCh
cim
CImage
cla
claude
CLASSDC
classmethod
CLASSNOTAVAILABLE
@@ -1040,6 +1042,7 @@ mmi
mmsys
mobileredirect
mockapi
modelcontextprotocol
MODALFRAME
MODESPRUNED
MONITORENUMPROC
@@ -1737,6 +1740,7 @@ STICKYKEYS
sticpl
storelogo
stprintf
streamable
streamjsonrpc
STRINGIZE
stringtable
@@ -1756,6 +1760,7 @@ SUBMODULEUPDATE
subresource
Superbar
sut
swe
svchost
SVGIn
SVGIO
@@ -1962,6 +1967,7 @@ visualeffects
vkey
vmovl
VMs
vnd
vorrq
VOS
vpaddlq

92
.github/agents/FixIssue.agent.md vendored Normal file
View File

@@ -0,0 +1,92 @@
---
description: 'Implements fixes for GitHub issues based on implementation plans'
name: 'FixIssue'
tools: ['read', 'edit', 'search', 'execute', 'agent', 'usages', 'problems', 'changes', 'testFailure', 'github/*', 'github.vscode-pull-request-github/*']
argument-hint: 'GitHub issue number (e.g., #12345)'
infer: true
---
# FixIssue Agent
You are an **IMPLEMENTATION AGENT** specialized in executing implementation plans to fix GitHub issues.
## Identity & Expertise
- Expert at translating plans into working code
- Deep knowledge of PowerToys codebase patterns and conventions
- Skilled at writing tests, handling edge cases, and validating builds
- You follow plans precisely while handling ambiguity gracefully
## Goal
For the given **issue_number**, execute the implementation plan and produce:
1. Working code changes applied directly to the repository
2. `Generated Files/issueFix/{{issue_number}}/pr-description.md` — PR-ready description
3. `Generated Files/issueFix/{{issue_number}}/manual-steps.md` — Only if human action needed
## Core Directive
**Follow the implementation plan in `Generated Files/issueReview/{{issue_number}}/implementation-plan.md` as the single source of truth.**
If the plan doesn't exist, invoke PlanIssue agent first via `runSubagent`.
## Working Principles
- **Plan First**: Read and understand the entire implementation plan before coding
- **Validate Always**: For each change: Edit → Build → Verify → Commit. Never proceed if build fails.
- **Atomic Commits**: Each commit must be self-contained, buildable, and meaningful
- **Ask, Don't Guess**: When uncertain, insert `// TODO(Human input needed): <question>` and document in manual-steps.md
## Strategy
**Core Loop** — For every unit of work:
1. **Edit**: Make focused changes to implement one logical piece
2. **Build**: Run `tools\build\build.cmd` and check for exit code 0
3. **Verify**: Use `problems` tool for lint/compile errors; run relevant tests
4. **Commit**: Only after build passes — use `.github/prompts/create-commit-title.prompt.md`
Never skip steps. Never commit broken code. Never proceed if build fails.
**Feature-by-Feature E2E**: For big scenarios with multiple features, complete each feature end-to-end before moving to the next:
- Settings UI → Functionality → Logging → Tests (for Feature 1)
- Then repeat for Feature 2
- Benefits: Each feature is self-contained, testable, easier to review, can ship incrementally
**Large Changes** (3+ files or cross-module):
- Use `tools\build\New-WorktreeFromBranch.ps1` for isolated worktrees
- Create separate branches per feature (e.g., `issue/{{issue_number}}-export`, `issue/{{issue_number}}-import`)
- Merge feature branches back after each is validated
**Recovery**: If implementation goes wrong:
- Create a checkpoint branch before risky changes
- On failure: branch from last known-good state, cherry-pick working changes, abandon broken branch
- For complex changes, consider multiple smaller PRs
## Guidelines
**DO**:
- Follow the plan exactly
- Validate build before every commit — **NEVER commit broken code**
- Use `.github/prompts/create-commit-title.prompt.md` for commit messages
- Add comprehensive tests for changed behavior
- Use worktrees for large changes (3+ files or cross-module)
- Document deviations from plan
**DON'T**:
- Implement everything in a single massive commit
- Continue after a failed build without fixing
- Make drive-by refactors outside issue scope
- Skip tests for behavioral changes
- Add noisy logs in hot paths
- Break IPC/JSON contracts without updating both sides
- Introduce dependencies without documenting in NOTICE.md
## References
- [Build Guidelines](../../tools/build/BUILD-GUIDELINES.md) — Build commands and validation
- [Coding Style](../../doc/devdocs/development/style.md) — Formatting and conventions
- [AGENTS.md](../../AGENTS.md) — Full contributor guide
## Parameter
- **issue_number**: Extract from `#123`, `issue 123`, or plain number. If missing, ask user.

65
.github/agents/PlanIssue.agent.md vendored Normal file
View File

@@ -0,0 +1,65 @@
---
description: 'Analyzes GitHub issues to produce overview and implementation plans'
name: 'PlanIssue'
tools: ['execute', 'read', 'edit', 'search', 'web', 'github/*', 'agent', 'github-artifacts/*', 'todo']
argument-hint: 'GitHub issue number (e.g., #12345)'
handoffs:
- label: Start Implementation
agent: FixIssue
prompt: 'Fix issue #{{issue_number}} using the implementation plan'
- label: Open Plan in Editor
agent: agent
prompt: 'Open Generated Files/issueReview/{{issue_number}}/overview.md and implementation-plan.md'
showContinueOn: false
send: true
infer: true
---
# PlanIssue Agent
You are a **PLANNING AGENT** specialized in analyzing GitHub issues and producing comprehensive planning documentation.
## Identity & Expertise
- Expert at issue triage, priority scoring, and technical analysis
- Deep knowledge of PowerToys architecture and codebase patterns
- Skilled at breaking down problems into actionable implementation steps
- You research thoroughly before planning, gathering 80% confidence before drafting
## Goal
For the given **issue_number**, produce two deliverables:
1. `Generated Files/issueReview/{{issue_number}}/overview.md` — Issue analysis with scoring
2. `Generated Files/issueReview/{{issue_number}}/implementation-plan.md` — Technical implementation plan
Above is the core interaction with the end user. If you cannot produce the files above, you fail the task. Each time, you must check whether the files exist or have been modified by the end user, without assuming you know their contents.
3. `Generated Files/issueReview/{{issue_number}}/logs/**` — logs for your diagnostic of root cause, research steps, and reasoning
## Core Directive
**Follow the template in `.github/prompts/review-issue.prompt.md` exactly.** Read it first, then apply every section as specified.
- Fetch issue details: reactions, comments, linked PRs, images, logs
- Search related code and similar past fixes
- Ask clarifying questions when ambiguous
- Identify subject matter experts via git history
<stopping_rules>
You are a PLANNING agent, NOT an implementation agent.
STOP if you catch yourself:
- Writing code or editing source files outside `Generated Files/issueReview/`
- Making assumptions without researching
- Skipping the scoring/assessment phase
Plans describe what the USER or FixIssue agent will execute later.
</stopping_rules>
## References
- [Review Issue Prompt](../.github/prompts/review-issue.prompt.md) — Template for plan structure
- [Architecture Overview](../../doc/devdocs/core/architecture.md) — System design context
- [AGENTS.md](../../AGENTS.md) — Full contributor guide
## Parameter
- **issue_number**: Extract from `#123`, `issue 123`, or plain number. If missing, ask user.

View File

@@ -0,0 +1,261 @@
---
description: 'Guidelines for creating high-quality Agent Skills for GitHub Copilot'
applyTo: '**/.github/skills/**/SKILL.md, **/.claude/skills/**/SKILL.md'
---
# Agent Skills File Guidelines
Instructions for creating effective and portable Agent Skills that enhance GitHub Copilot with specialized capabilities, workflows, and bundled resources.
## What Are Agent Skills?
Agent Skills are self-contained folders with instructions and bundled resources that teach AI agents specialized capabilities. Unlike custom instructions (which define coding standards), skills enable task-specific workflows that can include scripts, examples, templates, and reference data.
Key characteristics:
- **Portable**: Works across VS Code, Copilot CLI, and Copilot coding agent
- **Progressive loading**: Only loaded when relevant to the user's request
- **Resource-bundled**: Can include scripts, templates, examples alongside instructions
- **On-demand**: Activated automatically based on prompt relevance
## Directory Structure
Skills are stored in specific locations:
| Location | Scope | Recommendation |
|----------|-------|----------------|
| `.github/skills/<skill-name>/` | Project/repository | Recommended for project skills |
| `.claude/skills/<skill-name>/` | Project/repository | Legacy, for backward compatibility |
| `~/.github/skills/<skill-name>/` | Personal (user-wide) | Recommended for personal skills |
| `~/.claude/skills/<skill-name>/` | Personal (user-wide) | Legacy, for backward compatibility |
Each skill **must** have its own subdirectory containing at minimum a `SKILL.md` file.
## Required SKILL.md Format
### Frontmatter (Required)
```yaml
---
name: webapp-testing
description: Toolkit for testing local web applications using Playwright. Use when asked to verify frontend functionality, debug UI behavior, capture browser screenshots, check for visual regressions, or view browser console logs. Supports Chrome, Firefox, and WebKit browsers.
license: Complete terms in LICENSE.txt
---
```
| Field | Required | Constraints |
|-------|----------|-------------|
| `name` | Yes | Lowercase, hyphens for spaces, max 64 characters (e.g., `webapp-testing`) |
| `description` | Yes | Clear description of capabilities AND use cases, max 1024 characters |
| `license` | No | Reference to LICENSE.txt (e.g., `Complete terms in LICENSE.txt`) or SPDX identifier |
### Description Best Practices
**CRITICAL**: The `description` field is the PRIMARY mechanism for automatic skill discovery. Copilot reads ONLY the `name` and `description` to decide whether to load a skill. If your description is vague, the skill will never be activated.
**What to include in description:**
1. **WHAT** the skill does (capabilities)
2. **WHEN** to use it (specific triggers, scenarios, file types, or user requests)
3. **Keywords** that users might mention in their prompts
**Good description:**
```yaml
description: Toolkit for testing local web applications using Playwright. Use when asked to verify frontend functionality, debug UI behavior, capture browser screenshots, check for visual regressions, or view browser console logs. Supports Chrome, Firefox, and WebKit browsers.
```
**Poor description:**
```yaml
description: Web testing helpers
```
The poor description fails because:
- No specific triggers (when should Copilot load this?)
- No keywords (what user prompts would match?)
- No capabilities (what can it actually do?)
### Body Content
The body contains detailed instructions that Copilot loads AFTER the skill is activated. Recommended sections:
| Section | Purpose |
|---------|---------|
| `# Title` | Brief overview of what this skill enables |
| `## When to Use This Skill` | List of scenarios (reinforces description triggers) |
| `## Prerequisites` | Required tools, dependencies, environment setup |
| `## Step-by-Step Workflows` | Numbered steps for common tasks |
| `## Troubleshooting` | Common issues and solutions table |
| `## References` | Links to bundled docs or external resources |
## Bundling Resources
Skills can include additional files that Copilot accesses on-demand:
### Supported Resource Types
| Folder | Purpose | Loaded into Context? | Example Files |
|--------|---------|---------------------|---------------|
| `scripts/` | Executable automation that performs specific operations | When executed | `helper.py`, `validate.sh`, `build.ts` |
| `references/` | Documentation the AI agent reads to inform decisions | Yes, when referenced | `api_reference.md`, `schema.md`, `workflow_guide.md` |
| `assets/` | **Static files used AS-IS** in output (not modified by the AI agent) | No | `logo.png`, `brand-template.pptx`, `custom-font.ttf` |
| `templates/` | **Starter code/scaffolds that the AI agent MODIFIES** and builds upon | Yes, when referenced | `viewer.html` (insert algorithm), `hello-world/` (extend) |
### Directory Structure Example
```
.github/skills/my-skill/
├── SKILL.md # Required: Main instructions
├── LICENSE.txt # Recommended: License terms (Apache 2.0 typical)
├── scripts/ # Optional: Executable automation
│ ├── helper.py # Python script
│ └── helper.ps1 # PowerShell script
├── references/ # Optional: Documentation loaded into context
│ ├── api_reference.md
│ ├── step1-setup.md # Detailed workflow (>3 steps)
│ └── step2-deployment.md
├── assets/ # Optional: Static files used AS-IS in output
│ ├── baseline.png # Reference image for comparison
│ └── report-template.html
└── templates/ # Optional: Starter code the AI agent modifies
├── scaffold.py # Code scaffold the AI agent customizes
└── config.template # Config template the AI agent fills in
```
> **LICENSE.txt**: When creating a skill, download the Apache 2.0 license text from https://www.apache.org/licenses/LICENSE-2.0.txt and save as `LICENSE.txt`. Update the copyright year and owner in the appendix section.
### Assets vs Templates: Key Distinction
**Assets** are static resources **consumed unchanged** in the output:
- A `logo.png` that gets embedded into a generated document
- A `report-template.html` copied as output format
- A `custom-font.ttf` applied to text rendering
**Templates** are starter code/scaffolds that **the AI agent actively modifies**:
- A `scaffold.py` where the AI agent inserts logic
- A `config.template` where the AI agent fills in values based on user requirements
- A `hello-world/` project directory that the AI agent extends with new features
**Rule of thumb**: If the AI agent reads and builds upon the file content → `templates/`. If the file is used as-is in output → `assets/`.
### Referencing Resources in SKILL.md
Use relative paths to reference files within the skill directory:
```markdown
## Available Scripts
Run the [helper script](./scripts/helper.py) to automate common tasks.
See [API reference](./references/api_reference.md) for detailed documentation.
Use the [scaffold](./templates/scaffold.py) as a starting point.
```
## Progressive Loading Architecture
Skills use three-level loading for efficiency:
| Level | What Loads | When |
|-------|------------|------|
| 1. Discovery | `name` and `description` only | Always (lightweight metadata) |
| 2. Instructions | Full `SKILL.md` body | When request matches description |
| 3. Resources | Scripts, examples, docs | Only when Copilot references them |
This means:
- Install many skills without consuming context
- Only relevant content loads per task
- Resources don't load until explicitly needed
## Content Guidelines
### Writing Style
- Use imperative mood: "Run", "Create", "Configure" (not "You should run")
- Be specific and actionable
- Include exact commands with parameters
- Show expected outputs where helpful
- Keep sections focused and scannable
### Script Requirements
When including scripts, prefer cross-platform languages:
| Language | Use Case |
|----------|----------|
| Python | Complex automation, data processing |
| pwsh | PowerShell Core scripting |
| Node.js | JavaScript-based tooling |
| Bash/Shell | Simple automation tasks |
Best practices:
- Include help/usage documentation (`--help` flag)
- Handle errors gracefully with clear messages
- Avoid storing credentials or secrets
- Use relative paths where possible
### When to Bundle Scripts
Include scripts in your skill when:
- The same code would be rewritten repeatedly by the agent
- Deterministic reliability is critical (e.g., file manipulation, API calls)
- Complex logic benefits from being pre-tested rather than generated each time
- The operation has a self-contained purpose that can evolve independently
- Testability matters — scripts can be unit tested and validated
- Predictable behavior is preferred over dynamic generation
Scripts enable evolution: even simple operations benefit from being implemented as scripts when they may grow in complexity, need consistent behavior across invocations, or require future extensibility.
### Security Considerations
- Scripts rely on existing credential helpers (no credential storage)
- Include `--force` flags only for destructive operations
- Warn users before irreversible actions
- Document any network operations or external calls
## Common Patterns
### Parameter Table Pattern
Document parameters clearly:
```markdown
| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `--input` | Yes | - | Input file or URL to process |
| `--action` | Yes | - | Action to perform |
| `--verbose` | No | `false` | Enable verbose output |
```
## Validation Checklist
Before publishing a skill:
- [ ] `SKILL.md` has valid frontmatter with `name` and `description`
- [ ] `name` is lowercase with hyphens, ≤64 characters
- [ ] `description` clearly states **WHAT** it does, **WHEN** to use it, and relevant **KEYWORDS**
- [ ] Body includes when to use, prerequisites, and step-by-step workflows
- [ ] SKILL.md body kept under 500 lines (split large content into `references/` folder)
- [ ] Large workflows (>5 steps) split into `references/` folder with clear links from SKILL.md
- [ ] Scripts include help documentation and error handling
- [ ] Relative paths used for all resource references
- [ ] No hardcoded credentials or secrets
## Workflow Execution Pattern
When executing multi-step workflows, create a TODO list where each step references the relevant documentation:
```markdown
## TODO
- [ ] Step 1: Configure environment - see [workflow-setup.md](./references/workflow-setup.md#environment)
- [ ] Step 2: Build project - see [workflow-setup.md](./references/workflow-setup.md#build)
- [ ] Step 3: Deploy to staging - see [workflow-deployment.md](./references/workflow-deployment.md#staging)
- [ ] Step 4: Run validation - see [workflow-deployment.md](./references/workflow-deployment.md#validation)
- [ ] Step 5: Deploy to production - see [workflow-deployment.md](./references/workflow-deployment.md#production)
```
This ensures traceability and allows resuming workflows if interrupted.
## Related Resources
- [Agent Skills Specification](https://agentskills.io/)
- [VS Code Agent Skills Documentation](https://code.visualstudio.com/docs/copilot/customization/agent-skills)
- [Reference Skills Repository](https://github.com/anthropics/skills)
- [Awesome Copilot Skills](https://github.com/github/awesome-copilot/blob/main/docs/README.skills.md)

View File

@@ -0,0 +1,61 @@
---
description: 'Guidelines for shared libraries including logging, IPC, settings, DPI, telemetry, and utilities consumed by multiple modules'
applyTo: 'src/common/**'
---
# Common Libraries Shared Code Guidance
Guidelines for modifying shared code in `src/common/`. Changes here can have wide-reaching impact across the entire PowerToys codebase.
## Scope
- Logging infrastructure (`src/common/logger/`)
- IPC primitives and named pipe utilities
- Settings serialization and management
- DPI awareness and scaling utilities
- Telemetry helpers
- General utilities (JSON parsing, string helpers, etc.)
## Guidelines
### API Stability
- Avoid breaking public headers/APIs; if changed, search & update all callers
- Coordinate ABI-impacting struct/class layout changes; keep binary compatibility
- When modifying public interfaces, grep the entire codebase for usages
### Performance
- Watch perf in hot paths (hooks, timers, serialization)
- Avoid avoidable allocations in frequently called code
- Profile changes that touch performance-sensitive areas
### Dependencies
- Ask before adding third-party deps or changing serialization formats
- New dependencies must be MIT-licensed or approved by PM team
- Add any new external packages to `NOTICE.md`
### Logging
- C++ logging uses spdlog (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`)
- Initialize with `init_logger()` early in startup
- Keep hot paths quiet no logging in tight loops or hooks
## Acceptance Criteria
- No unintended ABI breaks
- No noisy logs in hot paths
- New non-obvious symbols briefly commented
- All callers updated when interfaces change
## Code Style
- **C++**: Follow `.clang-format` in `src/`; use Modern C++ patterns per C++ Core Guidelines
- **C#**: Follow `src/.editorconfig`; enforce StyleCop.Analyzers
## Validation
- Build: `tools\build\build.cmd` from `src/common/` folder
- Verify no ABI breaks: grep for changed function/struct names across codebase
- Check logs: ensure no new logging in performance-critical paths

View File

@@ -0,0 +1,68 @@
---
description: 'Guidelines for Runner and Settings UI components that communicate via named pipes and manage module lifecycle'
applyTo: 'src/runner/**,src/settings-ui/**'
---
# Runner & Settings UI Core Components Guidance
Guidelines for modifying the Runner (tray/module loader) and Settings UI (configuration app). These components communicate via Windows Named Pipes using JSON messages.
## Runner (`src/runner/`)
### Scope
- Module bootstrap, hotkey management, settings bridge, update/elevation handling
### Guidelines
- If IPC/JSON contracts change, mirror updates in `src/settings-ui/**`
- Keep module discovery in `src/runner/main.cpp` in sync when adding/removing modules
- Keep startup lean: avoid blocking/network calls in early init path
- Preserve GPO & elevation behaviors; confirm no regression in policy handling
- Ask before modifying update workflow or elevation logic
### Acceptance Criteria
- Stable startup, consistent contracts, no unnecessary logging noise
## Settings UI (`src/settings-ui/`)
### Scope
- WinUI/WPF UI, communicates with Runner over named pipes; manages persisted settings schema
### Guidelines
- Don't break settings schema silently; add migration when shape changes
- If IPC/JSON contracts change, align with `src/runner/**` implementation
- Keep UI responsive: marshal to UI thread for UI-bound operations
- Reuse existing styles/resources; avoid duplicate theme keys
- Add/adjust migration or serialization tests when changing persisted settings
### Acceptance Criteria
- Schema integrity preserved, responsive UI, consistent contracts, no style duplication
## Shared Concerns
### IPC Contract Changes
When modifying the JSON message format between Runner and Settings UI:
1. Update both `src/runner/` and `src/settings-ui/` in the same PR
2. Preserve backward compatibility where possible
3. Add migration logic for settings schema changes
4. Test both directions of communication
### Code Style
- **C++ (Runner)**: Follow `.clang-format` in `src/`
- **C# (Settings UI)**: Follow `src/.editorconfig`, use StyleCop.Analyzers
- **XAML**: Use XamlStyler or run `.\.pipelines\applyXamlStyling.ps1 -Main`
## Validation
- Build Runner: `tools\build\build.cmd` from `src/runner/`
- Build Settings UI: `tools\build\build.cmd` from `src/settings-ui/`
- Test IPC: Launch both Runner and Settings UI, verify communication works
- Schema changes: Run serialization tests if settings shape changed

View File

@@ -0,0 +1,228 @@
---
description: 'Instructions for building Model Context Protocol (MCP) servers using the TypeScript SDK'
applyTo: '**/*.ts, **/*.js, **/package.json'
---
# TypeScript MCP Server Development
## Instructions
- Use the **@modelcontextprotocol/sdk** npm package: `npm install @modelcontextprotocol/sdk`
- Import from specific paths: `@modelcontextprotocol/sdk/server/mcp.js`, `@modelcontextprotocol/sdk/server/stdio.js`, etc.
- Use `McpServer` class for high-level server implementation with automatic protocol handling
- Use `Server` class for low-level control with manual request handlers
- Use **zod** for input/output schema validation: `npm install zod@3`
- Always provide `title` field for tools, resources, and prompts for better UI display
- Use `registerTool()`, `registerResource()`, and `registerPrompt()` methods (recommended over older APIs)
- Define schemas using zod: `{ inputSchema: { param: z.string() }, outputSchema: { result: z.string() } }`
- Return both `content` (for display) and `structuredContent` (for structured data) from tools
- For HTTP servers, use `StreamableHTTPServerTransport` with Express or similar frameworks
- For local integrations, use `StdioServerTransport` for stdio-based communication
- Create new transport instances per request to prevent request ID collisions (stateless mode)
- Use session management with `sessionIdGenerator` for stateful servers
- Enable DNS rebinding protection for local servers: `enableDnsRebindingProtection: true`
- Configure CORS headers and expose `Mcp-Session-Id` for browser-based clients
- Use `ResourceTemplate` for dynamic resources with URI parameters: `new ResourceTemplate('resource://{param}', { list: undefined })`
- Support completions for better UX using `completable()` wrapper from `@modelcontextprotocol/sdk/server/completable.js`
- Implement sampling with `server.server.createMessage()` to request LLM completions from clients
- Use `server.server.elicitInput()` to request additional user input during tool execution
- Enable notification debouncing for bulk updates: `debouncedNotificationMethods: ['notifications/tools/list_changed']`
- Dynamic updates: call `.enable()`, `.disable()`, `.update()`, or `.remove()` on registered items to emit `listChanged` notifications
- Use `getDisplayName()` from `@modelcontextprotocol/sdk/shared/metadataUtils.js` for UI display names
- Test servers with MCP Inspector: `npx @modelcontextprotocol/inspector`
## Best Practices
- Keep tool implementations focused on single responsibilities
- Provide clear, descriptive titles and descriptions for LLM understanding
- Use proper TypeScript types for all parameters and return values
- Implement comprehensive error handling with try-catch blocks
- Return `isError: true` in tool results for error conditions
- Use async/await for all asynchronous operations
- Close database connections and clean up resources properly
- Validate input parameters before processing
- Use structured logging for debugging without polluting stdout/stderr
- Consider security implications when exposing file system or network access
- Implement proper resource cleanup on transport close events
- Use environment variables for configuration (ports, API keys, etc.)
- Document tool capabilities and limitations clearly
- Test with multiple clients to ensure compatibility
## Common Patterns
### Basic Server Setup (HTTP)
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
const server = new McpServer({
name: 'my-server',
version: '1.0.0'
});
const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
res.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000);
```
### Basic Server Setup (stdio)
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({
name: 'my-server',
version: '1.0.0'
});
// ... register tools, resources, prompts ...
const transport = new StdioServerTransport();
await server.connect(transport);
```
### Simple Tool
```typescript
import { z } from 'zod';
server.registerTool(
'calculate',
{
title: 'Calculator',
description: 'Perform basic calculations',
inputSchema: { a: z.number(), b: z.number(), op: z.enum(['+', '-', '*', '/']) },
outputSchema: { result: z.number() }
},
async ({ a, b, op }) => {
const result = op === '+' ? a + b : op === '-' ? a - b :
op === '*' ? a * b : a / b;
const output = { result };
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output
};
}
);
```
### Dynamic Resource
```typescript
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
server.registerResource(
'user',
new ResourceTemplate('users://{userId}', { list: undefined }),
{
title: 'User Profile',
description: 'Fetch user profile data'
},
async (uri, { userId }) => ({
contents: [{
uri: uri.href,
text: `User ${userId} data here`
}]
})
);
```
### Tool with Sampling
```typescript
server.registerTool(
'summarize',
{
title: 'Text Summarizer',
description: 'Summarize text using LLM',
inputSchema: { text: z.string() },
outputSchema: { summary: z.string() }
},
async ({ text }) => {
const response = await server.server.createMessage({
messages: [{
role: 'user',
content: { type: 'text', text: `Summarize: ${text}` }
}],
maxTokens: 500
});
const summary = response.content.type === 'text' ?
response.content.text : 'Unable to summarize';
const output = { summary };
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output
};
}
);
```
### Prompt with Completion
```typescript
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
server.registerPrompt(
'review',
{
title: 'Code Review',
description: 'Review code with specific focus',
argsSchema: {
language: completable(z.string(), value =>
['typescript', 'python', 'javascript', 'java']
.filter(l => l.startsWith(value))
),
code: z.string()
}
},
({ language, code }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: `Review this ${language} code:\n\n${code}`
}
}]
})
);
```
### Error Handling
```typescript
server.registerTool(
'risky-operation',
{
title: 'Risky Operation',
description: 'An operation that might fail',
inputSchema: { input: z.string() },
outputSchema: { result: z.string() }
},
async ({ input }) => {
try {
const result = await performRiskyOperation(input);
const output = { result };
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output
};
} catch (err: unknown) {
const error = err as Error;
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
isError: true
};
}
}
);
```

View File

@@ -15,8 +15,14 @@ For **#{{issue_number}}** produce:
Figure out required inputs {{issue_number}} from the invocation context; if anything is missing, ask for the value or note it as a gap.
# CONTEXT (brief)
Ground evidence using `gh issue view {{issue_number}} --json number,title,body,author,createdAt,updatedAt,state,labels,milestone,reactions,comments,linkedPullRequests`, and download images to better understand the issue context.
Locate source code in the current workspace; feel free to use `rg`/`git grep`. Link related issues and PRs.
Ground evidence using `gh issue view {{issue_number}} --json number,title,body,author,createdAt,updatedAt,state,labels,milestone,reactions,comments,linkedPullRequests`, download images via MCP `github_issue_images` to better understand the issue context. Finally, use MCP `github_issue_attachments` to download logs with parameter `extractFolder` as `Generated Files/issueReview/{{issue_number}}/logs`, and analyze the downloaded logs if available to identify relevant issues. Locate the source code in the current workspace (use `rg`/`git grep` as needed). Link related issues and PRs.
## When to call MCP tools
If the following MCP "github-artifacts" tools are available in the environment, use them:
- `github_issue_images`: use when the issue/PR likely contains screenshots or other visual evidence (UI bugs, glitches, design problems).
- `github_issue_attachments`: use when the issue/PR mentions attached ZIPs (PowerToysReport_*.zip, logs.zip, debug.zip) or asks to analyze logs/diagnostics. Always provide `extractFolder` as `Generated Files/issueReview/{{issue_number}}/logs`
If these tools are not available (not listed by the runtime), start the MCP server "github-artifacts" first.
# OVERVIEW.MD
## Summary

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Microsoft Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,132 @@
---
name: release-note-generation
description: Toolkit for generating PowerToys release notes from GitHub milestone PRs or commit ranges. Use when asked to create release notes, summarize milestone PRs, generate changelog, prepare release documentation, request Copilot reviews for PRs, update README for a new release, manage PR milestones, or collect PRs between commits/tags. Supports PR collection by milestone or commit range, milestone assignment, grouping by label, summarization with external contributor attribution, and README version bumping.
license: Complete terms in LICENSE.txt
---
# Release Note Generation Skill
Generate professional release notes for PowerToys milestones by collecting merged PRs, requesting Copilot code reviews, grouping by label, and producing user-facing summaries.
## Output Directory
All generated artifacts are placed under `Generated Files/ReleaseNotes/` at the repository root (gitignored).
```
Generated Files/ReleaseNotes/
├── milestone_prs.json # Raw PR data from GitHub
├── sorted_prs.csv # Sorted PR list with Copilot summaries
├── prs_with_milestone.csv # Milestone assignment tracking
├── grouped_csv/ # PRs grouped by label (one CSV per label)
├── grouped_md/ # Generated markdown summaries per label
└── v{VERSION}-release-notes.md # Final consolidated release notes
```
## When to Use This Skill
- Generate release notes for a milestone
- Summarize PRs merged in a release
- Request Copilot reviews for milestone PRs
- Assign milestones to PRs missing them
- Collect PRs between two commits/tags
- Update README.md for a new version
## Prerequisites
- GitHub CLI (`gh`) installed and authenticated
- MCP Server: github-mcp-server installed
- GitHub Copilot code review enabled for the org/repo
## Required Variables
⚠️ **Before starting**, confirm `{{ReleaseVersion}}` with the user. If not provided, **ASK**: "What release version are we generating notes for? (e.g., 0.98)"
| Variable | Description | Example |
|----------|-------------|---------|
| `{{ReleaseVersion}}` | Target release version | `0.98` |
## Workflow Overview
```
┌────────────────────────────────┐
│ 1.1 Collect PRs (stable range) │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 1.2 Assign Milestones │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 2.12.4 Label PRs (auto+human) │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 3.1 Request Reviews (Copilot) │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 3.2 Refresh PR data │
│ (CopilotSummary) │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 3.3 Group by label │
│ (grouped_csv) │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 4.1 Summarize (grouped_md) │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 4.2 Final notes (v{VERSION}.md) │
└────────────────────────────────┘
```
| Step | Action | Details |
|------|--------|---------|
| 1.1 | Collect PRs | From previous release tag on `stable` branch → `sorted_prs.csv` |
| 1.2 | Assign Milestones | Ensure all PRs have correct milestone |
| 2.12.4 | Label PRs | Auto-suggest + human label low-confidence |
| 3.13.3 | Reviews & Grouping | Request Copilot reviews → refresh → group by label |
| 4.14.2 | Summaries & Final | Generate grouped summaries, then consolidate |
## Detailed workflow docs
Do not read all steps at once—only read the step you are executing.
- [Step 1: Collection & Milestones](./references/step1-collection.md)
- [Step 2: Labeling PRs](./references/step2-labeling.md)
- [Step 3: Reviews & Grouping](./references/step3-review-grouping.md)
- [Step 4: Summarization](./references/step4-summarization.md)
## Available Scripts
| Script | Purpose |
|--------|---------|
| [dump-prs-since-commit.ps1](./scripts/dump-prs-since-commit.ps1) | Fetch PRs between commits/tags |
| [group-prs-by-label.ps1](./scripts/group-prs-by-label.ps1) | Group PRs into CSVs |
| [collect-or-apply-milestones.ps1](./scripts/collect-or-apply-milestones.ps1) | Assign milestones |
| [diff_prs.ps1](./scripts/diff_prs.ps1) | Incremental PR diff |
## References
- [Sample Output](./references/SampleOutput.md) - Example summary formatting
- [Detailed Instructions](./references/Instruction.md) - Legacy full documentation
## Conventions
- **Terminal usage**: Disabled by default; only run scripts when user explicitly requests
- **Batch generation**: Generate ALL grouped_md files in one pass, then human reviews
- **PR order**: Preserve order from `sorted_prs.csv` in all outputs
- **Label filtering**: Keeps `Product-*`, `Area-*`, `GitHub*`, `*Plugin`, `Issue-*`
## Troubleshooting
| Issue | Solution |
|-------|----------|
| `gh` command not found | Install GitHub CLI and add to PATH |
| No PRs returned | Verify milestone title matches exactly |
| Empty CopilotSummary | Request Copilot reviews first, then re-run dump |
| Many unlabeled PRs | Return to labeling step before grouping |

View File

@@ -0,0 +1,9 @@
- Added mouse button actions so you can choose what left, right, or middle click does. Thanks [@PesBandi](https://github.com/PesBandi)!
- Aligned window styling with current Windows theme for a cleaner look. Thanks [@sadirano](https://github.com/sadirano)!
- Ensured screen readers are notified when the selected item in the list changes for better accessibility.
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours.
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)!

View File

@@ -0,0 +1,143 @@
# Step 1: Collection and Milestones
## 1.0 To-do
- 1.0.1 Generate MemberList.md (REQUIRED)
- 1.1 Collect PRs
- 1.2 Assign Milestones (REQUIRED)
## Required Variables
⚠️ **Before starting**, confirm these values with the user:
| Variable | Description | Example |
|----------|-------------|---------|
| `{{ReleaseVersion}}` | Target release version | `0.97` |
| `{{PreviousReleaseTag}}` | Previous release tag from releases page | `v0.96.1` |
**If user hasn't specified `{{ReleaseVersion}}`, ASK:** "What release version are we generating notes for? (e.g., 0.97)"
**`{{PreviousReleaseTag}}` is derived from the releases page, not user input.** Use the latest published release tag (top of the page). You will use its tag name and tag commit SHA in Step 1.
---
## 1.0.1 Generate MemberList.md (REQUIRED)
Create `Generated Files/ReleaseNotes/MemberList.md` from the **PowerToys core team** section in [COMMUNITY.md](../../../COMMUNITY.md).
Rules:
- One GitHub username per line, **no** `@` prefix.
- Use the usernames exactly as listed in the core team section.
- Do not include former team members or other sections.
Example (format only):
```
example-user
another-user
```
---
## 1.1 Collect PRs
### 1.1.1 Get the previous release commit
1. Open the [PowerToys releases page](https://github.com/microsoft/PowerToys/releases/)
2. Find the latest release (e.g., v0.96.1, which should be at the top)
3. Set `{{PreviousReleaseTag}}` to that tag name (e.g., `v0.96.1`)
4. Copy the full tag commit SHA as `{{SHALastRelease}}`
**If the release SHA is not in your branch history:** Use the helper script to find an equivalent commit on the target branch by matching the commit title:
```powershell
pwsh ./.github/skills/release-note-generation/scripts/find-commit-by-title.ps1 `
-Commit '{{SHALastRelease}}' `
-Branch 'stable'
```
### 1.1.2 Run collection script against stable branch
```powershell
# Collect PRs from previous release to current HEAD of stable branch
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
-StartCommit '{{SHALastRelease}}' `
-Branch 'stable' `
-OutputDir 'Generated Files/ReleaseNotes'
```
**Parameters:**
- `-StartCommit` - Previous release tag or commit SHA (exclusive)
- `-Branch` - Always use `stable` branch, not `main` (script uses `origin/stable` as the end ref)
- `-EndCommit` - Optional override if you need a custom end ref
- `-OutputDir` - Output directory for generated files
**Reliability check:** If the script reports “No commits found”, the stable branch has not moved since the last release. In that case, either:
- Confirm this is expected and stop (no new release notes), or
- Re-run against `main` to gather pending changes for the next release cycle.
The script detects both merge commits (`Merge pull request #12345`) and squash commits (`Feature (#12345)`).
**Output** (in `Generated Files/ReleaseNotes/`):
- `milestone_prs.json` - raw PR data
- `sorted_prs.csv` - sorted PR list with columns: Id, Title, Labels, Author, Url, Body, CopilotSummary, NeedThanks
---
## 1.2 Assign Milestones (REQUIRED)
**Before generating release notes**, ensure all collected PRs have the correct milestone assigned.
⚠️ **CRITICAL:** Do NOT proceed to labeling until all PRs have milestones assigned.
### 1.2.1 Check current milestone status (dry run)
```powershell
# Dry run first to see what would be changed:
pwsh ./.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1 `
-InputCsv 'Generated Files/ReleaseNotes/sorted_prs.csv' `
-OutputCsv 'Generated Files/ReleaseNotes/prs_with_milestone.csv' `
-DefaultMilestone 'PowerToys {{ReleaseVersion}}' `
-ApplyMissing -WhatIf
```
This queries GitHub for each PR's current milestone and shows which PRs would be updated.
### 1.2.2 Apply milestones to PRs missing them
```powershell
# Apply for real:
pwsh ./.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1 `
-InputCsv 'Generated Files/ReleaseNotes/sorted_prs.csv' `
-OutputCsv 'Generated Files/ReleaseNotes/prs_with_milestone.csv' `
-DefaultMilestone 'PowerToys {{ReleaseVersion}}' `
-ApplyMissing
```
**Script Behavior:**
- Queries each PR's current milestone from GitHub
- PRs that already have a milestone are **skipped** (not overwritten)
- PRs missing a milestone get the default milestone applied
- Outputs `prs_with_milestone.csv` with (Id, Milestone) columns
- Produces summary: `Updated=X Skipped=Y Failed=Z`
**Validation:** After assignment, all PRs in `prs_with_milestone.csv` should have the target milestone.
---
## Additional Commands
### Collect milestones only (no changes to GitHub)
```powershell
pwsh ./.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1 `
-InputCsv 'Generated Files/ReleaseNotes/sorted_prs.csv' `
-OutputCsv 'Generated Files/ReleaseNotes/prs_with_milestone.csv'
```
### Local assignment only (fill blanks in CSV, no GitHub changes)
```powershell
pwsh ./.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1 `
-InputCsv 'Generated Files/ReleaseNotes/sorted_prs.csv' `
-OutputCsv 'Generated Files/ReleaseNotes/prs_with_milestone.csv' `
-DefaultMilestone 'PowerToys {{ReleaseVersion}}' `
-LocalAssign
```

View File

@@ -0,0 +1,131 @@
# Step 2: Label Unlabeled PRs
## 2.0 To-do
- 2.1 Identify unlabeled PRs (Agent Mode)
- 2.2 Suggest labels (Agent Mode)
- 2.3 Human label low-confidence PRs
- 2.4 Recheck labels, delete Unlabeled.csv, and re-collect
**Before grouping**, ensure all PRs have appropriate labels for categorization.
⚠️ **CRITICAL:** Do NOT proceed to grouping until all PRs have labels assigned. PRs without labels will end up in `Unlabeled.csv` and won't appear in the correct release note sections.
## 2.1 Identify unlabeled PRs (Agent Mode)
Read `sorted_prs.csv` and identify PRs with empty or missing `Labels` column.
For each unlabeled PR, analyze:
- **Title** - Often contains module name or feature
- **Body** - PR description with context
- **CopilotSummary** - AI-generated summary of changes
## 2.2 Suggest labels (Agent Mode)
For each unlabeled PR, suggest an appropriate label based on the content analysis.
**Output:** Create `Generated Files/ReleaseNotes/prs_label_review.md` with the following format:
```markdown
# PR Label Review
Generated: YYYY-MM-DD HH:mm:ss
## Summary
- Total unlabeled PRs: X
- High confidence: X
- Medium confidence: X
- Low confidence: X
---
## PRs Needing Review (sorted by confidence, low first)
| PR | Title | Suggested Label | Confidence | Reason |
|----|-------|-----------------|------------|--------|
| [#12347](url) | Some generic fix | ??? | Low | Unclear from content |
| [#12346](url) | Update dependencies | `Area-Build` | Medium | Body mentions NuGet packages |
```
Sort by confidence (low first) so human reviews uncertain ones first.
After writing `prs_label_review.md`, **generate `prs_to_label.csv`, apply labels, and re-run collection** so the CSV/labels stay in sync:
```powershell
# Generate CSV from suggestions (agent)
# Apply labels
pwsh ./.github/skills/release-note-generation/scripts/apply-labels.ps1 `
-InputCsv 'Generated Files/ReleaseNotes/prs_to_label.csv'
# Refresh collection
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
-StartCommit '{{PreviousReleaseTag}}' -Branch 'stable' `
-OutputDir 'Generated Files/ReleaseNotes'
```
## 2.3 Human label low-confidence PRs
Ask the human to label **low-confidence** PRs directly (in GitHub). Skip any they decide not to label.
## 2.4 Recheck labels, delete Unlabeled.csv, and re-collect
Recheck that all PRs now have labels. Delete `Unlabeled.csv` (if present), then re-run the collection script to update `sorted_prs.csv`:
```powershell
# Remove stale unlabeled output if it exists
Remove-Item 'Generated Files/ReleaseNotes/Unlabeled.csv' -ErrorAction SilentlyContinue
```
```powershell
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
-StartCommit '{{PreviousReleaseTag}}' -Branch 'stable' `
-OutputDir 'Generated Files/ReleaseNotes'
```
---
## Common Label Mappings
| Keywords/Patterns | Suggested Label |
| ----------------- | --------------- |
| Advanced Paste, AP, clipboard, paste | `Product-Advanced Paste` |
| CmdPal, Command Palette, cmdpal | `Product-Command Palette` |
| FancyZones, zones, layout | `Product-FancyZones` |
| ZoomIt, zoom, screen annotation | `Product-ZoomIt` |
| Settings, settings-ui, Quick Access, flyout | `Product-Settings` |
| Installer, setup, MSI, MSIX, WiX | `Area-Setup/Install` |
| Build, pipeline, CI/CD, msbuild | `Area-Build` |
| Test, unit test, UI test, fuzz | `Area-Tests` |
| Localization, loc, translation, resw | `Area-Localization` |
| Foundry, AI, LLM | `Product-Advanced Paste` (AI features) |
| Mouse Without Borders, MWB | `Product-Mouse Without Borders` |
| PowerRename, rename, regex | `Product-PowerRename` |
| Peek, preview, file preview | `Product-Peek` |
| Image Resizer, resize | `Product-Image Resizer` |
| LightSwitch, theme, dark mode | `Product-LightSwitch` |
| Quick Accent, accent, diacritics | `Product-Quick Accent` |
| Awake, keep awake, caffeine | `Product-Awake` |
| ColorPicker, color picker, eyedropper | `Product-ColorPicker` |
| Hosts, hosts file | `Product-Hosts` |
| Keyboard Manager, remap | `Product-Keyboard Manager` |
| Mouse Highlighter | `Product-Mouse Highlighter` |
| Mouse Jump | `Product-Mouse Jump` |
| Find My Mouse | `Product-Find My Mouse` |
| Mouse Pointer Crosshairs | `Product-Mouse Pointer Crosshairs` |
| Shortcut Guide | `Product-Shortcut Guide` |
| Text Extractor, OCR, PowerOCR | `Product-Text Extractor` |
| Workspaces | `Product-Workspaces` |
| File Locksmith | `Product-File Locksmith` |
| Crop And Lock | `Product-CropAndLock` |
| Environment Variables | `Product-Environment Variables` |
| New+ | `Product-New+` |
## Label Filtering Rules
The grouping script keeps labels matching these patterns:
- `Product-*`
- `Area-*`
- `GitHub*`
- `*Plugin`
- `Issue-*`
Other labels are ignored for grouping purposes.

View File

@@ -0,0 +1,37 @@
# Step 3: Copilot Reviews and Grouping
## 3.0 To-do
- 3.1 Request Copilot Reviews (Agent Mode)
- 3.2 Refresh PR Data
- 3.3 Group PRs by Label
## 3.1 Request Copilot Reviews (Agent Mode)
Use MCP tools to request Copilot reviews for all PRs in `Generated Files/ReleaseNotes/sorted_prs.csv`:
- Use `mcp_github_request_copilot_review` for each PR ID
- Do NOT generate or run scripts for this step
---
## 3.2 Refresh PR Data
Re-run the collection script to capture Copilot review summaries into the `CopilotSummary` column:
```powershell
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
-StartCommit '{{PreviousReleaseTag}}' -Branch 'stable' `
-OutputDir 'Generated Files/ReleaseNotes'
```
---
## 3.3 Group PRs by Label
```powershell
pwsh ./.github/skills/release-note-generation/scripts/group-prs-by-label.ps1 -CsvPath 'Generated Files/ReleaseNotes/sorted_prs.csv' -OutDir 'Generated Files/ReleaseNotes/grouped_csv'
```
Creates `Generated Files/ReleaseNotes/grouped_csv/` with one CSV per label combination.
**Validation:** The `Unlabeled.csv` file should be minimal (ideally empty). If many PRs remain unlabeled, return to Step 2 (see [step2-labeling.md](./step2-labeling.md)).

View File

@@ -0,0 +1,88 @@
# Step 4: Summaries and Final Release Notes
## 4.0 To-do
- 4.1 Generate Summary Markdown (Agent Mode)
- 4.2 Produce Final Release Notes File
## 4.1 Generate Summary Markdown (Agent Mode)
For each CSV in `Generated Files/ReleaseNotes/grouped_csv/`, create a markdown file in `Generated Files/ReleaseNotes/grouped_md/`.
⚠️ **IMPORTANT:** Generate **ALL** markdown files first. Do NOT pause between files or ask for feedback during generation. Complete the entire batch, then human reviews afterwards.
### Structure per file
**1. Bullet list** - one concise, user-facing line per PR:
- Use the “Verb-ed + Scenario + Impact” sentence structure—make readers think, “Thats exactly what I need” or “Yes, thats an awesome fix.”; The "impact" can be end-user focused (written to convey user excitement) or technical (performance/stability) when user-facing impact is minimal.
- If nothing special on impact or unclear impact, mark as needing human summary
- Source from Title, Body, and CopilotSummary (prefer CopilotSummary when available)
- If the column `NeedThanks` in CSV is `True`, append: `Thanks [@Author](https://github.com/Author)!`
- Do NOT include PR numbers in bullet lines
- Do NOT mention “security” or “privacy” issues, since these are not known and could be leveraged by attackers in earlier versions. Instead, describe the user-facing scenario, usage, or impact.
- If confidence < 70%, write: `Human Summary Needed: <PR full link>`
**See [SampleOutput.md](./SampleOutput.md) for examples of well-written bullet summaries.**
**2. Three-column table** (same PR order):
- Column 1: Concise summary (same as bullet)
- Column 2: PR link `[#ID](URL)`
- Column 3: Confidence level (High/Medium/Low)
### Review Process (AFTER all files generated)
- Human reviews each `grouped_md/*.md` file and requests rewrites as needed
- Human may say "rewrite Product-X" or "combine these bullets"—apply changes to that specific file
- Do NOT interrupt generation to ask for feedback
---
## 4.2 Produce Final Release Notes File
Once all `grouped_md/*.md` files are reviewed and approved, consolidate into a single release notes file.
**Output:** `Generated Files/ReleaseNotes/v{{ReleaseVersion}}-release-notes.md`
### Structure
**1. Highlights section** (top):
- 8-12 bullets covering the most user-visible features and impactful fixes
- Pattern: `**Module**: brief description`
- Avoid internal refactors; focus on what users will notice
**2. Module sections** (alphabetical order):
- One section per product (Advanced Paste, Awake, Command Palette, etc.)
- Migrate bullet summaries from the approved `grouped_md/Product-*.md` files
- One section 'Development' for all the rest summaries from the approved `grouped_md/Area-*.md` files
- Re-review E2E, group release improvements by section, and move the most important items to the top of each section.
Some items in the Development section may overlap and should be moved to the Module section where more applicable.
### Example Final Structure
```markdown
# PowerToys v{{ReleaseVersion}} Release Notes
## Highlights
- **Command Palette**: Added theme customization and drag-and-drop support
- **Advanced Paste**: Image input for AI, color detection in clipboard history
- **FancyZones**: New CLI tool for command-line layout management
...
---
## Advanced Paste
- Wrapped paste option lists in a single ScrollViewer
- Added image input handling for AI-powered transformations
...
## Awake
- Fixed timed mode expiration. Thanks [@daverayment](https://github.com/daverayment)!
...
---
## Development
...
```

View File

@@ -0,0 +1,90 @@
<#
.SYNOPSIS
Apply labels to PRs from a CSV file.
.DESCRIPTION
Reads a CSV with Id and Label columns and applies the specified label to each PR via GitHub CLI.
Supports dry-run mode to preview changes before applying.
.PARAMETER InputCsv
CSV file with Id and Label columns. Default: prs_to_label.csv
.PARAMETER Repo
GitHub repository (owner/name). Default: microsoft/PowerToys
.PARAMETER WhatIf
Dry run - show what would be applied without making changes.
.EXAMPLE
pwsh ./apply-labels.ps1 -InputCsv 'Generated Files/ReleaseNotes/prs_to_label.csv'
.EXAMPLE
pwsh ./apply-labels.ps1 -InputCsv 'Generated Files/ReleaseNotes/prs_to_label.csv' -WhatIf
.NOTES
Requires: gh CLI authenticated with repo write access.
Input CSV format:
Id,Label
12345,Product-Advanced Paste
12346,Product-Settings
#>
[CmdletBinding()] param(
[Parameter(Mandatory=$false)][string]$InputCsv = 'prs_to_label.csv',
[Parameter(Mandatory=$false)][string]$Repo = 'microsoft/PowerToys',
[switch]$WhatIf
)
$ErrorActionPreference = 'Stop'
function Write-Info($m){ Write-Host "[info] $m" -ForegroundColor Cyan }
function Write-Warn($m){ Write-Host "[warn] $m" -ForegroundColor Yellow }
function Write-Err($m){ Write-Host "[error] $m" -ForegroundColor Red }
function Write-OK($m){ Write-Host "[ok] $m" -ForegroundColor Green }
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { Write-Err "GitHub CLI 'gh' not found in PATH"; exit 1 }
if (-not (Test-Path -LiteralPath $InputCsv)) { Write-Err "Input CSV not found: $InputCsv"; exit 1 }
$rows = Import-Csv -LiteralPath $InputCsv
if (-not $rows) { Write-Info "No rows in CSV."; exit 0 }
$firstCols = $rows[0].PSObject.Properties.Name
if (-not ($firstCols -contains 'Id' -and $firstCols -contains 'Label')) {
Write-Err "CSV must contain 'Id' and 'Label' columns"; exit 1
}
Write-Info "Processing $($rows.Count) label assignments..."
if ($WhatIf) { Write-Warn "DRY RUN - no changes will be made" }
$applied = 0
$skipped = 0
$failed = 0
foreach ($row in $rows) {
$id = $row.Id
$label = $row.Label
if ([string]::IsNullOrWhiteSpace($id) -or [string]::IsNullOrWhiteSpace($label)) {
Write-Warn "Skipping row with empty Id or Label"
$skipped++
continue
}
if ($WhatIf) {
Write-Info "Would apply label '$label' to PR #$id"
$applied++
continue
}
try {
gh pr edit $id --repo $Repo --add-label $label 2>&1 | Out-Null
Write-OK "Applied '$label' to PR #$id"
$applied++
} catch {
Write-Warn "Failed to apply label to PR #${id}: $_"
$failed++
}
}
Write-Info ""
Write-Info "Summary: Applied=$applied Skipped=$skipped Failed=$failed"

View File

@@ -0,0 +1,172 @@
<#
.SYNOPSIS
Collect existing PR milestones or (optionally) assign/apply a milestone to missing PRs in one script.
.DESCRIPTION
This unified script merges the behaviors of the previous add-milestone-column (collector) and
set-milestones-missing (remote updater) scripts.
Modes (controlled by switches):
1. Collect (default) For each PR Id in the input CSV, queries GitHub for the current milestone and
outputs a two-column CSV (Id,Milestone) leaving blanks where none are set.
2. LocalAssign Same as Collect, but for rows that end up blank assigns the value of -DefaultMilestone
in memory (does NOT touch GitHub). Useful for quickly preparing a fully populated CSV.
3. ApplyMissing After determining which PRs have no milestone, call GitHub API to set their milestone
to -DefaultMilestone. Requires milestone to already exist (open). Network + write.
You can combine LocalAssign and ApplyMissing: the remote update uses the existing live state; LocalAssign only
affects the output CSV/pipeline objects.
.PARAMETER InputCsv
Source CSV with at least an Id column. Default: sorted_prs.csv
.PARAMETER OutputCsv
Destination CSV for collected (and optionally locally assigned) milestones. Default: prs_with_milestone.csv
.PARAMETER Repo
GitHub repository (owner/name). Default: microsoft/PowerToys
.PARAMETER DefaultMilestone
Milestone title used when -LocalAssign or -ApplyMissing is specified. Default: 'PowerToys 0.97'
.PARAMETER Offline
Skip ALL GitHub lookups / updates. Implies Collect-only with all Milestone cells blank (unless LocalAssign).
.PARAMETER LocalAssign
Populate empty Milestone cells in the output with -DefaultMilestone (does not modify GitHub).
.PARAMETER ApplyMissing
For PRs which currently have no milestone (live on GitHub), set them to -DefaultMilestone via Issues API.
.PARAMETER WhatIf
Dry run for ApplyMissing: show intended remote changes without performing PATCH requests.
.EXAMPLE
# Collect only
pwsh ./collect-or-apply-milestones.ps1
.EXAMPLE
# Collect and fill blanks locally in the output only
pwsh ./collect-or-apply-milestones.ps1 -LocalAssign
.EXAMPLE
# Collect and remotely apply milestone to missing PRs
pwsh ./collect-or-apply-milestones.ps1 -ApplyMissing
.EXAMPLE
# Dry run remote application
pwsh ./collect-or-apply-milestones.ps1 -ApplyMissing -WhatIf
.EXAMPLE
# Offline local assignment
pwsh ./collect-or-apply-milestones.ps1 -Offline -LocalAssign -DefaultMilestone 'PowerToys 0.96'
.NOTES
Requires gh CLI unless -Offline AND -ApplyMissing not specified.
Remote apply path queries milestones to resolve numeric ID.
#>
[CmdletBinding()] param(
[Parameter(Mandatory=$false)][string]$InputCsv = 'sorted_prs.csv',
[Parameter(Mandatory=$false)][string]$OutputCsv = 'prs_with_milestone.csv',
[Parameter(Mandatory=$false)][string]$Repo = 'microsoft/PowerToys',
[Parameter(Mandatory=$false)][string]$DefaultMilestone = 'PowerToys 0.97',
[switch]$Offline,
[switch]$LocalAssign,
[switch]$ApplyMissing,
[switch]$WhatIf
)
$ErrorActionPreference = 'Stop'
function Write-Info($m){ Write-Host "[info] $m" -ForegroundColor Cyan }
function Write-Warn($m){ Write-Host "[warn] $m" -ForegroundColor Yellow }
function Write-Err($m){ Write-Host "[error] $m" -ForegroundColor Red }
if (-not (Test-Path -LiteralPath $InputCsv)) { Write-Err "Input CSV not found: $InputCsv"; exit 1 }
$rows = Import-Csv -LiteralPath $InputCsv
if (-not $rows) { Write-Warn "Input CSV has no rows."; @() | Export-Csv -NoTypeInformation -LiteralPath $OutputCsv; exit 0 }
if (-not ($rows[0].PSObject.Properties.Name -contains 'Id')) { Write-Err "Input CSV missing 'Id' column."; exit 1 }
$needGh = (-not $Offline) -and ($ApplyMissing -or -not $Offline)
if ($needGh -and -not (Get-Command gh -ErrorAction SilentlyContinue)) { Write-Err "GitHub CLI 'gh' not found. Use -Offline or install gh."; exit 1 }
# Step 1: Collect current milestone titles
$milestoneCache = @{}
$collected = New-Object System.Collections.Generic.List[object]
$idx = 0
foreach ($row in $rows) {
$idx++
$id = $row.Id
if (-not $id) { Write-Warn "Row $idx missing Id; skipping"; continue }
$ms = ''
if (-not $Offline) {
if ($milestoneCache.ContainsKey($id)) { $ms = $milestoneCache[$id] }
else {
try {
$json = gh pr view $id --repo $Repo --json milestone 2>$null | ConvertFrom-Json
if ($json -and $json.milestone -and $json.milestone.title) { $ms = $json.milestone.title }
} catch {
Write-Warn "Failed to fetch PR #$id milestone: $_"
}
$milestoneCache[$id] = $ms
}
}
$collected.Add([PSCustomObject]@{ Id = $id; Milestone = $ms }) | Out-Null
}
# Step 2: Remote apply (if requested)
$applySummary = @()
if ($ApplyMissing) {
if ($Offline) { Write-Err "Cannot use -ApplyMissing with -Offline."; exit 1 }
Write-Info "Resolving milestone id for '$DefaultMilestone' ..."
$milestonesRaw = gh api repos/$Repo/milestones --paginate --jq '.[] | {number,title,state}'
$msObj = $milestonesRaw | ConvertFrom-Json | Where-Object { $_.title -eq $DefaultMilestone -and $_.state -eq 'open' } | Select-Object -First 1
if (-not $msObj) { Write-Err "Milestone '$DefaultMilestone' not found/open."; exit 1 }
$msNumber = $msObj.number
$targets = $collected | Where-Object { [string]::IsNullOrWhiteSpace($_.Milestone) }
Write-Info ("ApplyMissing: {0} PR(s) without milestone." -f $targets.Count)
foreach ($t in $targets) {
$id = $t.Id
try {
# Verify still missing live
$current = gh pr view $id --repo $Repo --json milestone --jq '.milestone.title // ""'
if ($current) {
$applySummary += [PSCustomObject]@{ Id=$id; Action='Skip (already has)'; Milestone=$current; Status='OK' }
continue
}
if ($WhatIf) {
$applySummary += [PSCustomObject]@{ Id=$id; Action='Would set'; Milestone=$DefaultMilestone; Status='DRY RUN' }
continue
}
gh api -X PATCH -H 'Accept: application/vnd.github+json' repos/$Repo/issues/$id -f milestone=$msNumber | Out-Null
$applySummary += [PSCustomObject]@{ Id=$id; Action='Set'; Milestone=$DefaultMilestone; Status='OK' }
# Reflect in collected object for CSV output if not LocalAssign already doing so
$t.Milestone = $DefaultMilestone
} catch {
$errText = $_ | Out-String
$applySummary += [PSCustomObject]@{ Id=$id; Action='Failed'; Milestone=$DefaultMilestone; Status=$errText.Trim() }
Write-Warn ("Failed to set milestone for PR #{0}: {1}" -f $id, ($errText.Trim()))
}
}
}
# Step 3: Local assignment (purely for output) AFTER remote so remote actual result not overwritten accidentally
if ($LocalAssign) {
foreach ($item in $collected) {
if ([string]::IsNullOrWhiteSpace($item.Milestone)) { $item.Milestone = $DefaultMilestone }
}
}
# Step 4: Export CSV
$collected | Export-Csv -LiteralPath $OutputCsv -NoTypeInformation -Encoding UTF8
Write-Info ("Wrote collected CSV -> {0}" -f (Resolve-Path -LiteralPath $OutputCsv))
# Step 5: Summaries
if ($ApplyMissing) {
$updated = ($applySummary | Where-Object { $_.Action -eq 'Set' }).Count
$skipped = ($applySummary | Where-Object { $_.Action -like 'Skip*' }).Count
$failed = ($applySummary | Where-Object { $_.Action -eq 'Failed' }).Count
Write-Info ("ApplyMissing summary: Updated={0} Skipped={1} Failed={2}" -f $updated, $skipped, $failed)
}
# Emit objects (final collected set)
return $collected

View File

@@ -0,0 +1,100 @@
<#
.SYNOPSIS
Produce an incremental PR CSV containing rows present in a newer full export but absent from a baseline export.
.DESCRIPTION
Compares two previously generated sorted PR CSV files (same schema). Any row whose key column value
(defaults to 'Number') does not exist in the baseline file is emitted to a new incremental CSV, preserving
the original column order. If no new rows are found, an empty CSV (with headers when determinable) is written.
.PARAMETER BaseCsv
Path to the baseline (earlier) PR CSV.
.PARAMETER AllCsv
Path to the newer full PR CSV containing superset (or equal set) of rows.
.PARAMETER OutCsv
Path to write the incremental CSV containing only new rows.
.PARAMETER Key
Column name used as unique identifier (defaults to 'Number'). Must exist in both CSVs.
.EXAMPLE
pwsh ./diff_prs.ps1 -BaseCsv sorted_prs_prev.csv -AllCsv sorted_prs.csv -OutCsv sorted_prs_incremental.csv
.NOTES
Requires: PowerShell 7+, both CSVs with identical column schemas.
Exit code 0 on success (even if zero incremental rows). Throws on missing files.
#>
[CmdletBinding()] param(
[Parameter(Mandatory=$false)][string]$BaseCsv = "./sorted_prs_93_round1.csv",
[Parameter(Mandatory=$false)][string]$AllCsv = "./sorted_prs.csv",
[Parameter(Mandatory=$false)][string]$OutCsv = "./sorted_prs_93_incremental.csv",
[Parameter(Mandatory=$false)][string]$Key = "Number"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-Info($m) { Write-Host "[info] $m" -ForegroundColor Cyan }
function Write-Warn($m) { Write-Host "[warn] $m" -ForegroundColor Yellow }
if (-not (Test-Path -LiteralPath $BaseCsv)) { throw "Base CSV not found: $BaseCsv" }
if (-not (Test-Path -LiteralPath $AllCsv)) { throw "All CSV not found: $AllCsv" }
# Load CSVs
$baseRows = Import-Csv -LiteralPath $BaseCsv
$allRows = Import-Csv -LiteralPath $AllCsv
if (-not $baseRows) { Write-Warn "Base CSV has no rows." }
if (-not $allRows) { Write-Warn "All CSV has no rows." }
# Validate key presence
if ($baseRows -and -not ($baseRows[0].PSObject.Properties.Name -contains $Key)) { throw "Key column '$Key' not found in base CSV." }
if ($allRows -and -not ($allRows[0].PSObject.Properties.Name -contains $Key)) { throw "Key column '$Key' not found in all CSV." }
# Build a set of existing keys from base
$set = New-Object 'System.Collections.Generic.HashSet[string]'
foreach ($row in $baseRows) {
$val = [string]($row.$Key)
if ($null -ne $val) { [void]$set.Add($val) }
}
# Filter rows in AllCsv whose key is not in base (these are the new / incremental rows)
$incremental = @()
foreach ($row in $allRows) {
$val = [string]($row.$Key)
if (-not $set.Contains($val)) { $incremental += $row }
}
# Preserve column order from the All CSV
$columns = @()
if ($allRows.Count -gt 0) {
$columns = $allRows[0].PSObject.Properties.Name
}
try {
if ($incremental.Count -gt 0) {
if ($columns.Count -gt 0) {
$incremental | Select-Object -Property $columns | Export-Csv -LiteralPath $OutCsv -NoTypeInformation -Encoding UTF8
} else {
$incremental | Export-Csv -LiteralPath $OutCsv -NoTypeInformation -Encoding UTF8
}
} else {
# Write an empty CSV with headers if we know them (facilitates downstream tooling expecting header row)
if ($columns.Count -gt 0) {
$obj = [PSCustomObject]@{}
foreach ($c in $columns) { $obj | Add-Member -NotePropertyName $c -NotePropertyValue $null }
$obj | Select-Object -Property $columns | Export-Csv -LiteralPath $OutCsv -NoTypeInformation -Encoding UTF8
} else {
'' | Out-File -LiteralPath $OutCsv -Encoding UTF8
}
}
Write-Info ("Incremental rows: {0}" -f $incremental.Count)
Write-Info ("Output: {0}" -f (Resolve-Path -LiteralPath $OutCsv))
}
catch {
Write-Host "[error] Failed writing output CSV: $_" -ForegroundColor Red
exit 1
}

View File

@@ -0,0 +1,344 @@
<#
.SYNOPSIS
Export merged PR metadata between two commits (exclusive start, inclusive end) to JSON and CSV.
.DESCRIPTION
Identifies merge/squash commits reachable from EndCommit but not StartCommit, extracts PR numbers,
queries GitHub for metadata plus (optionally) Copilot review/comment summaries, filters labels, then
emits a JSON artifact and a sorted CSV (first label alphabetical).
.PARAMETER StartCommit
Exclusive starting commit (SHA, tag, or ref). Commits AFTER this one are considered.
.PARAMETER EndCommit
Inclusive ending commit (SHA, tag, or ref). If not provided, uses origin/<Branch> when Branch is set; otherwise uses HEAD.
.PARAMETER Repo
GitHub repository (owner/name). Default: microsoft/PowerToys.
.PARAMETER OutputCsv
Destination CSV path. Default: sorted_prs.csv.
.PARAMETER OutputJson
Destination JSON path containing raw PR objects. Default: milestone_prs.json.
.EXAMPLE
pwsh ./dump-prs-since-commit.ps1 -StartCommit 0123abcd -Branch stable
.EXAMPLE
pwsh ./dump-prs-since-commit.ps1 -StartCommit 0123abcd -EndCommit 89ef7654 -OutputCsv delta.csv
.NOTES
Requires: git, gh (authenticated). No Set-StrictMode to keep parity with existing release scripts.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$StartCommit, # exclusive start (commits AFTER this one)
[string]$EndCommit,
[string]$Branch,
[string]$Repo = "microsoft/PowerToys",
[string]$OutputDir,
[string]$OutputCsv = "sorted_prs.csv",
[string]$OutputJson = "milestone_prs.json"
)
<#
.SYNOPSIS
Dump merged PR information whose merge commits are reachable from EndCommit but not from StartCommit.
.DESCRIPTION
Uses git rev-list to compute commits in the (StartCommit, EndCommit] range, extracts PR numbers from merge commit messages,
queries GitHub (gh CLI) for details, then outputs a CSV.
PR merge commit messages in PowerToys generally contain patterns like:
Merge pull request #12345 from ...
.EXAMPLE
pwsh ./dump-prs-since-commit.ps1 -StartCommit 0123abcd -Branch stable
.EXAMPLE
pwsh ./dump-prs-since-commit.ps1 -StartCommit 0123abcd -EndCommit 89ef7654 -OutputCsv changes.csv
.NOTES
Requires: gh CLI authenticated; git available in working directory (must be inside PowerToys repo clone).
CopilotSummary behavior:
- Attempts to locate the latest GitHub Copilot authored review (preferred).
- If no review is found, lazily fetches PR comments to look for a Copilot-authored comment.
- Normalizes whitespace and strips newlines. Empty when no Copilot activity detected.
- Run with -Verbose to see whether the summary came from a 'review' or 'comment' source.
#>
function Write-Info($msg) { Write-Host $msg -ForegroundColor Cyan }
function Write-Warn($msg) { Write-Host $msg -ForegroundColor Yellow }
function Write-Err($msg) { Write-Host $msg -ForegroundColor Red }
function Write-DebugMsg($msg) { if ($PSBoundParameters.ContainsKey('Verbose') -or $VerbosePreference -eq 'Continue') { Write-Host "[VERBOSE] $msg" -ForegroundColor DarkGray } }
# Load member list from Generated Files/ReleaseNotes/MemberList.md (internal team - no thanks needed)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Resolve-Path (Join-Path $scriptDir "..\..\..\..")
$defaultMemberListPath = Join-Path $repoRoot "Generated Files\ReleaseNotes\MemberList.md"
$memberListPath = $defaultMemberListPath
if ($OutputDir) {
$memberListFromOutputDir = Join-Path $OutputDir "MemberList.md"
if (Test-Path $memberListFromOutputDir) {
$memberListPath = $memberListFromOutputDir
}
}
$memberList = @()
if (Test-Path $memberListPath) {
$memberListContent = Get-Content $memberListPath -Raw
# Extract usernames - skip markdown code fence lines, get all non-empty lines
$memberList = ($memberListContent -split "`n") | Where-Object { $_ -notmatch '^\s*```' -and $_.Trim() -ne '' } | ForEach-Object { $_.Trim() }
if (-not $memberList -or $memberList.Count -eq 0) {
Write-Err "MemberList.md is empty at $memberListPath"
exit 1
}
Write-DebugMsg "Loaded $($memberList.Count) members from MemberList.md"
} else {
Write-Err "MemberList.md not found at $memberListPath"
exit 1
}
# Validate we are in a git repo
#if (-not (Test-Path .git)) {
# Write-Err "Current directory does not appear to be the root of a git repository."
# exit 1
#}
# Resolve output directory (if specified)
if ($OutputDir) {
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
if (-not [System.IO.Path]::IsPathRooted($OutputCsv)) {
$OutputCsv = Join-Path $OutputDir $OutputCsv
}
if (-not [System.IO.Path]::IsPathRooted($OutputJson)) {
$OutputJson = Join-Path $OutputDir $OutputJson
}
}
# Resolve commits
try {
if ($Branch) {
Write-Info "Fetching latest '$Branch' from origin (with tags)..."
git fetch origin $Branch --tags | Out-Null
if ($LASTEXITCODE -ne 0) { throw "git fetch origin $Branch --tags failed" }
}
$startSha = (git rev-parse --verify $StartCommit) 2>$null
if (-not $startSha) { throw "StartCommit '$StartCommit' not found" }
if ($Branch) {
$branchRef = $Branch
$branchSha = (git rev-parse --verify $branchRef) 2>$null
if (-not $branchSha) {
$branchRef = "origin/$Branch"
$branchSha = (git rev-parse --verify $branchRef) 2>$null
}
if (-not $branchSha) { throw "Branch '$Branch' not found" }
if (-not $PSBoundParameters.ContainsKey('EndCommit') -or [string]::IsNullOrWhiteSpace($EndCommit)) {
$EndCommit = $branchRef
}
}
if (-not $PSBoundParameters.ContainsKey('EndCommit') -or [string]::IsNullOrWhiteSpace($EndCommit)) {
$EndCommit = "HEAD"
}
$endSha = (git rev-parse --verify $EndCommit) 2>$null
if (-not $endSha) { throw "EndCommit '$EndCommit' not found" }
}
catch {
Write-Err $_
exit 1
}
Write-Info "Collecting commits between $startSha..$endSha (excluding start, including end)."
# Get list of commits reachable from end but not from start.
# IMPORTANT: In PowerShell, the .. operator creates a numeric/char range. If $startSha and $endSha look like hex strings,
# `$startSha..$endSha` must be passed as a single string argument.
$rangeArg = "$startSha..$endSha"
$commitList = git rev-list $rangeArg
# Normalize list (filter out empty strings)
$normalizedCommits = $commitList | Where-Object { $_ -and $_.Trim() -ne '' }
$commitCount = ($normalizedCommits | Measure-Object).Count
Write-DebugMsg ("Raw commitList length (including blanks): {0}" -f (($commitList | Measure-Object).Count))
Write-DebugMsg ("Normalized commit count: {0}" -f $commitCount)
if ($commitCount -eq 0) {
Write-Warn "No commits found in specified range ($startSha..$endSha)."; exit 0
}
Write-DebugMsg ("First 5 commits: {0}" -f (($normalizedCommits | Select-Object -First 5) -join ', '))
<#
Extract PR numbers from commits.
Patterns handled:
1. Merge commits: 'Merge pull request #12345 from ...'
2. Squash commits: 'Some feature change (#12345)' (GitHub default squash format)
We collect both. If a commit matches both (unlikely), it's deduped later.
#>
# Extract PR numbers from merge or squash commits
$mergeCommits = @()
foreach ($c in $normalizedCommits) {
$subject = git show -s --format=%s $c
$matched = $false
# Pattern 1: Traditional merge commit
if ($subject -match 'Merge pull request #([0-9]+) ') {
$prNumber = [int]$matches[1]
$mergeCommits += [PSCustomObject]@{ Sha = $c; Pr = $prNumber; Subject = $subject; Pattern = 'merge' }
Write-DebugMsg "Matched merge PR #$prNumber in commit $c"
$matched = $true
}
# Pattern 2: Squash merge subject line with ' (#12345)' at end (allow possible whitespace before paren)
if ($subject -match '\(#([0-9]+)\)$') {
$prNumber2 = [int]$matches[1]
# Avoid duplicate object if pattern 1 already captured same number for same commit
if (-not ($mergeCommits | Where-Object { $_.Sha -eq $c -and $_.Pr -eq $prNumber2 })) {
$mergeCommits += [PSCustomObject]@{ Sha = $c; Pr = $prNumber2; Subject = $subject; Pattern = 'squash' }
Write-DebugMsg "Matched squash PR #$prNumber2 in commit $c"
}
$matched = $true
}
if (-not $matched) {
Write-DebugMsg "No PR pattern in commit $c : $subject"
}
}
if (-not $mergeCommits -or $mergeCommits.Count -eq 0) {
Write-Warn "No merge commits with PR numbers found in range."; exit 0
}
# Deduplicate PR numbers (in case of revert or merges across branches)
$prNumbers = $mergeCommits | Select-Object -ExpandProperty Pr -Unique | Sort-Object
Write-Info ("Found {0} unique PRs: {1}" -f $prNumbers.Count, ($prNumbers -join ', '))
Write-DebugMsg ("Total merge commits examined: {0}" -f $mergeCommits.Count)
# Query GitHub for each PR
$prDetails = @()
function Get-CopilotSummaryFromPrJson {
param(
[Parameter(Mandatory=$true)]$PrJson,
[switch]$VerboseMode
)
# Returns a hashtable with Summary and Source keys.
$result = @{ Summary = ""; Source = "" }
if (-not $PrJson) { return $result }
$candidateAuthors = @(
'github-copilot[bot]', 'github-copilot', 'copilot'
)
# 1. Reviews (preferred) pick the LONGEST valid Copilot body, not the most recent
$reviews = $PrJson.reviews
if ($reviews) {
$copilotReviews = $reviews | Where-Object {
($candidateAuthors -contains $_.author.login -or $_.author.login -like '*copilot*') -and $_.body -and $_.body.Trim() -ne ''
}
if ($copilotReviews) {
$longest = $copilotReviews | Sort-Object { $_.body.Length } -Descending | Select-Object -First 1
if ($longest) {
$body = $longest.body
$norm = ($body -replace "`r", '') -replace "`n", ' '
$norm = $norm -replace '\s+', ' '
$result.Summary = $norm
$result.Source = 'review'
if ($VerboseMode) { Write-DebugMsg "Selected Copilot review length=$($body.Length) (longest)." }
return $result
}
}
}
# 2. Comments fallback (some repos surface Copilot summaries as PR comments rather than review objects)
if ($null -eq $PrJson.comments) {
try {
# Lazy fetch comments only if needed
$commentsJson = gh pr view $PrJson.number --repo $Repo --json comments 2>$null | ConvertFrom-Json
if ($commentsJson -and $commentsJson.comments) {
$PrJson | Add-Member -NotePropertyName comments -NotePropertyValue $commentsJson.comments -Force
}
} catch {
if ($VerboseMode) { Write-DebugMsg "Failed to fetch comments for PR #$($PrJson.number): $_" }
}
}
if ($PrJson.comments) {
$copilotComments = $PrJson.comments | Where-Object {
($candidateAuthors -contains $_.author.login -or $_.author.login -like '*copilot*') -and $_.body -and $_.body.Trim() -ne ''
}
if ($copilotComments) {
$longestC = $copilotComments | Sort-Object { $_.body.Length } -Descending | Select-Object -First 1
if ($longestC) {
$body = $longestC.body
$norm = ($body -replace "`r", '') -replace "`n", ' '
$norm = $norm -replace '\s+', ' '
$result.Summary = $norm
$result.Source = 'comment'
if ($VerboseMode) { Write-DebugMsg "Selected Copilot comment length=$($body.Length) (longest)." }
return $result
}
}
}
return $result
}
foreach ($pr in $prNumbers) {
Write-Info "Fetching PR #$pr ..."
try {
# Include comments only if Verbose asked; if not, we lazily pull when reviews are missing
$fields = 'number,title,labels,author,url,body,reviews'
if ($PSBoundParameters.ContainsKey('Verbose')) { $fields += ',comments' }
$json = gh pr view $pr --repo $Repo --json $fields 2>$null | ConvertFrom-Json
if ($null -eq $json) { throw "Empty response" }
$copilot = Get-CopilotSummaryFromPrJson -PrJson $json -VerboseMode:($PSBoundParameters.ContainsKey('Verbose'))
if ($copilot.Summary -and $copilot.Source -and $PSBoundParameters.ContainsKey('Verbose')) {
Write-DebugMsg "Copilot summary source=$($copilot.Source) chars=$($copilot.Summary.Length)"
} elseif (-not $copilot.Summary) {
Write-DebugMsg "No Copilot summary found for PR #$pr"
}
# Filter labels
$filteredLabels = $json.labels | Where-Object {
($_.name -like "Product-*") -or
($_.name -like "Area-*") -or
($_.name -like "GitHub*") -or
($_.name -like "*Plugin") -or
($_.name -like "Issue-*")
}
$labelNames = ($filteredLabels | ForEach-Object { $_.name }) -join ", "
$bodyValue = if ($json.body) { ($json.body -replace "`r", '') -replace "`n", ' ' } else { '' }
$bodyValue = $bodyValue -replace '\s+', ' '
# Determine if author needs thanks (not in member list)
$authorLogin = $json.author.login
$needThanks = $true
if ($memberList.Count -gt 0 -and $authorLogin) {
$needThanks = -not ($memberList -contains $authorLogin)
}
$prDetails += [PSCustomObject]@{
Id = $json.number
Title = $json.title
Labels = $labelNames
Author = $authorLogin
Url = $json.url
Body = $bodyValue
CopilotSummary = $copilot.Summary
NeedThanks = $needThanks
}
}
catch {
$err = $_
Write-Warn ("Failed to fetch PR #{0}: {1}" -f $pr, $err)
}
}
if (-not $prDetails) { Write-Warn "No PR details fetched."; exit 0 }
# Sort by Labels like original script (first label alphabetical)
$sorted = $prDetails | Sort-Object { ($_.Labels -split ',')[0] }
# Output JSON raw (optional)
$sorted | ConvertTo-Json -Depth 6 | Out-File -Encoding UTF8 $OutputJson
Write-Info "Saving CSV to $OutputCsv ..."
$sorted | Export-Csv $OutputCsv -NoTypeInformation
Write-Host "✅ Done. Generated $($prDetails.Count) PR rows." -ForegroundColor Green

View File

@@ -0,0 +1,80 @@
<#
.SYNOPSIS
Find a commit on a branch that has the same subject line as a reference commit.
.DESCRIPTION
Given a commit SHA (often from a release tag) and a branch name, this script
resolves the reference commit's subject, then searches the branch history for
commits with the exact same subject line. Useful when the release tag commit
is not reachable from your current branch history.
.PARAMETER Commit
The reference commit SHA or ref (e.g., v0.96.1 or a full SHA).
.PARAMETER Branch
The branch to search (e.g., stable or main). Defaults to stable.
.PARAMETER RepoPath
Path to the local repo. Defaults to current directory.
.EXAMPLE
pwsh ./find-commit-by-title.ps1 -Commit b62f6421845f7e5c92b8186868d98f46720db442 -Branch stable
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Commit,
[string]$Branch = "stable",
[string]$RepoPath = "."
)
function Write-Info($msg) { Write-Host $msg -ForegroundColor Cyan }
function Write-Err($msg) { Write-Host $msg -ForegroundColor Red }
Push-Location $RepoPath
try {
Write-Info "Fetching latest '$Branch' from origin (with tags)..."
git fetch origin $Branch --tags | Out-Null
if ($LASTEXITCODE -ne 0) { throw "git fetch origin $Branch --tags failed" }
$commitSha = (git rev-parse --verify $Commit) 2>$null
if (-not $commitSha) { throw "Commit '$Commit' not found" }
$subject = (git show -s --format=%s $commitSha) 2>$null
if (-not $subject) { throw "Unable to read subject for '$commitSha'" }
$branchRef = $Branch
$branchSha = (git rev-parse --verify $branchRef) 2>$null
if (-not $branchSha) {
$branchRef = "origin/$Branch"
$branchSha = (git rev-parse --verify $branchRef) 2>$null
}
if (-not $branchSha) { throw "Branch '$Branch' not found" }
Write-Info "Reference commit: $commitSha"
Write-Info "Reference title: $subject"
Write-Info "Searching branch: $branchRef"
$matches = git log $branchRef --format="%H|%s" | Where-Object { $_ -match '\|' }
$results = @()
foreach ($line in $matches) {
$parts = $line -split '\|', 2
if ($parts.Count -eq 2 -and $parts[1] -eq $subject) {
$results += [PSCustomObject]@{ Sha = $parts[0]; Title = $parts[1] }
}
}
if (-not $results -or $results.Count -eq 0) {
Write-Info "No matching commit found on $branchRef for the given title."
exit 0
}
Write-Info ("Found {0} matching commit(s):" -f $results.Count)
$results | ForEach-Object { Write-Host ("{0} {1}" -f $_.Sha, $_.Title) }
}
catch {
Write-Err $_
exit 1
}
finally {
Pop-Location
}

View File

@@ -0,0 +1,85 @@
<#
.SYNOPSIS
Group PR rows by their Labels column and emit per-label CSV files.
.DESCRIPTION
Reads a milestone PR CSV (usually produced by dump-prs-information / dump-prs-since-commit scripts),
splits rows by label list, normalizes/sorts individual labels, and writes one CSV per unique label combination.
Each output preserves the original row ordering within that subset and column order from the source.
.PARAMETER CsvPath
Input CSV containing PR rows with a 'Labels' column (comma-separated list).
.PARAMETER OutDir
Output directory to place grouped CSVs (created if missing). Default: 'grouped_csv'.
.NOTES
Label combinations are joined using ' | ' when multiple labels present. Filenames are sanitized (invalid characters,
whitespace collapsed) and truncated to <= 120 characters.
#>
param(
[string]$CsvPath = "sorted_prs.csv",
[string]$OutDir = "grouped_csv"
)
$ErrorActionPreference = 'Stop'
function Write-Info($msg) { Write-Host "[info] $msg" -ForegroundColor Cyan }
function Write-Warn($msg) { Write-Host "[warn] $msg" -ForegroundColor Yellow }
if (-not (Test-Path -LiteralPath $CsvPath)) { throw "CSV not found: $CsvPath" }
Write-Info "Reading CSV: $CsvPath"
$rows = Import-Csv -LiteralPath $CsvPath
Write-Info ("Loaded {0} rows" -f $rows.Count)
function ConvertTo-SafeFileName {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$Name
)
if ([string]::IsNullOrWhiteSpace($Name)) { return 'Unnamed' }
$s = $Name -replace '[<>:"/\\|?*]', '-' # invalid path chars
$s = $s -replace '\s+', '-' # spaces to dashes
$s = $s -replace '-{2,}', '-' # collapse dashes
$s = $s.Trim('-')
if ($s.Length -gt 120) { $s = $s.Substring(0,120).Trim('-') }
if ([string]::IsNullOrWhiteSpace($s)) { return 'Unnamed' }
return $s
}
# Build groups keyed by normalized, sorted label combinations. Preserve original CSV row order.
$groups = @{}
foreach ($row in $rows) {
$labelsRaw = $row.Labels
if ([string]::IsNullOrWhiteSpace($labelsRaw)) {
$labelParts = @('Unlabeled')
} else {
$parts = $labelsRaw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
if (-not $parts -or $parts.Count -eq 0) { $labelParts = @('Unlabeled') }
else { $labelParts = $parts | Sort-Object }
}
$key = ($labelParts -join ' | ')
if (-not $groups.ContainsKey($key)) { $groups[$key] = New-Object System.Collections.ArrayList }
[void]$groups[$key].Add($row)
}
if (-not (Test-Path -LiteralPath $OutDir)) {
Write-Info "Creating output directory: $OutDir"
New-Item -ItemType Directory -Path $OutDir | Out-Null
}
Write-Info ("Generating {0} grouped CSV file(s) into: {1}" -f $groups.Count, $OutDir)
foreach ($key in $groups.Keys) {
$labelParts = if ($key -eq 'Unlabeled') { @('Unlabeled') } else { $key -split '\s\|\s' }
$safeName = ($labelParts | ForEach-Object { ConvertTo-SafeFileName -Name $_ }) -join '-'
$filePath = Join-Path $OutDir ("$safeName.csv")
# Keep same columns and order
$groups[$key] | Export-Csv -LiteralPath $filePath -NoTypeInformation -Encoding UTF8
}
Write-Info "Done. Sample output files:"
Get-ChildItem -LiteralPath $OutDir | Select-Object -First 10 Name | Format-Table -HideTableHeaders

View File

@@ -31,82 +31,59 @@
"Common.Search.dll",
"PowerToys.AlwaysOnTop.exe",
"PowerToys.AlwaysOnTopModuleInterface.dll",
"PowerToys.CmdNotFoundModuleInterface.dll",
"PowerToys.ColorPicker.dll",
"PowerToys.ColorPickerUI.dll",
"PowerToys.ColorPickerUI.exe",
"PowerToys.CropAndLockModuleInterface.dll",
"PowerToys.CropAndLock.exe",
"PowerToys.PowerOCRModuleInterface.dll",
"PowerToys.PowerOCR.dll",
"PowerToys.PowerOCR.exe",
"PowerToys.AdvancedPasteModuleInterface.dll",
"WinUI3Apps\\PowerToys.AdvancedPaste.exe",
"WinUI3Apps\\PowerToys.AdvancedPaste.dll",
"PowerToys.AwakeModuleInterface.dll",
"PowerToys.Awake.exe",
"PowerToys.Awake.dll",
"PowerToys.FancyZonesEditor.exe",
"PowerToys.FancyZonesEditor.dll",
"PowerToys.FancyZonesEditorCommon.dll",
"PowerToys.FancyZonesModuleInterface.dll",
"PowerToys.FancyZones.exe",
"FancyZonesCLI.exe",
"FancyZonesCLI.dll",
"PowerToys.GcodePreviewHandler.dll",
"PowerToys.GcodePreviewHandler.exe",
"PowerToys.GcodePreviewHandlerCpp.dll",
"PowerToys.GcodeThumbnailProvider.dll",
"PowerToys.GcodeThumbnailProvider.exe",
"PowerToys.GcodeThumbnailProviderCpp.dll",
"PowerToys.BgcodePreviewHandler.dll",
"PowerToys.BgcodePreviewHandler.exe",
"PowerToys.BgcodePreviewHandlerCpp.dll",
"PowerToys.BgcodeThumbnailProvider.dll",
"PowerToys.BgcodeThumbnailProvider.exe",
"PowerToys.BgcodeThumbnailProviderCpp.dll",
"PowerToys.ManagedTelemetry.dll",
"PowerToys.MarkdownPreviewHandler.dll",
"PowerToys.MarkdownPreviewHandler.exe",
"PowerToys.MarkdownPreviewHandlerCpp.dll",
"PowerToys.MonacoPreviewHandler.dll",
"PowerToys.MonacoPreviewHandler.exe",
"PowerToys.MonacoPreviewHandlerCpp.dll",
"PowerToys.PdfPreviewHandler.dll",
"PowerToys.PdfPreviewHandler.exe",
"PowerToys.PdfPreviewHandlerCpp.dll",
"PowerToys.PdfThumbnailProvider.dll",
"PowerToys.PdfThumbnailProvider.exe",
"PowerToys.PdfThumbnailProviderCpp.dll",
"PowerToys.powerpreview.dll",
"PowerToys.PreviewHandlerCommon.dll",
"PowerToys.QoiPreviewHandler.dll",
"PowerToys.QoiPreviewHandler.exe",
"PowerToys.QoiPreviewHandlerCpp.dll",
"PowerToys.QoiThumbnailProvider.dll",
"PowerToys.QoiThumbnailProvider.exe",
"PowerToys.QoiThumbnailProviderCpp.dll",
"PowerToys.StlThumbnailProvider.dll",
"PowerToys.StlThumbnailProvider.exe",
"PowerToys.StlThumbnailProviderCpp.dll",
"PowerToys.SvgPreviewHandler.dll",
"PowerToys.SvgPreviewHandler.exe",
"PowerToys.SvgPreviewHandlerCpp.dll",
"PowerToys.SvgThumbnailProvider.dll",
"PowerToys.SvgThumbnailProvider.exe",
"PowerToys.SvgThumbnailProviderCpp.dll",
"WinUI3Apps\\PowerToys.HostsModuleInterface.dll",
"WinUI3Apps\\PowerToys.HostsUILib.dll",
"WinUI3Apps\\PowerToys.Hosts.dll",
"WinUI3Apps\\PowerToys.Hosts.exe",
@@ -129,7 +106,6 @@
"WinUI3Apps\\PowerToys.QuickAccess.exe",
"WinUI3Apps\\PowerToys.Settings.UI.Controls.dll",
"WinUI3Apps\\PowerToys.EnvironmentVariablesModuleInterface.dll",
"WinUI3Apps\\PowerToys.EnvironmentVariablesUILib.dll",
"WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
"WinUI3Apps\\PowerToys.EnvironmentVariables.exe",
@@ -142,7 +118,6 @@
"PowerToys.ImageResizerContextMenu.dll",
"ImageResizerContextMenuPackage.msix",
"PowerToys.LightSwitchModuleInterface.dll",
"LightSwitchService\\PowerToys.LightSwitchService.exe",
"PowerToys.KeyboardManager.dll",
@@ -178,7 +153,6 @@
"RunPlugins\\WebSearch\\Community.PowerToys.Run.Plugin.WebSearch.dll",
"RunPlugins\\WindowsTerminal\\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.dll",
"WinUI3Apps\\PowerToys.MeasureToolModuleInterface.dll",
"WinUI3Apps\\PowerToys.MeasureToolCore.dll",
"WinUI3Apps\\PowerToys.MeasureToolUI.dll",
"WinUI3Apps\\PowerToys.MeasureToolUI.exe",
@@ -194,7 +168,6 @@
"PowerToys.MouseWithoutBorders.dll",
"PowerToys.MouseWithoutBorders.exe",
"PowerToys.MouseWithoutBordersModuleInterface.dll",
"PowerToys.MouseWithoutBordersService.dll",
"PowerToys.MouseWithoutBordersService.exe",
"PowerToys.MouseWithoutBordersHelper.dll",
@@ -207,7 +180,6 @@
"PowerAccent.Core.dll",
"PowerToys.PowerAccent.dll",
"PowerToys.PowerAccent.exe",
"PowerToys.PowerAccentModuleInterface.dll",
"PowerToys.PowerAccentKeyboardService.dll",
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
@@ -222,7 +194,6 @@
"PowerToys.WorkspacesEditor.dll",
"PowerToys.WorkspacesLauncherUI.exe",
"PowerToys.WorkspacesLauncherUI.dll",
"PowerToys.WorkspacesModuleInterface.dll",
"PowerToys.WorkspacesCsharpLibrary.dll",
"WinUI3Apps\\PowerToys.RegistryPreviewExt.dll",
@@ -231,16 +202,13 @@
"WinUI3Apps\\PowerToys.RegistryPreview.exe",
"PowerToys.ShortcutGuide.exe",
"PowerToys.ShortcutGuideModuleInterface.dll",
"PowerToys.ZoomIt.exe",
"PowerToys.ZoomItModuleInterface.dll",
"PowerToys.ZoomItSettingsInterop.dll",
"WinUI3Apps\\PowerToys.Settings.dll",
"WinUI3Apps\\PowerToys.Settings.exe",
"PowerToys.CmdPalModuleInterface.dll",
"CmdPalKeyboardService.dll",
"PowerToys.ModuleContracts.dll",
"Awake.ModuleServices.dll",

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

@@ -0,0 +1,13 @@
{
"servers": {
"github-artifacts": {
"command": "node",
"args": [
"tools/mcp/github-artifacts/launch.js"
],
"env": {
"GITHUB_TOKEN": "${env:GITHUB_TOKEN}"
}
}
}
}

165
AGENTS.md Normal file
View File

@@ -0,0 +1,165 @@
---
description: 'Top-level AI contributor guidance for developing PowerToys - a collection of Windows productivity utilities'
applyTo: '**'
---
# PowerToys AI Contributor Guide
This is the top-level guidance for AI contributions to PowerToys. Keep changes atomic, follow existing patterns, and cite exact paths in PRs.
## Overview
PowerToys is a set of utilities for power users to tune and streamline their Windows experience.
| Area | Location | Description |
|------|----------|-------------|
| Runner | `src/runner/` | Main executable, tray icon, module loader, hotkey management |
| Settings UI | `src/settings-ui/` | WinUI/WPF configuration app communicating via named pipes |
| Modules | `src/modules/` | Individual PowerToys utilities (each in its own subfolder) |
| Common Libraries | `src/common/` | Shared code: logging, IPC, settings, DPI, telemetry, utilities |
| Build Tools | `tools/build/` | Build scripts and automation |
| Documentation | `doc/devdocs/` | Developer documentation |
| Installer | `installer/` | WiX-based installer projects |
For architecture details and module types, see [Architecture Overview](doc/devdocs/core/architecture.md).
## Conventions
For detailed coding conventions, see:
- [Coding Guidelines](doc/devdocs/development/guidelines.md) Dependencies, testing, PR management
- [Coding Style](doc/devdocs/development/style.md) Formatting, C++/C#/XAML style rules
- [Logging](doc/devdocs/development/logging.md) C++ spdlog and C# Logger usage
### Component-Specific Instructions
These instruction files are automatically applied when working in their respective areas:
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md) IPC contracts, schema migrations
- [Common Libraries](.github/instructions/common-libraries.instructions.md) ABI stability, shared code guidelines
## Build
### Prerequisites
- Visual Studio 2022 17.4+
- Windows 10 1803+ (April 2018 Update or newer)
- Initialize submodules once: `git submodule update --init --recursive`
### Build Commands
| Task | Command |
|------|---------|
| First build / NuGet restore | `tools\build\build-essentials.cmd` |
| Build current folder | `tools\build\build.cmd` |
| Build with options | `build.ps1 -Platform x64 -Configuration Release` |
### Build Discipline
1. One terminal per operation (build → test). Do not switch or open new ones mid-flow
2. After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`)
3. Use scripts to build: `tools/build/build.ps1` or `tools/build/build.cmd`
4. For first build or missing NuGet packages, run `build-essentials.cmd` first
5. **Exit code 0 = success; non-zero = failure** treat this as absolute
6. On failure, read the errors log: `build.<config>.<platform>.errors.log`
7. Do not start tests or launch Runner until the build succeeds
### Build Logs
Located next to the solution/project being built:
- `build.<configuration>.<platform>.errors.log` errors only (check this first)
- `build.<configuration>.<platform>.all.log` full log
- `build.<configuration>.<platform>.trace.binlog` for MSBuild Structured Log Viewer
For complete details, see [Build Guidelines](tools/build/BUILD-GUIDELINES.md).
## Tests
### Test Discovery
- Find test projects by product code prefix (e.g., `FancyZones`, `AdvancedPaste`)
- Look for sibling folders or 1-2 levels up named `<Product>*UnitTests` or `<Product>*UITests`
### Running Tests
1. **Build the test project first**, wait for exit code 0
2. Run via VS Test Explorer (`Ctrl+E, T`) or `vstest.console.exe` with filters
3. **Avoid `dotnet test`** in this repo use VS Test Explorer or vstest.console.exe
### Test Types
| Type | Requirements | Setup |
|------|--------------|-------|
| Unit Tests | Standard dev environment | None |
| UI Tests | WinAppDriver v1.2.1, Developer Mode | Install from [WinAppDriver releases](https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1) |
| Fuzz Tests | OneFuzz, .NET 8 | See [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md) |
### Test Discipline
1. Add or adjust tests when changing behavior
2. If tests skipped, state why (e.g., comment-only change, string rename)
3. New modules handling file I/O or user input **must** implement fuzzing tests
### Special Requirements
- **Mouse Without Borders**: Requires 2+ physical computers (not VMs)
- **Multi-monitor utilities**: Test with 2+ monitors, different DPI settings
For UI test setup details, see [UI Tests](doc/devdocs/development/ui-tests.md).
## Boundaries
### Ask for Clarification When
- Ambiguous spec after scanning relevant docs
- Cross-module impact (shared enum/struct) is unclear
- Security, elevation, or installer changes involved
- GPO or policy handling modifications needed
### Areas Requiring Extra Care
| Area | Concern | Reference |
|------|---------|-----------|
| `src/common/` | ABI breaks | [Common Libraries Instructions](.github/instructions/common-libraries.instructions.md) |
| `src/runner/`, `src/settings-ui/` | IPC contracts, schema | [Runner & Settings UI Instructions](.github/instructions/runner-settings-ui.instructions.md) |
| Installer files | Release impact | Careful review required |
| Elevation/GPO logic | Security | Confirm no regression in policy handling |
### What NOT to Do
- Don't merge incomplete features into main (use feature branches)
- Don't break IPC/JSON contracts without updating both runner and settings-ui
- Don't add noisy logs in hot paths
- Don't introduce third-party deps without PM approval and `NOTICE.md` update
## Validation Checklist
Before finishing, verify:
- [ ] Build clean with exit code 0
- [ ] Tests updated and passing locally
- [ ] No unintended ABI breaks or schema changes
- [ ] IPC contracts consistent between runner and settings-ui
- [ ] New dependencies added to `NOTICE.md`
- [ ] PR is atomic (one logical change), with issue linked
## Documentation Index
### Core Architecture
- [Architecture Overview](doc/devdocs/core/architecture.md)
- [Runner](doc/devdocs/core/runner.md)
- [Settings System](doc/devdocs/core/settings/readme.md)
- [Module Interface](doc/devdocs/modules/interface.md)
### Development
- [Coding Guidelines](doc/devdocs/development/guidelines.md)
- [Coding Style](doc/devdocs/development/style.md)
- [Logging](doc/devdocs/development/logging.md)
- [UI Tests](doc/devdocs/development/ui-tests.md)
- [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md)
### Build & Tools
- [Build Guidelines](tools/build/BUILD-GUIDELINES.md)
- [Tools Overview](doc/devdocs/tools/readme.md)
### Instructions (Auto-Applied)
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
- [Common Libraries](.github/instructions/common-libraries.instructions.md)

View File

@@ -2,6 +2,17 @@
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Generate version data -->
<Target Name="GenerateVersionData" BeforeTargets="PrepareForBuild">
<ItemGroup>
<HeaderLines Include="#pragma once" />
<HeaderLines Include="#define VERSION_MAJOR $(Version.Split('.')[0])" />
<HeaderLines Include="#define VERSION_MINOR $(Version.Split('.')[1])" />
<HeaderLines Include="#define VERSION_REVISION $(Version.Split('.')[2])" />
</ItemGroup>
<WriteLinesToFile File="Generated Files\version_gen.h" Lines="@(HeaderLines)" Overwrite="true" Encoding="Unicode" WriteOnlyWhenDifferent="true" />
</Target>
<!-- Project configurations -->
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">

View File

@@ -30,7 +30,11 @@ _If you want to find diagnostic data events in the source code, these two links
- [C++ events](https://github.com/search?q=repo%3Amicrosoft%2FPowerToys+ProjectTelemetryPrivacyDataTag&type=code)
### General
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -43,6 +47,18 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.GeneralSettingsChanged</td>
<td>Logs changes made to general settings within PowerToys.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.Install_Fail</td>
<td>Triggered when the PowerToys installation process encounters an error and fails to complete.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.Repair_Cancel</td>
<td>Triggered when a PowerToys repair operation is cancelled by the user before completion.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.Repair_Fail</td>
<td>Triggered when the PowerToys repair operation fails to complete successfully due to an error.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.Runner_Launch</td>
<td>Indicates when the PowerToys Runner is launched.</td>
@@ -59,6 +75,18 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.ScoobeStartedEvent</td>
<td>Triggered when SCOOBE (Secondary Out-of-box experience) starts.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.ShortcutConflictControlClickedEvent</td>
<td>Triggered when a user clicks on the Shortcut Conflict Control button in the PowerToys Settings UI Dashboard.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.ShortcutConflictDetectedEvent</td>
<td>Triggered when keyboard shortcut conflicts are detected in the PowerToys Settings UI Dashboard.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.ShortcutConflictResolvedEvent</td>
<td>Triggered when a keyboard shortcut conflict is resolved in the PowerToys Settings UI.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.TrayFlyoutActivatedEvent</td>
<td>Indicates when the tray flyout menu is activated.</td>
@@ -67,6 +95,14 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.TrayFlyoutModuleRunEvent</td>
<td>Logs when a utility from the tray flyout menu is run.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.UnInstall_Cancel</td>
<td>Triggered when the PowerToys uninstallation process is cancelled by the user before completion.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.UnInstall_Fail</td>
<td>Triggered when the PowerToys uninstallation process fails to complete successfully due to an error. </td>
</tr>
<tr>
<td>Microsoft.PowerToys.Uninstall_Success</td>
<td>Logs when PowerToys is successfully uninstalled (who would do such a thing!).</td>
@@ -82,11 +118,19 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### OOBE (Out-of-box experience)
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.OobeModuleRunEvent</td>
<td>Triggered when a user clicks to run or launch a PowerToys module directly from the OOBE (out-of-box experience) interface.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.OobeSectionEvent</td>
<td>Occurs when OOBE is shown to the user.</td>
@@ -99,10 +143,18 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.OobeStartedEvent</td>
<td>Indicates when the out-of-box experience has been initiated.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.OobeVariantAssignmentEvent</td>
<td>This event logs A/B testing assignments for experimental features, helping track which users are in control or alternate groups for feature experiments. </td>
</tr>
</table>
### Advanced Paste
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -170,7 +222,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Always on Top
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -190,7 +246,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Awake
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -218,7 +278,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Color Picker
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -235,18 +299,14 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.ColorPicker_Settings</td>
<td>Triggered when the settings for the Color Picker are accessed or modified.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.ColorPickerCancelledEvent</td>
<td>Occurs when a color picking action is cancelled by the user.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.ColorPickerShowEvent</td>
<td>Triggered when the Color Picker UI is displayed on the screen.</td>
</tr>
</table>
### Command Not Found
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -259,10 +319,6 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.CmdNotFoundInstallEvent</td>
<td>Triggered when a Command Not Found is installed.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdNotFoundInstanceCreatedEvent</td>
<td>Occurs when an instance of a Command Not Found is created.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdNotFoundUninstallEvent</td>
<td>Triggered when Command Not Found is uninstalled after being previously installed.</td>
@@ -271,7 +327,11 @@ _If you want to find diagnostic data events in the source code, these two links
### Command Palette
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -335,7 +395,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Crop And Lock
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -344,10 +408,26 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.CropAndLock_ActivateReparent</td>
<td>Triggered when the cropping interface is activated for reparenting the cropped content.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CropAndLock_ActivateScreenshot</td>
<td>Triggered when the screenshot mode is activated in Crop and Lock.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CropAndLock_ActivateThumbnail</td>
<td>Occurs when the thumbnail view for cropped content is activated.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CropAndLock_CreateReparentWindow</td>
<td>Triggered when a reparent window is created in Crop and Lock mode.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CropAndLock_CreateScreenshotWindow</td>
<td>Triggered when a screenshot window is created in Crop and Lock mode.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CropAndLock_CreateThumbnailWindow</td>
<td>Triggered when a thumbnail window is created in Crop and Lock mode.<-/td>
</tr>
<tr>
<td>Microsoft.PowerToys.CropAndLock_EnableCropAndLock</td>
<td>Triggered when Crop and Lock is enabled.</td>
@@ -358,8 +438,28 @@ _If you want to find diagnostic data events in the source code, these two links
</tr>
</table>
### Cursor Wrap
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.CursorWrap_EnableCursorWrap</td>
<td>Triggered when Cursor Wrap is enabled or disabled.</td>
</tr>
</table>
### Environment Variables
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -387,7 +487,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### FancyZones
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -404,6 +508,10 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.FancyZones_EnableFancyZones</td>
<td>Occurs when FancyZones is enabled.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZones_Error</td>
<td>Triggered when an error occurs within the FancyZones module. This event logs critical errors to help diagnose and troubleshoot issues with FancyZones functionality, such as failures to set up Windows hooks or other system-level operations required for window management.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZones_KeyboardSnapWindowToZone</td>
<td>Triggered when a window is snapped to a zone using the keyboard.</td>
@@ -416,10 +524,6 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.FancyZones_MoveOrResizeStarted</td>
<td>Triggered when a window move or resize action is initiated.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZones_MoveSizeEnd</td>
<td>Occurs when the moving or resizing of a window has ended.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZones_OnKeyDown</td>
<td>Triggered when a key is pressed down while interacting with zones.</td>
@@ -432,10 +536,6 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.FancyZones_Settings</td>
<td>Triggered when FancyZones settings are accessed or modified.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZones_SettingsChanged</td>
<td>Occurs when there is a change in the FancyZones settings.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZones_SnapNewWindowIntoZone</td>
<td>Triggered when a new window is snapped into a zone.</td>
@@ -456,10 +556,50 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.FancyZones_CLICommand</td>
<td>Triggered when a FancyZones CLI command is executed, logging the command name and success status.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZonesEditorStartEvent</td>
<td>Triggered when the FancyZones Editor application starts. This logs the initialization of the editor UI, which is used to create and configure custom zone layouts.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZonesEditorStartFinishEvent</td>
<td>Triggered when the FancyZones Editor has completed loading and is ready for user interaction.</td>
</tr>
</table>
### File Locksmith
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.FileLocksmith_EnableFileLocksmith</td>
<td>Triggered when File Locksmith is enabled.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FileLocksmith_Invoked</td>
<td>Occurs when File Locksmith is invoked.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FileLocksmith_InvokedRet</td>
<td>Triggered when File Locksmith invocation returns a result.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FileLocksmith_QueryContextMenuError</td>
<td>Occurs when there is an error querying the context menu for File Locksmith.</td>
</tr>
</table>
### FileExplorerAddOns
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -496,6 +636,10 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.MarkdownFilePreviewed</td>
<td>Triggered when a Markdown file is previewed in File Explorer.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.MarkdownFilePreviewError</td>
<td>Triggered when there is an error previewing a Markdown file in File Explorer.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.PdfFileHandlerLoaded</td>
<td>Occurs when a PDF file handler is loaded.</td>
@@ -504,6 +648,10 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.PdfFilePreviewed</td>
<td>Triggered when a PDF file is previewed in File Explorer.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.PdfFilePreviewError</td>
<td>Triggered when there is an error previewing a PDF file in File Explorer.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.PowerPreview_Enabled</td>
<td>Occurs when preview is enabled.</td>
@@ -520,6 +668,10 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.PowerPreview_TweakUISettings_InitSet__ErrorLoadingFile</td>
<td>Triggered when there is an error loading a file during Tweak UI settings initialization.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.PowerPreview_TweakUISettings_SetConfig__InvalidJSONGiven</td>
<td>Triggered when invalid JSON is provided to the Power Preview settings configuration</td>
</tr>
<tr>
<td>Microsoft.PowerToys.PowerPreview_TweakUISettings_SuccessfullyUpdatedSettings</td>
<td>Occurs when the Tweak UI settings for Power Preview are successfully updated.</td>
@@ -528,6 +680,10 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.QoiFilePreviewed</td>
<td>Triggered when a QOI file is previewed in File Explorer.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.QoiFilePreviewError</td>
<td>Triggered when there is an error previewing a QOI (Quite OK Image) file in File Explorer.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.SvgFileHandlerLoaded</td>
<td>Occurs when an SVG file handler is loaded.</td>
@@ -542,32 +698,12 @@ _If you want to find diagnostic data events in the source code, these two links
</tr>
</table>
### File Locksmith
<table style="width:100%">
<tr>
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.FileLocksmith_EnableFileLocksmith</td>
<td>Triggered when File Locksmith is enabled.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FileLocksmith_Invoked</td>
<td>Occurs when File Locksmith is invoked.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FileLocksmith_InvokedRet</td>
<td>Triggered when File Locksmith invocation returns a result.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FileLocksmith_QueryContextMenuError</td>
<td>Occurs when there is an error querying the context menu for File Locksmith.</td>
</tr>
</table>
### Find My Mouse
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -583,7 +719,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Hosts File Editor
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -600,10 +740,22 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.HostsFileEditorOpenedEvent</td>
<td>Fires when Hosts File Editor is opened.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.HostEditorStartEvent</td>
<td>Triggered when the Hosts File Editor application starts. This logs the initialization of the Hosts File Editor UI with a timestamp.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.HostEditorStartFinishEvent</td>
<td>Triggered when the Hosts File Editor has completed loading and is ready for user interaction.</td>
</tr>
</table>
### Image Resizer
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -620,10 +772,18 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.ImageResizer_InvokedRet</td>
<td>Fires when the Image Resizer operation is completed and returns a result.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.ImageResizer_QueryContextMenuError</td>
<td>Triggered when there is an error querying the context menu for Image Resizer.</td>
</tr>
</table>
### Keyboard Manager
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -636,10 +796,22 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.KeyboardManager_AppSpecificShortcutRemapCount</td>
<td>Logs the number of application-specific shortcut remaps configured by the user.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.KeyboardManager_AppSpecificShortcutToKeyRemapInvoked</td>
<td>Logs each instance when an application-specific shortcut-to-key remap is used.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.KeyboardManager_AppSpecificShortcutToShortcutRemapInvoked</td>
<td>Logs each instance when an application-specific shortcut-to-shortcut remap is used.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.KeyboardManager_Error</td>
<td>Triggered when an error occurs in Keyboard Manager. This logs the error code, error message, and the method name where the error occurred.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.KeyboardManager_ErrorSendingKeyAndShortcutRemapLoadedConfiguration</td>
<td>Triggered when there is an error sending remapping configuration telemetry. This occurs when Keyboard Manager fails to report the loaded key and shortcut remap configurations</td>
</tr>
<tr>
<td>Microsoft.PowerToys.KeyboardManager_DailyAppSpecificShortcutToKeyRemapInvoked</td>
<td>Logs the daily count of application-specific shortcut-to-key remaps executed by the user.</td>
@@ -703,7 +875,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Light Switch
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -727,7 +903,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Mouse Highlighter
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -740,10 +920,18 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.MouseHighlighter_StartHighlightingSession</td>
<td>Occurs when a new highlighting session is started.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.MouseHighlighter_StartSpotlightSession</td>
<td>Triggered when a spotlight session is started in Mouse Highlighter. This occurs when the user activates the spotlight mode.</td>
</tr>
</table>
### Mouse Jump
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -767,7 +955,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Mouse Pointer Crosshairs
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -783,7 +975,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Mouse Without Borders
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -835,7 +1031,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### New+
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -867,7 +1067,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Peek
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -900,10 +1104,18 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.Peek_Settings</td>
<td>Triggered when the settings for Peek are modified.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.Peek_SpaceModeEnabled</td>
<td>Triggered when the Space key activation mode is enabled or disabled in Peek</td>
</tr>
</table>
### PowerRename
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -935,7 +1147,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### PowerToys Run
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -976,14 +1192,14 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.RunPluginsSettingsEvent</td>
<td>Triggered when the settings for PowerToys Run plugins are accessed or modified.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.WindowWalker_EnableWindowWalker</td>
<td>Triggered when the Window Walker plugin is enabled.</td>
</tr>
</table>
### Quick Accent
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -999,7 +1215,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Registry Preview
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -1012,10 +1232,22 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.RegistryPreview_EnableRegistryPreview</td>
<td>Occurs when Registry Preview is enabled.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.RegistryPreviewEditorStartEvent</td>
<td>Triggered when the Registry Preview application starts. This logs the initialization of the Registry Preview UI with a timestamp.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.RegistryPreviewEditorStartFinishEvent</td>
<td>Triggered when the Registry Preview application has completed loading and is ready for user interaction.</td>
</tr>
</table>
### Screen Ruler
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -1035,7 +1267,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Shortcut Guide
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -1051,7 +1287,11 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Text Extractor
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
@@ -1075,15 +1315,15 @@ _If you want to find diagnostic data events in the source code, these two links
</table>
### Workspaces
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.Projects_CLIUsage</td>
<td>Logs usage of command-line arguments for launching apps.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.Workspaces_CreateEvent</td>
<td>Triggered when a new workspace is created.</td>
@@ -1105,13 +1345,21 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Triggered when a workspace is launched.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.Workspaces_Settings</td>
<td>Logs changes to workspaces settings.</td>
<td>Microsoft.PowerToys.WorkspacesEditorStartEvent</td>
<td>Triggered when the Workspaces Editor application starts. This logs the initialization of the Workspaces Editor UI with a timestamp.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.WorkspacesEditorStartFinishEvent</td>
<td>Triggered when the Workspaces Editor has completed loading and is ready for user interaction.</td>
</tr>
</table>
### ZoomIt
<table style="width:100%">
<table style="width:100%; table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:60%">
</colgroup>
<tr>
<th>Event Name</th>
<th>Description</th>

View File

@@ -17,7 +17,6 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/common/COMUtils/COMUtils.vcxproj" Id="7319089e-46d6-4400-bc65-e39bdf1416ee" />
<Project Path="src/common/Display/Display.vcxproj" Id="caba8dfb-823b-4bf2-93ac-3f31984150d9" />
<Project Path="src/common/FilePreviewCommon/FilePreviewCommon.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
@@ -55,8 +54,6 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
</Folder>
<Folder Name="/common/interop/">
<Project Path="src/common/interop/interop-tests/Common.Interop.UnitTests.csproj">
@@ -116,8 +113,10 @@
<File Path="src/common/utils/string_utils.h" />
<File Path="src/common/utils/timeutil.h" />
<File Path="src/common/utils/UnhandledExceptionHandler.h" />
<File Path="src/common/utils/version.h" />
<File Path="src/common/utils/winapi_error.h" />
<File Path="src/common/utils/window.h" />
<File Path="version.h" />
</Folder>
<Folder Name="/DSC/">
<Project Path="src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj">
@@ -143,7 +142,6 @@
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj" Id="fc373b24-3293-453c-aaf5-cf2909dcee6a" />
</Folder>
<Folder Name="/modules/AdvancedPaste/Tests/">
<Project Path="src/modules/AdvancedPaste/AdvancedPaste.FuzzTests/AdvancedPaste.FuzzTests.csproj">
@@ -157,7 +155,6 @@
</Folder>
<Folder Name="/modules/AlwaysOnTop/">
<Project Path="src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj" Id="1dc3be92-ce89-43fb-8110-9c043a2fe7a2" />
<Project Path="src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.vcxproj" Id="48a0a19e-a0be-4256-acf8-cc3b80291af9" />
</Folder>
<Folder Name="/modules/awake/">
<Project Path="src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj">
@@ -168,19 +165,12 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/awake/AwakeModuleInterface/AwakeModuleInterface.vcxproj" Id="5e7360a8-d048-4ed3-8f09-0bfd64c5529a" />
</Folder>
<Folder Name="/modules/cmdNotFound/">
<Project Path="src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj" Id="0014d652-901f-4456-8d65-06fc5f997fb0" />
</Folder>
<Folder Name="/modules/colorpicker/">
<Project Path="src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/colorPicker/ColorPicker/ColorPicker.vcxproj" Id="655c9af2-18d3-4da6-80e4-85504a7722ba">
<BuildDependency Project="src/common/logger/logger.vcxproj" />
</Project>
<Project Path="src/modules/colorPicker/ColorPickerUI/ColorPickerUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -194,7 +184,6 @@
</Folder>
<Folder Name="/modules/CommandPalette/">
<Project Path="src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj" Id="5f63c743-f6ce-4dba-a200-2b3f8a14e8c2" />
<Project Path="src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj" Id="0adeb797-c8c7-4ffa-acd5-2af6cad7ecd8" />
</Folder>
<Folder Name="/modules/CommandPalette/Built-in Extensions/">
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj">
@@ -371,14 +360,12 @@
</Folder>
<Folder Name="/modules/CropAndLock/">
<Project Path="src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj" Id="f5e1146e-b7b3-4e11-85fd-270a500bd78c" />
<Project Path="src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj" Id="3157fa75-86cf-4ee2-8f62-c43f776493c6" />
</Folder>
<Folder Name="/modules/EnvironmentVariables/">
<Project Path="src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj" Id="b9420661-b0e4-4241-abd4-4a27a1f64250" />
<Project Path="src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj" />
</Folder>
<Folder Name="/modules/fancyzones/">
@@ -396,7 +383,6 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj" Id="f9c68edf-ac74-4b77-9af1-005d9c9f6a99" />
<Project Path="src/modules/fancyzones/FancyZonesModuleInterface/FancyZonesModuleInterface.vcxproj" Id="48804216-2a0e-4168-a6d8-9cd068d14227" />
</Folder>
<Folder Name="/modules/fancyzones/Tests/">
<Project Path="src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj">
@@ -419,8 +405,121 @@
<BuildDependency Project="src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj" />
</Project>
</Folder>
<Folder Name="/modules/File Explorer/">
<Project Path="src/modules/previewpane/BgcodePreviewHandler/BgcodePreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/BgcodeThumbnailProvider/BgcodeThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/Common/PreviewHandlerCommon.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/FileExplorerDllExporter/FileExplorerDllExporter.vcxproj" Id="f6088a11-1c9e-4420-aa90-cf7e78dd7f1c" />
<Project Path="src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/MarkdownPreviewHandler/MarkdownPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/PdfPreviewHandler/PdfPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/PdfThumbnailProvider/PdfThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/StlThumbnailProvider/StlThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/SvgPreviewHandler/SvgPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/File Explorer/Tests/">
<Project Path="src/modules/previewpane/UnitTests-BgcodePreviewHandler/Preview.BgcodePreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-BgcodeThumbnailProvider/Preview.BgcodeThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-GcodePreviewHandler/Preview.GcodePreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-GcodeThumbnailProvider/Preview.GcodeThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-MarkdownPreviewHandler/Preview.MarkdownPreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-PdfPreviewHandler/Preview.PdfPreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-PdfThumbnailProvider/Preview.PdfThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-PreviewHandlerCommon/Preview.PreviewHandlerCommon.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-QoiPreviewHandler/Preview.QoiPreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-QoiThumbnailProvider/Preview.QoiThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-StlThumbnailProvider/Preview.StlThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-SvgPreviewHandler/Preview.SvgPreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-SvgThumbnailProvider/Preview.SvgThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/FileLocksmith/">
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj" Id="49D456D3-F485-45AF-8875-45B44F193DDC" />
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj" Id="49d456d3-f485-45af-8875-45b44f193ddc" />
<Project Path="src/modules/FileLocksmith/FileLocksmithContextMenu/FileLocksmithContextMenu.vcxproj" Id="799a50d8-de89-4ed1-8ff8-ad5a9ed8c0ca" />
<Project Path="src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj" Id="57175ec7-92a5-4c1e-8244-e3fbca2a81de" />
<Project Path="src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj" Id="9d52fd25-ef90-4f9a-a015-91efc5daf54f" />
@@ -431,14 +530,13 @@
</Project>
</Folder>
<Folder Name="/modules/FileLocksmith/Tests/">
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj" Id="A1B2C3D4-E5F6-7890-1234-567890ABCDEF" />
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-1234-567890abcdef" />
</Folder>
<Folder Name="/modules/Hosts/">
<Project Path="src/modules/Hosts/Hosts/Hosts.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.vcxproj" Id="b41b888c-7db8-4747-b262-4062e05a230d" />
<Project Path="src/modules/Hosts/HostsUILib/HostsUILib.csproj" />
</Folder>
<Folder Name="/modules/Hosts/Tests/">
@@ -457,13 +555,13 @@
</Folder>
<Folder Name="/modules/imageresizer/">
<Project Path="src/modules/imageresizer/dll/ImageResizerExt.vcxproj" Id="0b43679e-edfa-4da0-ad30-f4628b308b1b" />
<Project Path="src/modules/imageresizer/ImageResizerContextMenu/ImageResizerContextMenu.vcxproj" Id="93b72a06-c8bd-484f-a6f7-c9f280b150bf" />
<Project Path="src/modules/imageresizer/ImageResizerLib/ImageResizerLib.vcxproj" Id="18b3db45-4ffe-4d01-97d6-5223feee1853" />
<Project Path="src/modules/imageresizer/ui/ImageResizerUI.csproj">
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Project Path="src/modules/imageresizer/ImageResizerContextMenu/ImageResizerContextMenu.vcxproj" Id="93b72a06-c8bd-484f-a6f7-c9f280b150bf" />
<Project Path="src/modules/imageresizer/ImageResizerLib/ImageResizerLib.vcxproj" Id="18b3db45-4ffe-4d01-97d6-5223feee1853" />
<Project Path="src/modules/imageresizer/ui/ImageResizerUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
@@ -479,7 +577,6 @@
</Folder>
<Folder Name="/modules/keyboardmanager/">
<Project Path="src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj" Id="8affa899-0b73-49ec-8c50-0fadda57b2fc" />
<Project Path="src/modules/keyboardmanager/dll/KeyboardManager.vcxproj" Id="89f34af7-1c34-4a72-aa6e-534bcf972bd9" />
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditor/KeyboardManagerEditor.vcxproj" Id="8df78b53-200e-451f-9328-01eb907193ae" />
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditorLibrary/KeyboardManagerEditorLibrary.vcxproj" Id="23d2070d-e4ad-4add-85a7-083d9c76ad49" />
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditorLibraryWrapper/KeyboardManagerEditorLibraryWrapper.vcxproj" Id="4382a954-179a-4078-92af-715187dfff50" />
@@ -495,9 +592,6 @@
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineTest/KeyboardManagerEngineTest.vcxproj" Id="7f4b3a60-bc27-45a7-8000-68b0b6ea7466" />
</Folder>
<Folder Name="/modules/launcher/">
<Project Path="src/modules/launcher/Microsoft.Launcher/Microsoft.Launcher.vcxproj" Id="e364f67b-bb12-4e91-b639-355866ebcd8b">
<BuildDependency Project="src/modules/launcher/PowerLauncher/PowerLauncher.csproj" />
</Project>
<Project Path="src/modules/launcher/PowerLauncher.Telemetry/PowerLauncher.Telemetry.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -665,8 +759,6 @@
</Project>
</Folder>
<Folder Name="/modules/LightSwitch/">
<Project Path="src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj" Id="79267138-2895-4346-9021-21408d65379f" />
<Project Path="src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj" Id="38177d56-6ad1-4adf-88c9-2843a7932166" />
<Project Path="src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj" Id="08e71c67-6a7e-4ca1-b04e-2fb336410bac" />
</Folder>
<Folder Name="/modules/LightSwitch/Tests/">
@@ -680,9 +772,7 @@
<Project Path="src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj" Id="54a93af7-60c7-4f6c-99d2-fbb1f75f853a">
<BuildDependency Project="src/common/Display/Display.vcxproj" />
<BuildDependency Project="src/common/SettingsAPI/SettingsAPI.vcxproj" />
<BuildDependency Project="src/common/version/version.vcxproj" />
</Project>
<Project Path="src/modules/MeasureTool/MeasureToolModuleInterface/MeasureToolModuleInterface.vcxproj" Id="92c39820-9f84-4529-bc7d-22aae514d63b" />
<Project Path="src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -702,7 +792,6 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseJump/MouseJump.vcxproj" Id="8a08d663-4995-40e3-b42c-3f910625f284" />
<Project Path="src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -774,14 +863,12 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/poweraccent/PowerAccentKeyboardService/PowerAccentKeyboardService.vcxproj" Id="c97d9a5d-206c-454e-997e-009e227d7f02" />
<Project Path="src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj" Id="34a354c5-23c7-4343-916c-c52daf4fc39d" />
</Folder>
<Folder Name="/modules/PowerOCR/">
<Project Path="src/modules/PowerOCR/PowerOCR/PowerOCR.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/PowerOCR/PowerOCRModuleInterface/PowerOCRModuleInterface.vcxproj" Id="6ab6a2d6-f859-4a82-9184-0bd29c9f07d1" />
</Folder>
<Folder Name="/modules/PowerOCR/Tests/">
<Project Path="src/modules/PowerOCR/PowerOCR-UITests/PowerOCR.UITests.csproj">
@@ -811,140 +898,11 @@
<BuildDependency Project="src/modules/powerrename/lib/PowerRenameLib.vcxproj" />
</Project>
</Folder>
<Folder Name="/modules/previewpane/">
<Project Path="src/modules/previewpane/BgcodePreviewHandler/BgcodePreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/BgcodePreviewHandlerCpp/BgcodePreviewHandlerCpp.vcxproj" Id="f6088a11-1c9e-4420-aa90-cf7e78dd7f1c" />
<Project Path="src/modules/previewpane/BgcodeThumbnailProvider/BgcodeThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/BgcodeThumbnailProviderCpp/BgcodeThumbnailProviderCpp.vcxproj" Id="47b0678c-806b-4fe1-9f50-46ba88989532" />
<Project Path="src/modules/previewpane/Common/PreviewHandlerCommon.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/GcodePreviewHandlerCpp/GcodePreviewHandlerCpp.vcxproj" Id="5a5dd09d-723a-44d3-8f2b-293584c3d731" />
<Project Path="src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/GcodeThumbnailProviderCpp/GcodeThumbnailProviderCpp.vcxproj" Id="56cc2f10-6e41-453d-be16-c593a5e58482" />
<Project Path="src/modules/previewpane/MarkdownPreviewHandler/MarkdownPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/MarkdownPreviewHandlerCpp/MarkdownPreviewHandlerCpp.vcxproj" Id="ed9a1ac6-aeb0-4569-a6e9-e1696182b545" />
<Project Path="src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/MonacoPreviewHandlerCpp/MonacoPreviewHandlerCpp.vcxproj" Id="b3e869c4-8210-4ebd-a621-ff4c4afcbfa9" />
<Project Path="src/modules/previewpane/PdfPreviewHandler/PdfPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/PdfPreviewHandlerCpp/PdfPreviewHandlerCpp.vcxproj" Id="54f7c616-fd41-4e62-bff9-015686914f4d" />
<Project Path="src/modules/previewpane/PdfThumbnailProvider/PdfThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/PdfThumbnailProviderCpp/PdfThumbnailProviderCpp.vcxproj" Id="ca5518ed-0458-4b09-8f53-4122b9888655" />
<Project Path="src/modules/previewpane/powerpreview/powerpreview.vcxproj" Id="217df501-135c-4e38-bfc8-99d4821032ea">
<BuildDependency Project="src/common/version/version.vcxproj" />
</Project>
<Project Path="src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/QoiPreviewHandlerCpp/QoiPreviewHandlerCpp.vcxproj" Id="3baf9c81-a194-4925-a035-5e24a5d1e542" />
<Project Path="src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/QoiThumbnailProviderCpp/QoiThumbnailProviderCpp.vcxproj" Id="ccb5e44f-84d9-4203-83c6-1c9ec9302bc7" />
<Project Path="src/modules/previewpane/StlThumbnailProvider/StlThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/StlThumbnailProviderCpp/StlThumbnailProviderCpp.vcxproj" Id="d6dcc3ae-18c0-488a-b978-baa9e3cff09d" />
<Project Path="src/modules/previewpane/SvgPreviewHandler/SvgPreviewHandler.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/SvgPreviewHandlerCpp/SvgPreviewHandlerCpp.vcxproj" Id="143f13e3-d2e3-4d83-b035-356612d99956" />
<Project Path="src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/SvgThumbnailProviderCpp/SvgThumbnailProviderCpp.vcxproj" Id="2bbc9e33-21ec-401c-84da-bb6590a9b2aa" />
</Folder>
<Folder Name="/modules/previewpane/Tests/">
<Project Path="src/modules/previewpane/UnitTests-BgcodePreviewHandler/Preview.BgcodePreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-BgcodeThumbnailProvider/Preview.BgcodeThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-GcodePreviewHandler/Preview.GcodePreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-GcodeThumbnailProvider/Preview.GcodeThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-MarkdownPreviewHandler/Preview.MarkdownPreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-PdfPreviewHandler/Preview.PdfPreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-PdfThumbnailProvider/Preview.PdfThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-PreviewHandlerCommon/Preview.PreviewHandlerCommon.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-QoiPreviewHandler/Preview.QoiPreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-QoiThumbnailProvider/Preview.QoiThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-StlThumbnailProvider/Preview.StlThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-SvgPreviewHandler/Preview.SvgPreviewHandler.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/previewpane/UnitTests-SvgThumbnailProvider/Preview.SvgThumbnailProvider.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/RegistryPreview/">
<Project Path="src/modules/registrypreview/RegistryPreview/RegistryPreview.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.vcxproj" Id="697c6af9-0a48-49a9-866c-67da12384015" />
<Project Path="src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewUILib.csproj" />
</Folder>
<Folder Name="/modules/RegistryPreview/Test/">
@@ -976,7 +934,6 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj" Id="b31fcc55-b5a4-4ea7-b414-2dceae6af332" />
<Project Path="src/modules/Workspaces/WorkspacesModuleInterface/WorkspacesModuleInterface.vcxproj" Id="45285df2-9742-4eca-9ac9-58951fc26489" />
<Project Path="src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj" Id="3d63307b-9d27-44fd-b033-b26f39245b85" />
<Project Path="src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj" Id="37d07516-4185-43a4-924f-3c7a5d95ecf6" />
</Folder>
@@ -1002,7 +959,6 @@
</Folder>
<Folder Name="/modules/ZoomIt/">
<Project Path="src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj" Id="0a84f764-3a88-44cd-aa96-41bdbd48627b" />
<Project Path="src/modules/ZoomIt/ZoomItModuleInterface/ZoomItModuleInterface.vcxproj" Id="e4585179-2ac1-4d5f-a3ff-cfc5392f694c" />
<Project Path="src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettingsInterop.vcxproj" Id="ca7d8106-30b9-4aec-9d05-b69b31b8c461" />
</Folder>
<Folder Name="/settings-ui/">
@@ -1047,37 +1003,35 @@
<File Path="src/Solution.props" />
<File Path="src/Version.props" />
</Folder>
<Project Path="src/ActionRunner/ActionRunner.vcxproj" Id="d29ddd63-e2cf-4657-9fd5-2aede4257e5d">
<BuildDependency Project="src/common/updating/updating.vcxproj" />
</Project>
<Project Path="src/PackageIdentity/PackageIdentity.vcxproj" Id="e2a5a82e-1e5b-4c8d-9a4f-2b1a8f9e5c3d" />
<Project Path="src/PowerToys.ActionRunner/PowerToys.ActionRunner.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/runner/runner.vcxproj" Id="9412d5c6-2cf2-4fc2-a601-b55508ea9b27">
<BuildDependency Project="src/ActionRunner/ActionRunner.vcxproj" />
<BuildDependency Project="src/common/notifications/BackgroundActivator/BackgroundActivator.vcxproj" />
<BuildDependency Project="src/common/notifications/BackgroundActivatorDLL/BackgroundActivatorDLL.vcxproj" />
<BuildDependency Project="src/common/updating/updating.vcxproj" />
<BuildDependency Project="src/modules/awake/Awake/Awake.csproj" />
<BuildDependency Project="src/modules/awake/AwakeModuleInterface/AwakeModuleInterface.vcxproj" />
<BuildDependency Project="src/modules/colorPicker/ColorPicker/ColorPicker.vcxproj" />
<BuildDependency Project="src/modules/colorPicker/ColorPickerUI/ColorPickerUI.csproj" />
<BuildDependency Project="src/modules/fancyzones/editor/FancyZonesEditor/FancyZonesEditor.csproj" />
<BuildDependency Project="src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj" />
<BuildDependency Project="src/modules/fancyzones/FancyZonesModuleInterface/FancyZonesModuleInterface.vcxproj" />
<BuildDependency Project="src/modules/imageresizer/dll/ImageResizerExt.vcxproj" />
<BuildDependency Project="src/modules/imageresizer/ui/ImageResizerUI.csproj" />
<BuildDependency Project="src/modules/keyboardmanager/dll/KeyboardManager.vcxproj" />
<BuildDependency Project="src/modules/launcher/Microsoft.Launcher/Microsoft.Launcher.vcxproj" />
<BuildDependency Project="src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj" />
<BuildDependency Project="src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj" />
<BuildDependency Project="src/modules/powerrename/dll/PowerRenameExt.vcxproj" />
<BuildDependency Project="src/modules/powerrename/lib/PowerRenameLib.vcxproj" />
<BuildDependency Project="src/modules/previewpane/Common/PreviewHandlerCommon.csproj" />
<BuildDependency Project="src/modules/previewpane/MarkdownPreviewHandler/MarkdownPreviewHandler.csproj" />
<BuildDependency Project="src/modules/previewpane/PdfPreviewHandler/PdfPreviewHandler.csproj" />
<BuildDependency Project="src/modules/previewpane/powerpreview/powerpreview.vcxproj" />
<BuildDependency Project="src/modules/previewpane/SvgPreviewHandler/SvgPreviewHandler.csproj" />
<BuildDependency Project="src/PackageIdentity/PackageIdentity.vcxproj" />
</Project>
<Project Path="src/Update/PowerToys.Update.vcxproj" Id="44ce9ae1-4390-42c5-bacc-0fd6b40aa203" />
<Project Path="tools/project_template/ModuleTemplate/ModuleTemplateCompileTest.vcxproj" Id="64a80062-4d8b-4229-8a38-dfa1d7497749" />
<Project Path="src/RunnerV2/RunnerV2/RunnerV2.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/Update/Update.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Solution>

View File

@@ -1,244 +0,0 @@
# Windows App SDK Semantic Search API 总结
## 1. 环境与依赖
| 项目 | 版本/值 |
|------|---------|
| **Windows App SDK** | `2.0.0-experimental3` |
| **.NET** | `net9.0-windows10.0.26100.0` |
| **AI Search NuGet** | `Microsoft.WindowsAppSDK.AI` (2.0.57-experimental) |
| **命名空间** | `Microsoft.Windows.AI.Search.Experimental.AppContentIndex` |
| **应用类型** | WinUI 3 MSIX 打包应用 |
---
## 2. 核心 API
### 2.1 索引管理
```csharp
// 创建/打开索引
var result = AppContentIndexer.GetOrCreateIndex("indexName");
if (result.Succeeded) {
_indexer = result.Indexer;
// result.Status: CreatedNew | OpenedExisting
}
// 等待索引能力就绪
await _indexer.WaitForIndexCapabilitiesAsync();
// 等待索引空闲(建索引完成)
await _indexer.WaitForIndexingIdleAsync(TimeSpan.FromSeconds(120));
// 清理
_indexer.RemoveAll(); // 删除所有索引
_indexer.Remove(id); // 删除单个
_indexer.Dispose();
```
### 2.2 添加内容到索引
```csharp
// 索引文本 → 自动建立 TextLexical + TextSemantic 索引
IndexableAppContent textContent = AppManagedIndexableAppContent.CreateFromString(id, text);
_indexer.AddOrUpdate(textContent);
// 索引图片 → 自动建立 ImageSemantic + ImageOcr 索引
IndexableAppContent imageContent = AppManagedIndexableAppContent.CreateFromBitmap(id, softwareBitmap);
_indexer.AddOrUpdate(imageContent);
```
### 2.3 查询
```csharp
// 文本查询
TextQueryOptions options = new TextQueryOptions {
Language = "en-US", // 可选
MatchScope = QueryMatchScope.Unconstrained, // 匹配范围
TextMatchType = TextLexicalMatchType.Fuzzy // Fuzzy | Exact
};
AppIndexTextQuery query = _indexer.CreateTextQuery(searchText, options);
IReadOnlyList<TextQueryMatch> matches = query.GetNextMatches(5);
// 图片查询
ImageQueryOptions imgOptions = new ImageQueryOptions {
MatchScope = QueryMatchScope.Unconstrained,
ImageOcrTextMatchType = TextLexicalMatchType.Fuzzy
};
AppIndexImageQuery imgQuery = _indexer.CreateImageQuery(searchText, imgOptions);
IReadOnlyList<ImageQueryMatch> imgMatches = imgQuery.GetNextMatches(5);
```
### 2.4 能力检查(只读)
```csharp
IndexCapabilities capabilities = _indexer.GetIndexCapabilities();
bool textLexicalOK = capabilities.GetCapabilityState(IndexCapability.TextLexical)
.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
bool textSemanticOK = capabilities.GetCapabilityState(IndexCapability.TextSemantic)
.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
bool imageSemanticOK = capabilities.GetCapabilityState(IndexCapability.ImageSemantic)
.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
bool imageOcrOK = capabilities.GetCapabilityState(IndexCapability.ImageOcr)
.InitializationStatus == IndexCapabilityInitializationStatus.Initialized;
```
---
## 3. 四种索引能力
| 能力 | 说明 | 触发方式 |
|------|------|----------|
| `TextLexical` | 词法/关键词搜索 | CreateFromString() 自动 |
| `TextSemantic` | AI 语义搜索 (Embedding) | CreateFromString() 自动 |
| `ImageSemantic` | 图像语义搜索 | CreateFromBitmap() 自动 |
| `ImageOcr` | 图片 OCR 文字搜索 | CreateFromBitmap() 自动 |
---
## 4. 可控选项(有限)
### TextQueryOptions
| 属性 | 类型 | 说明 |
|------|------|------|
| `Language` | string | 查询语言(可选,如 "en-US"|
| `MatchScope` | QueryMatchScope | Unconstrained / Region / ContentItem |
| `TextMatchType` | TextLexicalMatchType | **Fuzzy** / Exact仅影响 Lexical|
### ImageQueryOptions
| 属性 | 类型 | 说明 |
|------|------|------|
| `MatchScope` | QueryMatchScope | Unconstrained / Region / ContentItem |
| `ImageOcrTextMatchType` | TextLexicalMatchType | **Fuzzy** / Exact仅影响 OCR|
### 枚举值说明
**QueryMatchScope:**
- `Unconstrained` - 无约束,同时使用 Lexical + Semantic
- `Region` - 限制在特定区域
- `ContentItem` - 限制在单个内容项
**TextLexicalMatchType:**
- `Fuzzy` - 模糊匹配,允许拼写错误、近似词
- `Exact` - 精确匹配,必须完全一致
---
## 5. 关键限制 ⚠️
| 限制 | 说明 |
|------|------|
| **不能单独指定 Semantic/Lexical** | 系统自动同时使用所有可用能力 |
| **Fuzzy/Exact 只影响 Lexical** | 对 Semantic 搜索无效 |
| **能力检查是只读的** | `GetIndexCapabilities()` 只能查看,不能控制 |
| **无相似度阈值** | 不能设置 Semantic 匹配的阈值 |
| **无结果排序控制** | 无法指定按相关度或其他方式排序 |
| **语言需手动传** | 不会自动检测,需开发者指定 |
| **无相关度分数** | 查询结果不返回匹配分数 |
---
## 6. 典型使用流程
```
┌─────────────────────────────────────────────────────────┐
│ App 启动时 │
├─────────────────────────────────────────────────────────┤
│ 1. GetOrCreateIndex("name") // 创建/打开索引 │
│ 2. WaitForIndexCapabilitiesAsync() // 等待能力就绪 │
│ 3. GetIndexCapabilities() // 检查可用能力 │
│ 4. IndexAll() // 索引所有数据 │
│ ├─ CreateFromString() × N │
│ └─ CreateFromBitmap() × N │
│ 5. WaitForIndexingIdleAsync() // 等待索引完成 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 运行时查询 │
├─────────────────────────────────────────────────────────┤
│ 1. CreateTextQuery(text, options) // 创建查询 │
│ 2. query.GetNextMatches(N) // 获取结果 │
│ 3. 处理 TextQueryMatch / ImageQueryMatch │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ App 退出时 │
├─────────────────────────────────────────────────────────┤
│ 1. _indexer.RemoveAll() // 清理索引 │
│ 2. _indexer.Dispose() // 释放资源 │
└─────────────────────────────────────────────────────────┘
```
---
## 7. 查询结果处理
```csharp
// 文本查询结果
foreach (var match in textMatches)
{
if (match.ContentKind == QueryMatchContentKind.AppManagedText)
{
AppManagedTextQueryMatch textResult = (AppManagedTextQueryMatch)match;
string contentId = match.ContentId; // 内容 ID
int offset = textResult.TextOffset; // 匹配文本偏移
int length = textResult.TextLength; // 匹配文本长度
}
}
// 图片查询结果
foreach (var match in imageMatches)
{
if (match.ContentKind == QueryMatchContentKind.AppManagedImage)
{
AppManagedImageQueryMatch imageResult = (AppManagedImageQueryMatch)match;
string contentId = imageResult.ContentId; // 图片 ID
}
}
```
---
## 8. 能力变化监听
```csharp
// 监听索引能力变化
_indexer.Listener.IndexCapabilitiesChanged += (indexer, capabilities) =>
{
// 重新检查能力状态,更新 UI
LoadAppIndexCapabilities();
};
```
---
## 9. 结论
这是一个**高度封装的黑盒 API**
### 优点 ✅
- 简单易用,几行代码即可实现搜索
- 自动处理 Lexical + Semantic
- 支持文本和图片多模态搜索
- 系统级集成,无需额外部署模型
### 缺点 ❌
- 无法精细控制搜索类型
- 不能只用 Semantic Search
- 选项有限,缺乏高级配置
- 实验性 API可能变更
### 替代方案
**如果需要纯 Semantic Search向量搜索**,建议:
- 直接使用 Embedding 模型生成向量
- 配合向量数据库Azure Cosmos DB、FAISS、Qdrant 等)
---
## 10. 相关 NuGet 包
```xml
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.0-experimental3" />
<PackageReference Include="Microsoft.WindowsAppSDK.AI" Version="2.0.57-experimental" />
```
---
*文档生成日期: 2026-01-21*

View File

@@ -1,496 +0,0 @@
# Common.Search Library Specification
## Overview
本文档描述 `Common.Search` 库的重构设计目标是提供一个通用的、可插拔的搜索框架支持多种搜索引擎实现Fuzzy Match、Semantic Search 等)。
## Goals
1. **解耦** - 搜索引擎与数据源完全解耦
2. **可插拔** - 支持替换不同的搜索引擎实现
3. **泛型** - 不绑定特定业务类型(如 SettingEntry
4. **可组合** - 支持多引擎组合(即时 Fuzzy + 延迟 Semantic
5. **可复用** - 可被 PowerToys 多个模块使用
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Consumer (e.g., Settings.UI) │
├─────────────────────────────────────────────────────────────────┤
│ SettingsDataProvider ← 业务特定的数据加载 │
│ SettingsSearchService ← 业务特定的搜索服务 │
│ SettingEntry : ISearchable ← 业务实体实现搜索契约 │
└─────────────────────────────────────────────────────────────────┘
│ uses
┌─────────────────────────────────────────────────────────────────┐
│ Common.Search (Library) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Core Abstractions │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ ISearchable ← 可搜索内容契约 │ │
│ │ ISearchEngine<T> ← 搜索引擎接口 │ │
│ │ SearchResult<T> ← 统一结果模型 │ │
│ │ SearchOptions ← 搜索选项 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Implementations │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ FuzzSearch/ │ │
│ │ ├── FuzzSearchEngine<T> ← 内存 Fuzzy 搜索 │ │
│ │ ├── StringMatcher ← 现有的模糊匹配算法 │ │
│ │ └── MatchResult ← Fuzzy 匹配结果 │ │
│ │ │ │
│ │ SemanticSearch/ │ │
│ │ ├── SemanticSearchEngine ← Windows AI Search 封装 │ │
│ │ └── SemanticSearchCapabilities │ │
│ │ │ │
│ │ CompositeSearch/ │ │
│ │ └── CompositeSearchEngine<T> ← 多引擎组合 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Core Interfaces
### ISearchable
定义可搜索内容的最小契约。
```csharp
namespace Common.Search;
/// <summary>
/// Defines a searchable item that can be indexed and searched.
/// </summary>
public interface ISearchable
{
/// <summary>
/// Gets the unique identifier for this item.
/// </summary>
string Id { get; }
/// <summary>
/// Gets the primary searchable text (e.g., title, header).
/// </summary>
string SearchableText { get; }
/// <summary>
/// Gets optional secondary searchable text (e.g., description).
/// Returns null if not available.
/// </summary>
string? SecondarySearchableText { get; }
}
```
### ISearchEngine&lt;T&gt;
搜索引擎核心接口。
```csharp
namespace Common.Search;
/// <summary>
/// Defines a pluggable search engine that can index and search items.
/// </summary>
/// <typeparam name="T">The type of items to search, must implement ISearchable.</typeparam>
public interface ISearchEngine<T> : IDisposable
where T : ISearchable
{
/// <summary>
/// Gets a value indicating whether the engine is ready to search.
/// </summary>
bool IsReady { get; }
/// <summary>
/// Gets the engine capabilities.
/// </summary>
SearchEngineCapabilities Capabilities { get; }
/// <summary>
/// Initializes the search engine.
/// </summary>
Task InitializeAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Indexes a single item.
/// </summary>
Task IndexAsync(T item, CancellationToken cancellationToken = default);
/// <summary>
/// Indexes multiple items in batch.
/// </summary>
Task IndexBatchAsync(IEnumerable<T> items, CancellationToken cancellationToken = default);
/// <summary>
/// Removes an item from the index by its ID.
/// </summary>
Task RemoveAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Clears all indexed items.
/// </summary>
Task ClearAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Searches for items matching the query.
/// </summary>
Task<IReadOnlyList<SearchResult<T>>> SearchAsync(
string query,
SearchOptions? options = null,
CancellationToken cancellationToken = default);
}
```
### SearchResult&lt;T&gt;
统一的搜索结果模型。
```csharp
namespace Common.Search;
/// <summary>
/// Represents a search result with the matched item and scoring information.
/// </summary>
public sealed class SearchResult<T>
where T : ISearchable
{
/// <summary>
/// Gets the matched item.
/// </summary>
public required T Item { get; init; }
/// <summary>
/// Gets the relevance score (higher is more relevant).
/// </summary>
public required double Score { get; init; }
/// <summary>
/// Gets the type of match that produced this result.
/// </summary>
public required SearchMatchKind MatchKind { get; init; }
/// <summary>
/// Gets the match details for highlighting (optional).
/// </summary>
public IReadOnlyList<MatchSpan>? MatchSpans { get; init; }
}
/// <summary>
/// Represents a span of matched text for highlighting.
/// </summary>
public readonly record struct MatchSpan(int Start, int Length);
/// <summary>
/// Specifies the kind of match that produced a search result.
/// </summary>
public enum SearchMatchKind
{
/// <summary>Exact text match.</summary>
Exact,
/// <summary>Fuzzy/approximate text match.</summary>
Fuzzy,
/// <summary>Semantic/AI-based match.</summary>
Semantic,
/// <summary>Combined match from multiple engines.</summary>
Composite,
}
```
### SearchOptions
搜索配置选项。
```csharp
namespace Common.Search;
/// <summary>
/// Options for configuring search behavior.
/// </summary>
public sealed class SearchOptions
{
/// <summary>
/// Gets or sets the maximum number of results to return.
/// Default is 20.
/// </summary>
public int MaxResults { get; set; } = 20;
/// <summary>
/// Gets or sets the minimum score threshold (0.0 to 1.0).
/// Results below this score are filtered out.
/// Default is 0.0 (no filtering).
/// </summary>
public double MinScore { get; set; } = 0.0;
/// <summary>
/// Gets or sets the language hint for the search (e.g., "en-US").
/// </summary>
public string? Language { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to include match spans for highlighting.
/// Default is false.
/// </summary>
public bool IncludeMatchSpans { get; set; } = false;
}
```
### SearchEngineCapabilities
引擎能力描述。
```csharp
namespace Common.Search;
/// <summary>
/// Describes the capabilities of a search engine.
/// </summary>
public sealed class SearchEngineCapabilities
{
/// <summary>
/// Gets a value indicating whether the engine supports fuzzy matching.
/// </summary>
public bool SupportsFuzzyMatch { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports semantic search.
/// </summary>
public bool SupportsSemanticSearch { get; init; }
/// <summary>
/// Gets a value indicating whether the engine persists the index to disk.
/// </summary>
public bool PersistsIndex { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports incremental indexing.
/// </summary>
public bool SupportsIncrementalIndex { get; init; }
/// <summary>
/// Gets a value indicating whether the engine supports match span highlighting.
/// </summary>
public bool SupportsMatchSpans { get; init; }
}
```
## Implementations
### FuzzSearchEngine&lt;T&gt;
基于现有 StringMatcher 的内存搜索引擎。
**特点:**
- 纯内存,无持久化
- 即时响应(毫秒级)
- 支持 match spans 高亮
- 基于字符的模糊匹配
**Capabilities**
```csharp
new SearchEngineCapabilities
{
SupportsFuzzyMatch = true,
SupportsSemanticSearch = false,
PersistsIndex = false,
SupportsIncrementalIndex = true,
SupportsMatchSpans = true,
}
```
### SemanticSearchEngine
基于 Windows App SDK AI Search API 的语义搜索引擎。
**特点:**
- 系统管理的持久化索引
- AI 驱动的语义理解
- 需要模型初始化(可能较慢)
- 可能不可用(依赖系统支持)
**Capabilities**
```csharp
new SearchEngineCapabilities
{
SupportsFuzzyMatch = true, // API 同时提供 lexical + semantic
SupportsSemanticSearch = true,
PersistsIndex = true,
SupportsIncrementalIndex = true,
SupportsMatchSpans = false, // API 不返回详细位置
}
```
**注意:** SemanticSearchEngine 不是泛型的,因为它需要将内容转换为字符串存入系统索引。实现时通过 `ISearchable` 接口提取文本。
### CompositeSearchEngine&lt;T&gt;
组合多个搜索引擎,支持 fallback 和结果合并。
```csharp
namespace Common.Search;
/// <summary>
/// A search engine that combines results from multiple engines.
/// </summary>
public sealed class CompositeSearchEngine<T> : ISearchEngine<T>
where T : ISearchable
{
/// <summary>
/// Strategy for combining results from multiple engines.
/// </summary>
public enum CombineStrategy
{
/// <summary>Use first ready engine only.</summary>
FirstReady,
/// <summary>Merge results from all ready engines.</summary>
MergeAll,
/// <summary>Use primary, fallback to secondary if primary not ready.</summary>
PrimaryWithFallback,
}
}
```
**典型用法:** Fuzzy 作为即时响应Semantic 准备好后增强结果。
## Directory Structure
```
src/common/Common.Search/
├── Common.Search.csproj
├── GlobalSuppressions.cs
├── ISearchable.cs
├── ISearchEngine.cs
├── SearchResult.cs
├── SearchOptions.cs
├── SearchEngineCapabilities.cs
├── SearchMatchKind.cs
├── MatchSpan.cs
├── FuzzSearch/
│ ├── FuzzSearchEngine.cs
│ ├── StringMatcher.cs (existing)
│ ├── MatchOption.cs (existing)
│ ├── MatchResult.cs (existing)
│ └── SearchPrecisionScore.cs (existing)
├── SemanticSearch/
│ ├── SemanticSearchEngine.cs
│ ├── SemanticSearchCapabilities.cs
│ └── SemanticSearchAdapter.cs (adapts ISearchable to Windows API)
└── CompositeSearch/
└── CompositeSearchEngine.cs
```
## Consumer Usage (Settings.UI)
### SettingEntry 实现 ISearchable
```csharp
// Settings.UI.Library/SettingEntry.cs
public struct SettingEntry : ISearchable
{
// Existing properties...
// ISearchable implementation
public string Id => ElementUid ?? $"{PageTypeName}|{ElementName}";
public string SearchableText => Header ?? string.Empty;
public string? SecondarySearchableText => Description;
}
```
### SettingsSearchService
```csharp
// Settings.UI/Services/SettingsSearchService.cs
public sealed class SettingsSearchService : IDisposable
{
private readonly ISearchEngine<SettingEntry> _engine;
public SettingsSearchService(ISearchEngine<SettingEntry> engine)
{
_engine = engine;
}
public async Task InitializeAsync(IEnumerable<SettingEntry> entries)
{
await _engine.InitializeAsync();
await _engine.IndexBatchAsync(entries);
}
public async Task<List<SettingEntry>> SearchAsync(string query, CancellationToken ct = default)
{
var results = await _engine.SearchAsync(query, cancellationToken: ct);
return results.Select(r => r.Item).ToList();
}
}
```
### Startup Configuration
```csharp
// Option 1: Fuzzy only (default, immediate)
var engine = new FuzzSearchEngine<SettingEntry>();
// Option 2: Semantic only (requires Windows AI)
var engine = new SemanticSearchAdapter<SettingEntry>("PowerToysSettings");
// Option 3: Composite (best of both worlds)
var engine = new CompositeSearchEngine<SettingEntry>(
primary: new SemanticSearchAdapter<SettingEntry>("PowerToysSettings"),
fallback: new FuzzSearchEngine<SettingEntry>(),
strategy: CombineStrategy.PrimaryWithFallback
);
var searchService = new SettingsSearchService(engine);
await searchService.InitializeAsync(settingEntries);
```
## Migration Plan
### Phase 1: Core Abstractions
1. 创建 `ISearchable`, `ISearchEngine<T>`, `SearchResult<T>` 等核心接口
2. 保持现有 FuzzSearch 代码不变
### Phase 2: FuzzSearchEngine&lt;T&gt;
1. 创建泛型 `FuzzSearchEngine<T>` 实现
2. 内部复用现有 `StringMatcher`
### Phase 3: SemanticSearchEngine
1. 完善现有 `SemanticSearchEngine` 实现
2. 创建 `SemanticSearchAdapter<T>` 桥接泛型接口
### Phase 4: Settings.UI Migration
1. `SettingEntry` 实现 `ISearchable`
2. 创建 `SettingsSearchService`
3. 迁移 `SearchIndexService` 到新架构
4. 保持 API 兼容,逐步废弃旧方法
### Phase 5: CompositeSearchEngine (Optional)
1. 实现组合引擎
2. 支持 Fuzzy + Semantic 混合搜索
## Open Questions
1. **是否需要支持图片搜索?** 当前 SemanticSearchEngine 支持 `IndexImage`,但 `ISearchable` 只有文本。如果需要图片,可能需要 `IImageSearchable` 扩展。
2. **结果去重策略?** CompositeEngine 合并结果时,同一个 Item 可能被多个引擎匹配,如何去重和合并分数?
3. **异步 vs 同步?** FuzzSearch 完全可以同步执行,但接口统一用 `Task` 是否合适?考虑提供同步重载?
4. **索引更新策略?** 当 Settings 内容变化时(例如用户切换语言),如何高效更新索引?
---
*Document Version: 1.0*
*Last Updated: 2026-01-21*

View File

@@ -2,7 +2,7 @@
//
#include <windows.h>
#include "resource.h"
#include "../../src/common/version/version.h"
#include "../../src/common/utils/version.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////

View File

@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DOTNET_CORE_DOWNLOAD_FAILURE" xml:space="preserve">
<value>Couldn't download .NET Core Desktop Runtime 3.1, please install it manually.</value>
</data>
<data name="DOTNET_CORE_DOWNLOAD_FAILURE_TITLE" xml:space="preserve">
<value>PowerToys installation error</value>
</data>
<data name="GITHUB_NEW_VERSION_AVAILABLE_OFFER_VISIT" xml:space="preserve">
<value>An update to PowerToys is available. Visit our GitHub page to update.</value>
</data>
</root>

View File

@@ -1,36 +0,0 @@
#include <windows.h>
#include "resource.h"
#include "../common/version/version.h"
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@@ -1,109 +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.
#define WIN32_LEAN_AND_MEAN
#include "Generated Files/resource.h"
#include <Windows.h>
#include <shellapi.h>
#include <filesystem>
#include <string_view>
#include <common/utils/elevation.h>
#include <common/utils/process_path.h>
#include <common/utils/resources.h>
#include <common/utils/timeutil.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/logger/logger.h>
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.Storage.h>
#include "../runner/tray_icon.h"
#include "../runner/ActionRunnerUtils.h"
using namespace cmdArg;
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
int nArgs = 0;
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
if (!args || nArgs < 2)
{
return 1;
}
std::wstring_view action{ args[1] };
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
logFilePath.append(LogSettings::actionRunnerLogPath);
Logger::init(LogSettings::actionRunnerLoggerName, logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location());
if (action == RUN_NONELEVATED)
{
int nextArg = 2;
std::wstring_view target;
std::wstring_view pidFile;
std::wstring params;
while (nextArg < nArgs)
{
if (std::wstring_view(args[nextArg]) == L"-target" && nextArg + 1 < nArgs)
{
target = args[nextArg + 1];
nextArg += 2;
}
else if (std::wstring_view(args[nextArg]) == L"-pidFile" && nextArg + 1 < nArgs)
{
pidFile = args[nextArg + 1];
nextArg += 2;
}
else
{
params += args[nextArg];
params += L' ';
nextArg++;
}
}
HANDLE hMapFile = NULL;
PDWORD pidBuffer = NULL;
if (!pidFile.empty())
{
hMapFile = OpenFileMappingW(FILE_MAP_WRITE, FALSE, pidFile.data());
if (hMapFile)
{
pidBuffer = static_cast<PDWORD>(MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(DWORD)));
if (pidBuffer)
{
*pidBuffer = 0;
}
}
}
run_same_elevation(target.data(), params, pidBuffer);
if (!pidFile.empty())
{
if (pidBuffer)
{
FlushViewOfFile(pidBuffer, sizeof(DWORD));
UnmapViewOfFile(pidBuffer);
}
if (hMapFile)
{
FlushFileBuffers(hMapFile);
CloseHandle(hMapFile);
}
}
}
return 0;
}

View File

@@ -1,75 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h actionRunner.base.rc actionRunner.rc" />
</Target>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}</ProjectGuid>
<RootNamespace>actionRunner</RootNamespace>
<ProjectName>PowerToys.ActionRunner</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<Import Project="..\..\deps\expected.props" />
<PropertyGroup>
<ConfigurationType>Application</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup>
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>../;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>WindowsApp.lib;Msi.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="ActionRunner.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<None Include="ActionRunner.base.rc" />
<ResourceCompile Include="Generated Files\ActionRunner.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -1,11 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by PowerToys.ActionRunner.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys ActionRunner"
#define INTERNAL_NAME "PowerToys.ActionRunner"
#define ORIGINAL_FILENAME "PowerToys.ActionRunner.exe"

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace PowerToys.ActionRunner;
internal sealed partial class Program
{
private static void Main(string[] args)
{
if (args.Length < 1)
{
Environment.Exit(1);
return;
}
switch (args[0])
{
case "-run-non-elevated":
ExecuteRunNonElevated(args[1..]);
break;
default:
Environment.Exit(1);
break;
}
}
private static void ExecuteRunNonElevated(string[] args)
{
string? target = null;
string? pidFile = null;
string? arguments = null;
for (int i = 0; i < args.Length; i++)
{
string arg = args[i];
if (arg == "-target" && i + 1 < args.Length)
{
target = args[i + 1];
i++;
continue;
}
else if (arg == "-pidFile" && i + 1 < args.Length)
{
pidFile = args[i + 1];
i++;
continue;
}
arguments = args[i + 1] + " ";
i++;
if (target == null)
{
Environment.Exit(1);
return;
}
if (!string.IsNullOrEmpty(pidFile))
{
IntPtr pidBuffer = IntPtr.Zero;
IntPtr mapFile = OpenFileMapping(0x0002 /* FILE_MAP_WRITE */, false, pidFile);
if (mapFile != IntPtr.Zero)
{
pidBuffer = MapViewOfFile(mapFile, 0x001F /* FILE_MAP_ALL_ACCESS */, 0, 0, sizeof(uint));
if (pidBuffer != IntPtr.Zero)
{
Marshal.WriteInt32(pidBuffer, 0);
}
}
Process? p = Process.Start(new ProcessStartInfo
{
FileName = target,
Arguments = arguments.Trim(),
UseShellExecute = true,
});
if (pidBuffer != IntPtr.Zero)
{
Marshal.WriteInt32(pidBuffer, p?.Id ?? 0);
FlushViewOfFile(pidBuffer, sizeof(uint));
UnmapViewOfFile(pidBuffer);
}
if (mapFile != IntPtr.Zero)
{
FlushFileBuffers(mapFile);
CloseHandle(mapFile);
}
}
}
}
[LibraryImport("Kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
private static partial IntPtr OpenFileMapping(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, string lpName);
[LibraryImport("Kernel32.dll", SetLastError = true)]
private static partial IntPtr MapViewOfFile(IntPtr hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, UIntPtr dwNumberOfBytesToMap);
[LibraryImport("Kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool UnmapViewOfFile(IntPtr lpBaseAddress);
[LibraryImport("Kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool FlushViewOfFile(IntPtr lpBaseAddress, uint dwNumberOfBytesToFlush);
[LibraryImport("Kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool FlushFileBuffers(IntPtr hFile);
[LibraryImport("Kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(IntPtr hObject);
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Windows.ApplicationModel;
namespace RunnerV2.Extensions
{
internal static class PackageVersionExtensions
{
public static Version ToVersion(this PackageVersion packageVersion)
{
return new Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision);
}
}
}

View File

@@ -0,0 +1,57 @@
// 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 RunnerV2.Helpers
{
internal static class COMUtils
{
public static void InitializeCOMSecurity(string securityDescriptor)
{
if (!NativeMethods.ConvertStringSecurityDescriptorToSecurityDescriptorW(
securityDescriptor,
1,
out IntPtr pSD,
out _))
{
return;
}
uint absoluteSDSize = 0;
uint daclSize = 0;
uint groupSize = 0;
uint ownerSize = 0;
uint saclSize = 0;
if (!NativeMethods.MakeAbsoluteSD(pSD, IntPtr.Zero, ref absoluteSDSize, IntPtr.Zero, ref daclSize, IntPtr.Zero, ref saclSize, IntPtr.Zero, ref ownerSize, IntPtr.Zero, ref groupSize))
{
return;
}
IntPtr absoluteSD = Marshal.AllocHGlobal((int)absoluteSDSize);
IntPtr dacl = Marshal.AllocHGlobal((int)daclSize);
IntPtr sacl = Marshal.AllocHGlobal((int)saclSize);
IntPtr owner = Marshal.AllocHGlobal((int)ownerSize);
IntPtr group = Marshal.AllocHGlobal((int)groupSize);
if (!NativeMethods.MakeAbsoluteSD(pSD, absoluteSD, ref absoluteSDSize, dacl, ref daclSize, sacl, ref saclSize, owner, ref ownerSize, group, ref groupSize))
{
return;
}
_ = NativeMethods.CoInitializeSecurity(
absoluteSD,
-1,
IntPtr.Zero,
IntPtr.Zero,
6, // RPC_C_AUTHN_LEVEL_PKT_PRIVACY
2, // RPC_C_IMP_LEVEL_IDENTIFY
IntPtr.Zero,
64 | 4096, // EOAC_DYNAMIC_CLOAKING | EOAC_DISABLE_AAA
IntPtr.Zero);
}
}
}

View File

@@ -0,0 +1,218 @@
// 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 ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Windows.System;
namespace RunnerV2.Helpers
{
internal static class CentralizedKeyboardHookManager
{
private static readonly UIntPtr _ignoreKeyEventFlag = 0x5555;
private static readonly Dictionary<string, List<(HotkeySettings HotkeySettings, Action Action)>> _keyboardHooks = [];
private static HotkeySettingsControlHook _hotkeySettingsControlHook = new(OnKeyDown, OnKeyUp, IsActive, (_, specialFlags) => specialFlags != _ignoreKeyEventFlag);
private static void OnKeyDown(int key)
{
if ((VirtualKey)key == VirtualKey.RightMenu && _ctrlState)
{
_ctrlAltState = true;
}
switch ((VirtualKey)key)
{
case VirtualKey.Control:
case VirtualKey.LeftControl:
case VirtualKey.RightControl:
_ctrlState = true;
break;
case VirtualKey.Menu:
case VirtualKey.LeftMenu:
case VirtualKey.RightMenu:
_altState = true;
break;
case VirtualKey.Shift:
case VirtualKey.LeftShift:
case VirtualKey.RightShift:
_shiftState = true;
break;
case VirtualKey.LeftWindows:
case VirtualKey.RightWindows:
_winState = true;
break;
default:
if (OnKeyboardEvent(new HotkeySettings
{
Code = key,
Ctrl = _ctrlState,
Alt = _altState,
Shift = _shiftState,
Win = _winState,
}))
{
return;
}
break;
}
SendSingleKeyboardInput((short)key, (uint)NativeKeyboardHelper.KeyEventF.KeyDown);
}
private static void OnKeyUp(int key)
{
switch ((VirtualKey)key)
{
case VirtualKey.Control:
case VirtualKey.LeftControl:
case VirtualKey.RightControl:
_ctrlState = false;
break;
case VirtualKey.Menu:
case VirtualKey.LeftMenu:
case VirtualKey.RightMenu:
_altState = false;
break;
case VirtualKey.Shift:
case VirtualKey.LeftShift:
case VirtualKey.RightShift:
_shiftState = false;
break;
case VirtualKey.LeftWindows:
case VirtualKey.RightWindows:
_winState = false;
break;
}
// Correctly release Ctrl key if Ctrl+Alt (AltGr) was used.
if (_ctrlAltState && (VirtualKey)key == VirtualKey.RightMenu)
{
_ctrlAltState = false;
_ctrlState = false;
SendSingleKeyboardInput((short)VirtualKey.LeftControl, (uint)NativeKeyboardHelper.KeyEventF.KeyUp);
}
SendSingleKeyboardInput((short)key, (uint)NativeKeyboardHelper.KeyEventF.KeyUp);
}
private static bool _ctrlState;
private static bool _altState;
private static bool _shiftState;
private static bool _winState;
private static bool _ctrlAltState;
private static bool _isActive;
private static bool IsActive()
{
return _isActive;
}
public static void AddKeyboardHook(string moduleName, HotkeySettings hotkeySettings, Action action)
{
#pragma warning disable CA1854 // Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method
if (!_keyboardHooks.ContainsKey(moduleName))
{
_keyboardHooks[moduleName] = [];
}
#pragma warning restore CA1854 // Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method
_keyboardHooks[moduleName].Add((hotkeySettings, action));
}
public static void RemoveAllHooksFromModule(string moduleName)
{
_keyboardHooks.Remove(moduleName);
}
private static bool OnKeyboardEvent(HotkeySettings pressedHotkey)
{
bool shortcutHandled = false;
foreach (var moduleHooks in _keyboardHooks.Values)
{
foreach (var (hotkeySettings, action) in moduleHooks)
{
if (hotkeySettings == pressedHotkey)
{
action();
shortcutHandled = true;
}
}
}
return shortcutHandled;
}
public static void Start()
{
if (_hotkeySettingsControlHook.GetDisposedState())
{
_hotkeySettingsControlHook = new(OnKeyDown, OnKeyUp, IsActive, (_, specialFlags) => specialFlags != _ignoreKeyEventFlag);
}
_isActive = true;
}
public static void Stop()
{
_isActive = false;
_hotkeySettingsControlHook.Dispose();
}
// Function to send a single key event to the system which would be ignored by the hotkey control.
private static void SendSingleKeyboardInput(short keyCode, uint keyStatus)
{
if (IsExtendedVirtualKey(keyCode))
{
keyStatus |= (uint)NativeKeyboardHelper.KeyEventF.ExtendedKey;
}
NativeKeyboardHelper.INPUT input = new()
{
type = NativeKeyboardHelper.INPUTTYPE.INPUT_KEYBOARD,
data = new NativeKeyboardHelper.InputUnion
{
ki = new NativeKeyboardHelper.KEYBDINPUT
{
wVk = keyCode,
dwFlags = keyStatus,
dwExtraInfo = _ignoreKeyEventFlag,
},
},
};
NativeKeyboardHelper.INPUT[] inputs = [input];
_ = NativeMethods.SendInput(1, inputs, NativeKeyboardHelper.INPUT.Size);
}
private static bool IsExtendedVirtualKey(short vk)
{
return vk switch
{
0xA5 => true, // VK_RMENU (Right Alt - AltGr)
0xA3 => true, // VK_RCONTROL
0x2D => true, // VK_INSERT
0x2E => true, // VK_DELETE
0x23 => true, // VK_END
0x24 => true, // VK_HOME
0x21 => true, // VK_PRIOR (Page Up)
0x22 => true, // VK_NEXT (Page Down)
0x25 => true, // VK_LEFT
0x26 => true, // VK_UP
0x27 => true, // VK_RIGHT
0x28 => true, // VK_DOWN
0x90 => true, // VK_NUMLOCK
_ => false,
};
}
}
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using static RunnerV2.NativeMethods;
namespace RunnerV2.Helpers
{
internal static partial class ElevationHelper
{
internal static RestartScheduledMode RestartScheduled { get; set; } = RestartScheduledMode.None;
internal enum RestartScheduledMode
{
None,
RestartElevated,
RestartElevatedWithOpenSettings,
RestartNonElevated,
}
private static bool? _cachedValue;
internal static void RestartIfScheudled()
{
switch (RestartScheduled)
{
case RestartScheduledMode.None:
return;
case RestartScheduledMode.RestartElevated:
RestartAsAdministrator("--restartedElevated");
break;
case RestartScheduledMode.RestartElevatedWithOpenSettings:
RestartAsAdministrator("--restartedElevated --open-settings");
break;
case RestartScheduledMode.RestartNonElevated:
// Todo: restart unelevated
break;
}
}
private static void RestartAsAdministrator(string arguments)
{
ProcessStartInfo processStartInfo = new()
{
Arguments = arguments,
Verb = "runas",
UseShellExecute = true,
FileName = Environment.ProcessPath,
};
try
{
Process.Start(processStartInfo);
}
catch (Exception ex)
{
Console.WriteLine("Failed to restart as administrator: " + ex);
}
}
internal static bool IsProcessElevated(bool useCachedValue = true)
{
if (_cachedValue is not null && useCachedValue)
{
return _cachedValue.Value;
}
bool elevated = false;
if (OpenProcessToken(Process.GetCurrentProcess().Handle, TOKENQUERY, out nint token))
{
TokenElevation elevation = default;
if (GetTokenInformation(token, TOKEN_INFORMATION_CLASS.TOKEN_ELEVATION, ref elevation, (uint)Marshal.SizeOf(elevation), out uint _))
{
elevated = elevation.TokenIsElevated != 0;
}
if (token != IntPtr.Zero)
{
CloseHandle(token);
}
}
_cachedValue = elevated;
return elevated;
}
}
}

View File

@@ -0,0 +1,52 @@
// 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.Linq;
using System.Text;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.Win32;
namespace RunnerV2.Helpers
{
internal static class NotificationHelper
{
public enum ToastType
{
ElevatedDontShowAgain,
CouldntToggleFileExplorerModules,
}
public static string GetToastKey(ToastType key) => key switch
{
ToastType.ElevatedDontShowAgain => @"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\DontShowMeThisDialogAgain\{e16ea82f-6d94-4f30-bb02-d6d911588afd}",
ToastType.CouldntToggleFileExplorerModules => @"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\DontShowMeThisDialogAgain\{7e29e2b2-b31c-4dcd-b7b0-79c078b02430})",
_ => throw new ArgumentOutOfRangeException(nameof(key), key, null),
};
public static bool DisableToast(ToastType type)
{
try
{
RegistryKey? key = Registry.CurrentUser.CreateSubKey(GetToastKey(type));
if (key != null)
{
key.SetValue(null, BitConverter.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeSeconds()), RegistryValueKind.QWord);
key.Close();
return true;
}
}
catch (Exception e)
{
Logger.LogError("Could not disable toast notification.", e);
}
return false;
}
}
}

View File

@@ -0,0 +1,179 @@
// 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.Reflection;
using System.Text.RegularExpressions;
using ManagedCommon;
using RunnerV2.Extensions;
using Windows.ApplicationModel;
using Windows.Foundation;
using Windows.Management.Deployment;
namespace RunnerV2.Helpers
{
/// <summary>
/// Provides helper methods for working with UWP packages.
/// </summary>
internal static partial class PackageHelper
{
/// <summary>
/// Gets the registered UWP package based on the display name and version check.
/// </summary>
/// <param name="packageDisplayName">The display name of the package.</param>
/// <param name="checkVersion">If true, the package version will be checked against the executing assembly version.</param>
/// <returns>If a package is found the corresponding <see cref="Package"/> object. If none is found <c>null</c>.</returns>
internal static Package? GetRegisteredPackage(string packageDisplayName, bool checkVersion)
{
PackageManager packageManager = new();
foreach (var package in packageManager.FindPackagesForUser(null))
{
if (package.Id.FullName.Contains(packageDisplayName) && (!checkVersion || package.Id.Version.ToVersion() == Assembly.GetExecutingAssembly().GetName().Version))
{
return package;
}
}
return null;
}
internal static string[] FindMsixFiles(string directoryPath, bool recursive)
{
if (!Directory.Exists(directoryPath))
{
Logger.LogError("Tried to search msix files in " + directoryPath + ", but it does not exist.");
return [];
}
List<string> matchedFiles = [];
try
{
foreach (string file in Directory.GetFiles(directoryPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
{
if (File.Exists(file) && msixPackagePattern().IsMatch(Path.GetFileName(file)))
{
matchedFiles.Add(file);
}
}
}
catch (Exception e)
{
Logger.LogError("An error occured while searching for MSIX files.", e);
}
return [.. matchedFiles];
}
/// <summary>
/// Installs the specified appx package along with its dependencies.
/// </summary>
/// <param name="packagePath">Path to the package</param>
/// <param name="dependencies">Array of dependency package paths</param>
/// <returns>True if the installation was successful, false otherwise</returns>
internal static bool InstallPackage(string packagePath, string[] dependencies)
{
Logger.LogInfo("Starting package install of package \"" + packagePath + "\"");
PackageManager packageManager = new();
List<Uri> uris = [];
foreach (string dependency in dependencies)
{
try
{
if (IsPackageSatisfied(dependency))
{
Logger.LogInfo("Dependency \"" + dependency + "\" is already satisfied.");
continue;
}
else
{
uris.Add(new Uri(packagePath));
}
}
catch (Exception ex)
{
Logger.LogError("Could not process dependency package at path \"" + dependency + "\"", ex);
}
}
try
{
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageAsync(new Uri(packagePath), uris, DeploymentOptions.ForceApplicationShutdown);
deploymentOperation.Get();
switch (deploymentOperation.Status)
{
case AsyncStatus.Error:
Logger.LogError($"Registering {packagePath} failed. ErrorCode: {deploymentOperation.ErrorCode}, ErrorText: {deploymentOperation.GetResults().ErrorText}");
break;
case AsyncStatus.Canceled:
Logger.LogError($"Registering {packagePath} was canceled.");
break;
case AsyncStatus.Completed:
Logger.LogInfo($"Registering {packagePath} succeded.");
break;
default:
Logger.LogDebug($"Registering {packagePath} package started.");
break;
}
return true;
}
catch (Exception e)
{
Logger.LogError($"Exception thrown while trying to register package: {packagePath}", e);
}
return false;
}
/// <summary>
/// Checks if the package specified by the given path is already installed and satisfies the required version.
/// </summary>
/// <param name="packagePath">Path to the package.</param>
/// <returns>True if the package is already installed and satisfies the required version, false otherwise.</returns>
private static bool IsPackageSatisfied(string packagePath)
{
if (!GetPackageNameAndVersionFromAppx(packagePath, out string name, out PackageVersion version))
{
Logger.LogError("Could not get package name and version from dependency package at path \"" + packagePath + "\"");
return false;
}
PackageManager packageManager = new();
foreach (var package in packageManager.FindPackagesForUser(null))
{
if (package.Id.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
package.Id.Version.ToVersion() > version.ToVersion())
{
Logger.LogInfo($@"Package ""{name}"" is already statisfied with version: {package.Id.Version}. Target version: {version}. PackagePath: {packagePath}");
return true;
}
}
Logger.LogInfo($@"Package ""{name}"" with version {version} is not satisfied. PackagePath: {packagePath}");
return false;
}
/// <summary>
/// Gets the package name and version from the specified appx package file.
/// </summary>
/// <param name="packagePath">Path to the package file.</param>
/// <param name="name">Output parameter for the package name.</param>
/// <param name="packageVersion">Output parameter for the package version.</param>
/// <returns>True if the package name and version were successfully retrieved, false otherwise.</returns>
private static bool GetPackageNameAndVersionFromAppx(string packagePath, out string name, out PackageVersion packageVersion)
{
// Todo: Implement this without interop if possible
return NativeMethods.GetPackageNameAndVersionFromAppx(packagePath, out name, out packageVersion);
}
[GeneratedRegex("(^.+\\.(appx|msix|msixbundle)$)", RegexOptions.IgnoreCase)]
private static partial Regex msixPackagePattern();
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace RunnerV2.Helpers
{
internal static class ProcessHelper
{
internal static void ScheudleProcessKill(string processName, int msDelay = 500)
{
new Thread(async () =>
{
Process[] processes = Process.GetProcessesByName(processName);
await Task.Delay(msDelay);
foreach (var process in processes)
{
if (!process.HasExited)
{
process.Kill();
}
}
}).Start();
}
}
}

View File

@@ -0,0 +1,228 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Text.Json;
using System.Threading;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.Interop;
using RunnerV2.Models;
using Update;
namespace RunnerV2.Helpers
{
/// <summary>
/// This class provides helper methods to interact with the PowerToys Settings window.
/// </summary>
internal static class SettingsHelper
{
private static readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
private static Process? _process;
private static TwoWayPipeMessageIPCManaged? _ipc;
public static void OpenSettingsWindow(bool showOobeWindow = false, bool showScoobeWindow = false, bool showFlyout = false, Point? flyoutPosition = null, string? additionalArguments = null)
{
if (_process is not null && _ipc is not null && !_process.HasExited)
{
if (showFlyout)
{
_ipc.Send(@"{""ShowYourself"": ""flyout""}");
}
else
{
_ipc.Send($@"{{""ShowYourself"": ""{additionalArguments ?? "Dashboard"}""}}");
}
return;
}
_ipc?.End();
_ipc = null;
// Arg 1: Executable path
string executablePath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException("No executable path found"), "WinUI3Apps", "PowerToys.Settings.exe");
// Arg 2,3: Pipe names
Pipe settingsPipe = new();
Pipe powertoysPipe = new();
string powerToysPipeName = @"\\.\pipe\powertoys_runner_" + Guid.NewGuid();
string settingsPipeName = @"\\.\pipe\powertoys_settings_" + Guid.NewGuid();
// Arg 4: Process pid
string currentProcessId = Environment.ProcessId.ToString(CultureInfo.InvariantCulture);
// Arg 5: Settings theme
string theme = Program.GeneralSettings.Theme switch
{
"light" => "light",
"dark" => "dark",
"system" when ThemeHelpers.GetAppTheme() == AppTheme.Light => "light",
"system" when ThemeHelpers.GetAppTheme() == AppTheme.Dark => "dark",
_ => throw new NotImplementedException(),
};
// Arg 6: Elevated status
string isElevated = Program.GeneralSettings.IsElevated ? "true" : "false";
// Arg 7: Is user an administrator
string isAdmin = Program.GeneralSettings.IsAdmin ? "true" : "false";
// Arg 8: Show OOBE window
string showOobeArg = showOobeWindow ? "true" : "false";
// Arg 9: Show SCOOBE window
string showScoobeArg = showScoobeWindow ? "true" : "false";
// Arg 10: Show flyout
string showFlyoutArg = showFlyout ? "true" : "false";
// Arg 11: Are there additional settings window arguments
string areThereadditionalArgs = string.IsNullOrEmpty(additionalArguments) ? "false" : "true";
// Arg 12: Are there flyout position arguments
string areThereFlyoutPositionArgs = flyoutPosition.HasValue ? "true" : "false";
string executableArgs = $"{powerToysPipeName} {settingsPipeName} {currentProcessId} {theme} {isElevated} {isAdmin} {showOobeArg} {showScoobeArg} {showFlyoutArg} {areThereadditionalArgs} {areThereFlyoutPositionArgs}";
if (!string.IsNullOrEmpty(additionalArguments))
{
executableArgs += $" {additionalArguments}";
}
if (flyoutPosition is not null)
{
executableArgs += $" {flyoutPosition.Value.X} {flyoutPosition.Value.Y}";
}
_process = Process.Start(executablePath, executableArgs);
// Initialize listening to pipes
_ipc = new TwoWayPipeMessageIPCManaged(powerToysPipeName, settingsPipeName, OnSettingsMessageReceived);
_ipc.Start();
}
private static void OnSettingsMessageReceived(string message)
{
JsonDocument messageDocument = JsonDocument.Parse(message);
foreach (var property in messageDocument.RootElement.EnumerateObject())
{
switch (property.Name)
{
case "action":
foreach (var moduleName in property.Value.EnumerateObject())
{
_settingsUtils.SaveSettings(moduleName.Value.ToString(), moduleName.Name);
if (moduleName.Name == "general")
{
switch (moduleName.Value.GetProperty("action_name").GetString())
{
case "restart_elevation":
ElevationHelper.RestartScheduled = ElevationHelper.RestartScheduledMode.RestartElevatedWithOpenSettings;
Runner.Close();
break;
case "restart_mentain_elevation":
// Todo:
break;
case "check_for_updates":
UpdateSettingsHelper.TriggerUpdateCheck();
break;
case "request_update_state_date":
// Todo:
break;
}
break;
}
foreach (IPowerToysModule ptModule in Runner.LoadedModules)
{
if (ptModule is IPowerToysModuleCustomActionsProvider customActionsProvider && customActionsProvider.CustomActions.TryGetValue(moduleName.Value.GetProperty("action_name").GetString() ?? string.Empty, out Action? action))
{
action();
}
}
}
break;
case "get_all_hotkey_conflicts":
// Todo: Handle hotkey conflict
break;
case "bugreport":
TrayIconManager.ProcessTrayMenuCommand((nuint)TrayIconManager.TrayButton.ReportBug);
break;
case "bug_report_status":
_ipc?.Send($@"{{""bug_report_running:"" {(TrayIconManager.IsBugReportToolRunning ? "true" : "false")}");
break;
case "killrunner":
Runner.Close();
break;
case "general":
try
{
_settingsUtils.SaveSettings(property.Value.ToString(), string.Empty);
}
catch (Exception)
{
// TODO: Log error
}
NativeMethods.PostMessageW(Runner.RunnerHwnd, (uint)NativeMethods.WindowMessages.REFRESH_SETTINGS, 0, 0);
foreach (IPowerToysModule module in Runner.ModulesToLoad)
{
if (module is IPowerToysModuleSettingsChangedSubscriber settingsChangedSubscriber)
{
settingsChangedSubscriber.OnSettingsChanged();
}
}
break;
case "powertoys":
foreach (var powertoysSettingsPart in property.Value.EnumerateObject())
{
_settingsUtils.SaveSettings(powertoysSettingsPart.Value.ToString(), powertoysSettingsPart.Name);
if (Runner.LoadedModules.Find(m => m.Name == powertoysSettingsPart.Name) is IPowerToysModuleSettingsChangedSubscriber module)
{
module.OnSettingsChanged();
}
else
{
// If no specific module was found, notify all enabled modules
foreach (IPowerToysModule module2 in Runner.LoadedModules.Where(m => m.Enabled))
{
if (module2 is IPowerToysModuleSettingsChangedSubscriber settingsChangedSubscriber)
{
settingsChangedSubscriber.OnSettingsChanged();
}
}
}
NativeMethods.PostMessageW(Runner.RunnerHwnd, (uint)NativeMethods.WindowMessages.REFRESH_SETTINGS, 0, 0);
}
break;
default:
Console.WriteLine($"Unknown message received from Settings: {property.Name}");
break;
}
}
}
public static void CloseSettingsWindow()
{
using var closeEventWrapper = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.PowerToysRunnerTerminateSettingsEvent());
closeEventWrapper.Set();
}
}
}

View File

@@ -0,0 +1,150 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using static RunnerV2.NativeMethods;
namespace RunnerV2.Helpers
{
internal static partial class TrayIconManager
{
internal static void StartTrayIcon()
{
NOTIFYICONDATA notifyicondata = new()
{
CbSize = (uint)Marshal.SizeOf<NOTIFYICONDATA>(),
HWnd = Runner.RunnerHwnd,
UId = 1,
HIcon = Icon.ExtractAssociatedIcon(Environment.ProcessPath!)!.Handle,
UFlags = 0x0000001 | 0x00000002 | 0x4,
UCallbackMessage = (uint)WindowMessages.ICON_NOTIFY,
SzTip = "PowerToys Runner",
};
ChangeWindowMessageFilterEx(Runner.RunnerHwnd, 0x0111, 0x0001, IntPtr.Zero);
Shell_NotifyIcon(NIMADD, ref notifyicondata);
}
internal static void StopTrayIcon()
{
NOTIFYICONDATA notifyicondata = new()
{
CbSize = (uint)Marshal.SizeOf<NOTIFYICONDATA>(),
HWnd = Runner.RunnerHwnd,
UId = 1,
};
Shell_NotifyIcon(NIMDELETE, ref notifyicondata);
}
internal enum TrayButton : uint
{
Settings = 1,
Documentation,
ReportBug,
Close,
}
private static bool _doubleClickTimerRunning;
private static bool _doubleClickDetected;
private static IntPtr _trayIconMenu;
static TrayIconManager()
{
_trayIconMenu = CreatePopupMenu();
AppendMenuW(_trayIconMenu, 0u, new UIntPtr((uint)TrayButton.Settings), "Settings\tDouble-click");
AppendMenuW(_trayIconMenu, 0x00000800u, UIntPtr.Zero, string.Empty); // separator
AppendMenuW(_trayIconMenu, 0u, new UIntPtr((uint)TrayButton.Documentation), "Documentation");
AppendMenuW(_trayIconMenu, 0u, new UIntPtr((uint)TrayButton.ReportBug), "Report a Bug");
AppendMenuW(_trayIconMenu, 0x00000800u, UIntPtr.Zero, string.Empty); // separator
AppendMenuW(_trayIconMenu, 0u, new UIntPtr((uint)TrayButton.Close), "Close");
}
internal static void ProcessTrayIconMessage(long lParam)
{
switch (lParam)
{
case 0x0205: // WM_RBUTTONDBLCLK
case 0x007B: // WM_CONTEXTMENU
SetForegroundWindow(Runner.RunnerHwnd);
TrackPopupMenu(_trayIconMenu, 0x0004 | 0x0020, Cursor.Position.X, Cursor.Position.Y, 0, Runner.RunnerHwnd, IntPtr.Zero);
break;
case 0x0202: // WM_LBUTTONUP
if (_doubleClickTimerRunning)
{
break;
}
_doubleClickTimerRunning = true;
Task.Delay(SystemInformation.DoubleClickTime).ContinueWith(_ =>
{
if (!_doubleClickDetected)
{
SettingsHelper.OpenSettingsWindow(showFlyout: true, flyoutPosition: Cursor.Position);
}
_doubleClickDetected = false;
_doubleClickTimerRunning = false;
});
break;
case 0x0203: // WM_LBUTTONDBLCLK
_doubleClickDetected = true;
SettingsHelper.OpenSettingsWindow();
break;
}
}
internal static bool IsBugReportToolRunning { get; set; }
internal static void ProcessTrayMenuCommand(nuint commandId)
{
switch ((TrayButton)commandId)
{
case TrayButton.Settings:
SettingsHelper.OpenSettingsWindow();
break;
case TrayButton.Documentation:
Process.Start(new ProcessStartInfo
{
FileName = "https://aka.ms/PowerToysOverview",
UseShellExecute = true,
});
break;
case TrayButton.ReportBug:
Process bugReportProcess = new();
bugReportProcess.StartInfo = new ProcessStartInfo
{
FileName = "Tools\\PowerToys.BugReportTool.exe",
CreateNoWindow = true,
};
bugReportProcess.EnableRaisingEvents = true;
EnableMenuItem(_trayIconMenu, (uint)TrayButton.ReportBug, 0x000000 | 0x00001);
bugReportProcess.Exited += (sender, e) =>
{
bugReportProcess.Dispose();
EnableMenuItem(_trayIconMenu, (uint)TrayButton.ReportBug, 0x00000000);
IsBugReportToolRunning = false;
};
bugReportProcess.Start();
IsBugReportToolRunning = true;
break;
case TrayButton.Close:
Runner.Close();
break;
}
}
}
}

View File

@@ -0,0 +1,47 @@
// 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.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
namespace RunnerV2.Models
{
public interface IPowerToysModule
{
/// <summary>
/// Gets the short name of the module. The same used as the name of the folder containing its settings.
/// </summary>
public string Name { get; }
/// <summary>
/// This function is called when the module is enabled.
/// </summary>
public void Enable();
/// <summary>
/// This function is called when the module is disabled.
/// </summary>
public void Disable();
/// <summary>
/// Gets a value indicating whether the module is enabled.
/// </summary>
/// <remarks>
/// This value shall be read from the settings of the module in the module interface implementation.
/// </remarks>
public bool Enabled { get; }
/// <summary>
/// Gets the GPO rule configured state for the module.
/// </summary>
/// <remarks>
/// This value shall be read from the GPO settings with the <see cref="GPOWrapper"/> class.
/// </remarks>
public GpoRuleConfigured GpoRuleConfigured { get; }
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RunnerV2.Models
{
internal interface IPowerToysModuleCustomActionsProvider
{
public Dictionary<string, Action> CustomActions { get; }
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RunnerV2.Models
{
internal interface IPowerToysModuleSettingsChangedSubscriber
{
public void OnSettingsChanged();
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using Microsoft.PowerToys.Settings.UI.Library;
namespace RunnerV2.Models
{
internal interface IPowerToysModuleShortcutsProvider
{
/// <summary>
/// Gets a list of shortcuts, that shall be registered in the keyboard hook, and their associated actions.
/// </summary>
/// <remarks>
/// If this property is not overridden, the module is considered to not have shortcuts.
/// </remarks>
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; }
}
}

View File

@@ -0,0 +1,168 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Globalization;
using RunnerV2.Helpers;
namespace RunnerV2.Models
{
/// <summary>
/// Base abstract class for modules that launch and manage external processes.
/// </summary>
internal abstract class ProcessModuleAbstractClass
{
/// <summary>
/// Options for launching a process.
/// </summary>
[Flags]
public enum ProcessLaunchOptions
{
/// <summary>
/// Only a single instance of the process should be running.
/// </summary>
SingletonProcess = 1,
/// <summary>
/// Elevate the process if the current process is elevated.
/// </summary>
ElevateIfApplicable = 2,
/// <summary>
/// Provide the runner process ID as the first argument to the launched process.
/// </summary>
RunnerProcessIdAsFirstArgument = 4,
/// <summary>
/// Indicates that the application should not launch automatically when the specified module is enabled.
/// </summary>
SupressLaunchOnModuleEnabled = 8,
/// <summary>
/// Specifies that the process should be started using the operating system shell.
/// </summary>
UseShellExecute = 16,
/// <summary>
/// Indicates that the process should never exit automatically.
/// </summary>
/// <remarks>Use this value when using a helper process to launch an application like explorer.exe.</remarks>
NeverExit = 32,
/// <summary>
/// Suppresses UI when process is launched.
/// </summary>
HideUI = 64,
/// <summary>
/// Sets the launched process to realtime priority.
/// </summary>
RealtimePriority = 128,
}
/// <summary>
/// Gets the relative or absolute path to the process executable.
/// </summary>
public abstract string ProcessPath { get; }
/// <summary>
/// Gets the name of the process without the .exe extension.
/// </summary>
/// <remarks>
/// Has no effect if the <see cref="LaunchOptions"/> has the <see cref="ProcessLaunchOptions.UseShellExecute"/> flag set.
/// </remarks>
public abstract string ProcessName { get; }
/// <summary>
/// Gets the arguments to pass to the process on launch.
/// </summary>
/// <remarks>
/// If not overridden, no arguments are passed.
/// If the <see cref="LaunchOptions"/> has the <see cref="ProcessLaunchOptions.RunnerProcessIdAsFirstArgument"/> flag is set, the runner process ID is prepended to these arguments.
/// </remarks>
public virtual string ProcessArguments { get; } = string.Empty;
/// <summary>
/// Gets the options used to configure how the process is launched.
/// </summary>
public abstract ProcessLaunchOptions LaunchOptions { get; }
/// <summary>
/// Ensures that atleast one process is launched. If the process is already running, does nothing.
/// </summary>
public void EnsureLaunched()
{
Process[] processes = Process.GetProcessesByName(ProcessName);
if (processes.Length > 0)
{
return;
}
LaunchProcess();
}
/// <summary>
/// Launches the process with the specified options.
/// </summary>
/// <param name="isModuleEnableProcess">Specifies if the <see cref="Runner"/> class is currently calling this function as part of a module startup</param>
public void LaunchProcess(bool isModuleEnableProcess = false)
{
if (isModuleEnableProcess && LaunchOptions.HasFlag(ProcessLaunchOptions.SupressLaunchOnModuleEnabled))
{
return;
}
if (LaunchOptions.HasFlag(ProcessLaunchOptions.SingletonProcess))
{
Process[] processes = Process.GetProcessesByName(ProcessName);
if (processes.Length > 0)
{
return;
}
}
string arguments = (LaunchOptions.HasFlag(ProcessLaunchOptions.RunnerProcessIdAsFirstArgument) ? Environment.ProcessId.ToString(CultureInfo.InvariantCulture) + (string.IsNullOrEmpty(ProcessArguments) ? string.Empty : " ") : string.Empty) + ProcessArguments;
Process? p = Process.Start(new ProcessStartInfo()
{
UseShellExecute = LaunchOptions.HasFlag(ProcessLaunchOptions.UseShellExecute),
FileName = ProcessPath,
Arguments = arguments,
Verb = LaunchOptions.HasFlag(ProcessLaunchOptions.ElevateIfApplicable) && ElevationHelper.IsProcessElevated() ? "runas" : "open",
});
if (LaunchOptions.HasFlag(ProcessLaunchOptions.RealtimePriority))
{
try
{
if (p != null && !p.HasExited)
{
p.PriorityClass = ProcessPriorityClass.RealTime;
}
}
catch (Exception ex)
{
Debug.WriteLine($"[ProcessModuleAbstractClass] Failed to set realtime priority for process {ProcessName}", ex);
}
}
}
/// <summary>
/// Schedules all processes with the specified <see cref="ProcessName"/>.
/// </summary>
/// <remarks>
/// If the <see cref="LaunchOptions"/> has the <see cref="ProcessLaunchOptions.NeverExit"/> flag set, this function does nothing.
/// </remarks>
public void ProcessExit()
{
if (LaunchOptions.HasFlag(ProcessLaunchOptions.NeverExit))
{
return;
}
ProcessHelper.ScheudleProcessKill(ProcessName);
}
}
}

View File

@@ -0,0 +1,45 @@
// 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.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RunnerV2.Models
{
internal readonly struct RegistryChangeSet
{
public RegistryValueChange[] Changes { get; init; }
public readonly bool IsApplied => Changes.All(c => c.IsApplied);
public readonly bool Apply()
{
bool allApplied = true;
foreach (var change in Changes)
{
allApplied = (change.Apply() || !change.Required) && allApplied;
}
return allApplied;
}
public readonly bool ApplyIfNotApplied() => IsApplied || Apply();
public readonly bool UnApplyIfApplied() => !IsApplied || UnApply();
public readonly bool UnApply()
{
bool allUnapplied = true;
foreach (var change in Changes)
{
allUnapplied = (change.UnApply() || !change.Required) && allUnapplied;
}
return allUnapplied;
}
}
}

View File

@@ -0,0 +1,124 @@
// 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.Linq;
using System.Text;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.Win32;
namespace RunnerV2.Models
{
internal readonly struct RegistryValueChange
{
public RegistryValueChange()
{
}
public required string KeyPath { get; init; }
public required string? KeyName { get; init; }
public bool Required { get; init; } = true;
public required object Value { get; init; }
public RegistryHive Scope { get; init; } = RegistryHive.CurrentUser;
private static RegistryValueKind ValueTypeToRegistryValueKind(object value)
{
return value switch
{
int => RegistryValueKind.DWord,
long => RegistryValueKind.QWord,
string => RegistryValueKind.String,
string[] => RegistryValueKind.MultiString,
byte[] => RegistryValueKind.Binary,
_ => throw new ArgumentException("Unsupported value type"),
};
}
public readonly bool IsApplied
{
get
{
try
{
using RegistryKey? key = RegistryKey.OpenBaseKey(Scope, RegistryView.Default).OpenSubKey(KeyPath, false);
return key != null && ValueTypeToRegistryValueKind(Value) == key.GetValueKind(KeyName) && Value.Equals(key.GetValue(KeyName));
}
catch (Exception e)
{
Logger.LogError($"Testing if registry change \"{this}\" is applied failed.", e);
return false;
}
}
}
public readonly bool RequiresElevation
{
get => Scope == RegistryHive.LocalMachine;
}
public readonly bool Apply()
{
try
{
using RegistryKey? key = RegistryKey.OpenBaseKey(Scope, RegistryView.Default).CreateSubKey(KeyPath, true);
if (key == null)
{
Logger.LogError($"Applying registry change \"{this}\" failed because the registry key could not be created.");
return false;
}
key.SetValue(KeyName, Value, ValueTypeToRegistryValueKind(Value));
return true;
}
catch (Exception e)
{
Logger.LogError($"Applying registry change \"{this}\" failed.", e);
return false;
}
}
public readonly bool UnApply()
{
try
{
using RegistryKey? key = RegistryKey.OpenBaseKey(Scope, RegistryView.Default).OpenSubKey(KeyPath, true);
if (key == null)
{
Logger.LogError($"Unapplying registry change \"{this}\" failed because the registry key could not be opened.");
return false;
}
if (KeyName is not null)
{
key.DeleteValue(KeyName, false);
}
else
{
key.SetValue(null, string.Empty); // Delete the default value
}
// Check if the path doesn't contain anything and delete it if so
if (key.GetValueNames().Length == 0 && key.GetSubKeyNames().Length == 0)
{
RegistryKey.OpenBaseKey(Scope, RegistryView.Default).DeleteSubKey(KeyPath, false);
}
return true;
}
catch (Exception e)
{
Logger.LogError($"Unapplying registry change \"{this}\" failed.", e);
return false;
}
}
public override readonly string ToString() => $"{RegistryKey.OpenBaseKey(Scope, RegistryView.Default).Name}\\{KeyPath}\\{KeyName}:{Value}";
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace RunnerV2.Models
{
internal enum SpecialMode
{
None,
Win32ToastNotificationCOMServer,
DisableCantDragElevatedNotification,
UpdateNow,
ReportSuccessfulUpdate,
CouldntToggleFileExplorerModulesNotification,
}
}

View File

@@ -0,0 +1,107 @@
// 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.Linq;
using System.Text.Json;
using System.Windows.Forms;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class AdvancedPasteModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IDisposable, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "AdvancedPaste";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.AdvancedPaste;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredAdvancedPasteEnabledValue();
public void Disable()
{
if (_ipc != null)
{
_ipc.Send(Constants.AdvancedPasteTerminateAppMessage());
_ipc.End();
_ipc = null;
}
}
private const string IpcName = @"\\.\pipe\PowerToys.AdvancedPaste";
private TwoWayPipeMessageIPCManaged? _ipc;
public void Enable()
{
_ipc = new TwoWayPipeMessageIPCManaged(string.Empty, IpcName, (_) => { });
_ipc.Start();
PopulateShortcuts();
}
public void OnSettingsChanged()
{
PopulateShortcuts();
}
public void PopulateShortcuts()
{
_ipc ??= new TwoWayPipeMessageIPCManaged(string.Empty, @"\\.\pipe\PowerToys.AdvancedPaste", (_) => { });
Shortcuts.Clear();
AdvancedPasteSettings settings = SettingsUtils.Default.GetSettingsOrDefault<AdvancedPasteSettings>(Name);
Shortcuts.Add((settings.Properties.AdvancedPasteUIShortcut, () =>
_ipc.Send(Constants.AdvancedPasteShowUIMessage())
));
Shortcuts.Add((settings.Properties.PasteAsPlainTextShortcut, TryToPasteAsPlainText));
Shortcuts.Add((settings.Properties.PasteAsMarkdownShortcut, () => _ipc.Send(Constants.AdvancedPasteMarkdownMessage())));
Shortcuts.Add((settings.Properties.PasteAsJsonShortcut, () => _ipc.Send(Constants.AdvancedPasteJsonMessage())));
HotkeyAccessor[] hotkeyAccessors = settings.GetAllHotkeyAccessors();
int additionalActionsCount = settings.Properties.AdditionalActions.GetAllActions().Count() - 2;
for (int i = 0; i < additionalActionsCount; i++)
{
int scopedI = i;
Shortcuts.Add((hotkeyAccessors[4 + i].Value, () => _ipc.Send(Constants.AdvancedPasteAdditionalActionMessage() + " " + (3 + scopedI))));
}
for (int i = 4 + additionalActionsCount; i < hotkeyAccessors.Length; i++)
{
int scopedI = i;
Shortcuts.Add((hotkeyAccessors[i].Value, () => _ipc.Send(Constants.AdvancedPasteCustomActionMessage() + " " + (scopedI - 4 - additionalActionsCount))));
}
}
private void TryToPasteAsPlainText()
{
if (Clipboard.ContainsText())
{
string text = Clipboard.GetText();
SendKeys.SendWait(text);
}
throw new NotImplementedException();
}
public void Dispose()
{
_ipc?.Dispose();
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public override string ProcessPath => "WinUI3Apps\\PowerToys.AdvancedPaste.exe";
public override string ProcessName => "PowerToys.AdvancedPaste";
public override string ProcessArguments => IpcName;
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
}
}

View File

@@ -0,0 +1,59 @@
// 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.Json;
using System.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class AlwaysOnTopModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.AlwaysOnTop;
public string Name => "AlwaysOnTop";
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredAlwaysOnTopEnabledValue();
public void Disable()
{
using var terminateEventWrapper = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.AlwaysOnTopTerminateEvent());
terminateEventWrapper.Set();
}
public void Enable()
{
InitializeHotkey();
}
private void InitializeHotkey()
{
Shortcuts.Clear();
Shortcuts.Add((SettingsUtils.Default.GetSettings<AlwaysOnTopSettings>(Name).Properties.Hotkey.Value, () =>
{
using var pinEventWrapper = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.AlwaysOnTopPinEvent());
pinEventWrapper.Set();
}
));
}
public void OnSettingsChanged()
{
InitializeHotkey();
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public override string ProcessPath => "PowerToys.AlwaysOnTop.exe";
public override string ProcessName => "PowerToys.AlwaysOnTop";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument | ProcessLaunchOptions.ElevateIfApplicable;
}
}

View File

@@ -0,0 +1,41 @@
// 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.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class AwakeModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
public string Name => "Awake";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.Awake;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredAwakeEnabledValue();
public override string ProcessPath => "PowerToys.Awake.exe";
public override string ProcessName => "PowerToys.Awake";
public override string ProcessArguments => $"--use-pt-config --pid {Environment.ProcessId.ToString(CultureInfo.InvariantCulture)}";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess;
public void Disable()
{
using var terminateEventWrapper = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.AwakeExitEvent());
terminateEventWrapper.Set();
}
public void Enable()
{
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using ManagedCommon;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class CmdNotFoundModuleInterface : IPowerToysModule
{
public string Name => "CmdNotFound";
public bool Enabled => true;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredCmdNotFoundEnabledValue();
public CmdNotFoundModuleInterface()
{
if (GpoRuleConfigured == GpoRuleConfigured.Disabled)
{
UninstallModule();
}
if (GpoRuleConfigured == GpoRuleConfigured.Enabled)
{
InstallModule();
}
}
public void InstallModule()
{
Logger.LogInfo("Installing Command Not Found module invoked through GPO");
new Thread(async () =>
{
Process p = Process.Start("pwsh.exe", "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + Path.GetDirectoryName(Environment.ProcessPath) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\EnableModule.ps1" + "\"" + " -scriptPath \"" + Path.GetDirectoryName(Environment.ProcessPath) + "\"");
await p.WaitForExitAsync();
if (p.ExitCode == 0)
{
Logger.LogInfo("Command Not Found was successfully installed.");
return;
}
Logger.LogInfo("Command Not Found failed to install with exit code: " + p.ExitCode);
}).Start();
}
public void UninstallModule()
{
Logger.LogInfo("Uninstalling Command Not Found module invoked through GPO");
new Thread(async () =>
{
Process p = Process.Start("pwsh.exe", "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + Path.GetDirectoryName(Environment.ProcessPath) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\DisableModule.ps1" + "\"" + " -scriptPath \"" + Path.GetDirectoryName(Environment.ProcessPath) + "\"");
await p.WaitForExitAsync();
if (p.ExitCode == 0)
{
Logger.LogInfo("Command Not Found was successfully uninstalled.");
return;
}
Logger.LogInfo("Command Not Found failed to uninstall with exit code: " + p.ExitCode);
}).Start();
}
public void Disable()
{
}
public void Enable()
{
}
}
}

View File

@@ -0,0 +1,59 @@
// 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.Json;
using System.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class ColorPickerModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "ColorPicker";
public bool Enabled => SettingsUtils.Default.GetSettingsOrDefault<GeneralSettings>().Enabled.ColorPicker;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredColorPickerEnabledValue();
public void Disable()
{
using var terminateEventWrapper = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.TerminateColorPickerSharedEvent());
terminateEventWrapper.Set();
}
public void Enable()
{
InitializeShortcuts();
}
private void InitializeShortcuts()
{
Shortcuts.Clear();
Shortcuts.Add((SettingsUtils.Default.GetSettings<ColorPickerSettings>(Name).Properties.ActivationShortcut, () =>
{
using var showUiEventWrapper = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowColorPickerSharedEvent());
showUiEventWrapper.Set();
}
));
}
public void OnSettingsChanged()
{
InitializeShortcuts();
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public override string ProcessPath => "PowerToys.ColorPickerUI.exe";
public override string ProcessName => "PowerToys.ColorPickerUI";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Helpers;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class CommandPaletteModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
private const string PackageName = "Microsoft.CommandPalette"
#if DEBUG
+ ".Dev"
#endif
;
public string Name => "CmdPal";
public bool Enabled => SettingsUtils.Default.GetSettingsOrDefault<GeneralSettings>().Enabled.CmdPal;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredCmdPalEnabledValue();
public override string ProcessPath => "explorer.exe";
public override string ProcessArguments => "x-cmdpal://background";
public override string ProcessName => string.Empty;
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.UseShellExecute | ProcessLaunchOptions.NeverExit;
public void Disable()
{
}
public void Enable()
{
if (PackageHelper.GetRegisteredPackage(PackageName, false) is null)
{
try
{
string architectureString = RuntimeInformation.ProcessArchitecture == Architecture.X64 ? "x64" : "ARM64";
#if DEBUG
string[] msixFiles = PackageHelper.FindMsixFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\WinUI3Apps\\CmdPal\\AppPackages\\Microsoft.CmdPal.UI_0.0.1.0_Debug_Test\\", false);
string[] dependencies = PackageHelper.FindMsixFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\WinUI3Apps\\CmdPal\\AppPackages\\Microsoft.CmdPal.UI_0.0.1.0_Debug_Test\\Dependencies\\" + architectureString + "\\", true);
#else
string[] msixFiles = PackageHelper.FindMsixFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\WinUI3Apps\\CmdPal\\", false);
string[] dependencies = PackageHelper.FindMsixFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\WinUI3Apps\\CmdPal\\Dependencies\\", true);
#endif
if (msixFiles.Length > 0)
{
if (!PackageHelper.InstallPackage(msixFiles[0], dependencies))
{
Logger.LogError("Failed to register Command Palette package.");
}
}
}
catch (Exception ex)
{
Logger.LogError("Exception occurred while enabling Command Palette package.", ex);
}
}
if (PackageHelper.GetRegisteredPackage(PackageName, false) is null)
{
Logger.LogError("Command Palette package is not registered after attempting to enable it.");
return;
}
}
}
}

View File

@@ -0,0 +1,68 @@
// 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.Json;
using System.Threading;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed partial class CropAndLockModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IDisposable, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "CropAndLock";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.CropAndLock;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredCropAndLockEnabledValue();
private EventWaitHandle _reparentEvent = new(false, EventResetMode.AutoReset, Constants.CropAndLockReparentEvent());
private EventWaitHandle _thumbnailEvent = new(false, EventResetMode.AutoReset, Constants.CropAndLockThumbnailEvent());
private EventWaitHandle _terminateEvent = new(false, EventResetMode.AutoReset, Constants.CropAndLockExitEvent());
public void Disable()
{
_terminateEvent.Set();
}
public void Enable()
{
PopulateShortcuts();
}
public void PopulateShortcuts()
{
Shortcuts.Clear();
var settings = SettingsUtils.Default.GetSettings<CropAndLockSettings>(Name);
Shortcuts.Add((settings.Properties.ThumbnailHotkey.Value, () => _thumbnailEvent.Set()));
Shortcuts.Add((settings.Properties.ReparentHotkey.Value, () => _reparentEvent.Set()));
}
public void OnSettingsChanged()
{
PopulateShortcuts();
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public override string ProcessPath => "PowerToys.CropAndLock.exe";
public override string ProcessName => "PowerToys.CropAndLock";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public void Dispose()
{
GC.SuppressFinalize(this);
_reparentEvent.Dispose();
_thumbnailEvent.Dispose();
_terminateEvent.Dispose();
}
}
}

View File

@@ -0,0 +1,69 @@
// 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.Runtime.InteropServices;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class CursorWrapModuleInterface : IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "CursorWrap";
private bool _hookActive;
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.CursorWrap;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredCursorWrapEnabledValue();
public void Disable()
{
_hookActive = false;
CursorWrapStopMouseHook();
}
public void Enable()
{
InitializeShortcuts();
_hookActive = true;
CursorWrapStartMouseHook();
}
public void OnSettingsChanged()
{
InitializeShortcuts();
}
private void InitializeShortcuts()
{
Shortcuts.Clear();
Shortcuts.Add((SettingsUtils.Default.GetSettings<CursorWrapSettings>(Name).Properties.DefaultActivationShortcut, () =>
{
if (_hookActive)
{
CursorWrapStopMouseHook();
_hookActive = false;
}
else
{
CursorWrapStartMouseHook();
_hookActive = true;
}
}));
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
[DllImport("PowerToys.CursorWrap.dll")]
private static extern bool CursorWrapStartMouseHook();
[DllImport("PowerToys.CursorWrap.dll")]
private static extern bool CursorWrapStopMouseHook();
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class EnvironmentVariablesModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
public string Name => "Environment Variables";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.EnvironmentVariables;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredEnvironmentVariablesEnabledValue();
public override string ProcessPath => "WinUI3Apps\\PowerToys.EnvironmentVariables.exe";
public override string ProcessName => "PowerToys.EnvironmentVariables";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SupressLaunchOnModuleEnabled | ProcessLaunchOptions.NeverExit;
public void Disable()
{
}
public void Enable()
{
}
}
}

View File

@@ -0,0 +1,72 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class FancyZonesModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleCustomActionsProvider, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "FancyZones";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.FancyZones;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredFancyZonesEnabledValue();
public override string ProcessPath => "PowerToys.FancyZones.exe";
public override string ProcessName => "PowerToys.FancyZones";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument | ProcessLaunchOptions.HideUI;
public void Disable()
{
using var terminateEvent = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset, Constants.FZEExitEvent());
terminateEvent.Set();
}
public void Enable()
{
InitializeShortcuts();
}
public Dictionary<string, Action> CustomActions => new()
{
{
"ToggledFZEditor",
() =>
{
EnsureLaunched();
using var invokeFZEditorEvent = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset, Constants.FZEToggleEvent());
invokeFZEditorEvent.Set();
}
},
};
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public void InitializeShortcuts()
{
Shortcuts.Clear();
var settings = SettingsUtils.Default.GetSettings<FancyZonesSettings>(Name);
Shortcuts.Add((settings.Properties.FancyzonesEditorHotkey.Value, () =>
{
EnsureLaunched();
using var invokeFZEditorEvent = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset, Constants.FZEToggleEvent());
invokeFZEditorEvent.Set();
}
));
}
public void OnSettingsChanged()
{
InitializeShortcuts();
}
}
}

View File

@@ -0,0 +1,331 @@
// 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.Linq;
using System.Reflection;
using System.Text.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class FileExplorerModuleInterface : IPowerToysModule, IPowerToysModuleSettingsChangedSubscriber
{
private record struct FileExplorerModule(Func<bool> IsEnabled, GpoRuleConfigured GpoRule, RegistryChangeSet RegistryChanges);
private static readonly List<FileExplorerModule> _fileExplorerModules;
private static readonly string[] ExtSVG = { ".svg" };
private static readonly string[] ExtMarkdown = { ".md", ".markdown", ".mdown", ".mkdn", ".mkd", ".mdwn", ".mdtxt", ".mdtext" };
private static readonly string[] ExtPDF = { ".pdf" };
private static readonly string[] ExtGCode = { ".gcode" };
private static readonly string[] ExtBGCode = { ".bgcode" };
private static readonly string[] ExtSTL = { ".stl" };
private static readonly string[] ExtQOI = { ".qoi" };
static FileExplorerModuleInterface()
{
static PowerPreviewProperties GetProperties() => SettingsUtils.Default.GetSettings<PowerPreviewSettings>(PowerPreviewSettings.ModuleName).Properties;
string installationPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
_fileExplorerModules = [
new FileExplorerModule(
() => GetProperties().EnableBgcodePreview,
GPOWrapper.GetConfiguredBgcodePreviewEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.PreviewHandler, "{0e6d5bdd-d5f8-4692-a089-8bb88cdd37f4}", "BgcodePreviewHandler", "Binary G-code Preview Handler", ExtBGCode)),
new FileExplorerModule(
() => GetProperties().EnableBgcodeThumbnail,
GPOWrapper.GetConfiguredBgcodeThumbnailsEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.ThumbnailProvider, "{5c93a1e4-99d0-4fb3-991c-6c296a27be21}", "BgcodeThumbnailProvider", "Binary G-code Thumbnail Provider", ExtBGCode)),
new FileExplorerModule(
() => GetProperties().EnableGcodePreview,
GPOWrapper.GetConfiguredGcodePreviewEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.PreviewHandler, "{A0257634-8812-4CE8-AF11-FA69ACAEAFAE}", "GcodePreviewHandler", "G-code Preview Handler", ExtGCode)),
new FileExplorerModule(
() => GetProperties().EnableGcodeThumbnail,
GPOWrapper.GetConfiguredGcodeThumbnailsEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.ThumbnailProvider, "{F2847CBE-CD03-4C83-A359-1A8052C1B9D5}", "GcodeThumbnailProvider", "G-code Thumbnail Provider", ExtGCode)),
new FileExplorerModule(
() => GetProperties().EnableMdPreview,
GPOWrapper.GetConfiguredMarkdownPreviewEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.PreviewHandler, "{60789D87-9C3C-44AF-B18C-3DE2C2820ED3}", "MarkdownPreviewHandler", "Markdown Preview Handler", ExtMarkdown)),
new FileExplorerModule(
() => GetProperties().EnablePdfPreview,
GPOWrapper.GetConfiguredPdfPreviewEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.PreviewHandler, "{A5A41CC7-02CB-41D4-8C9B-9087040D6098}", "PdfPreviewHandler", "PDF Preview Handler", ExtPDF)),
new FileExplorerModule(
() => GetProperties().EnablePdfThumbnail,
GPOWrapper.GetConfiguredPdfThumbnailsEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.ThumbnailProvider, "{D8BB9942-93BD-412D-87E4-33FAB214DC1A}", "PdfThumbnailProvider", "PDF Thumbnail Provider", ExtPDF)),
new FileExplorerModule(
() => GetProperties().EnableQoiPreview,
GPOWrapper.GetConfiguredQoiPreviewEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.PreviewHandler, "{729B72CD-B72E-4FE9-BCBF-E954B33FE699}", "QoiPreviewHandler", "QOI Preview Handler", ExtQOI)),
new FileExplorerModule(
() => GetProperties().EnableQoiThumbnail,
GPOWrapper.GetConfiguredQoiThumbnailsEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.ThumbnailProvider, "{AD856B15-D25E-4008-AFB7-AFAA55586188}", "QoiThumbnailProvider", "QOI Thumbnail Provider", ExtQOI, "image", "Picture")),
new FileExplorerModule(
() => GetProperties().EnableStlThumbnail,
GPOWrapper.GetConfiguredStlThumbnailsEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.ThumbnailProvider, "{77257004-6F25-4521-B602-50ECC6EC62A6}", "StlThumbnailProvider", "STL Thumbnail Provider", ExtSTL)),
new FileExplorerModule(
() => GetProperties().EnableSvgPreview,
GPOWrapper.GetConfiguredSvgPreviewEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.PreviewHandler, "{FCDD4EED-41AA-492F-8A84-31A1546226E0}", "SvgPreviewHandler", "SVG Preview Handler", ExtSVG)),
new FileExplorerModule(
() => GetProperties().EnableSvgThumbnail,
GPOWrapper.GetConfiguredSvgThumbnailsEnabledValue(),
GetFileExplorerAddOnChangeSet(FileExplorerAddOnType.ThumbnailProvider, "{10144713-1526-46C9-88DA-1FB52807A9FF}", "SvgThumbnailProvider", "SVG Thumbnail Provider", ExtSVG, "image", "Picture")),
GetMonacoFileExplorerModule(installationPath)
];
}
private static FileExplorerModule GetMonacoFileExplorerModule(string installationPath)
{
// .svgz is a binary file type that Monaco cannot handle, so we exclude it from the preview handler
string[] extExclusions = [..ExtMarkdown, ..ExtSVG, ".svgz"];
List<string> extensions = [];
string languagesFilePath = Path.Combine(installationPath, "Assets\\Monaco\\monaco_languages.json");
if (!File.Exists(languagesFilePath))
{
Logger.LogError("PowerPreviewModuleInterface: Unable to find monaco_languages.json file at " + languagesFilePath);
goto returnLabel;
}
try
{
JsonDocument jsonDocument = JsonDocument.Parse(File.ReadAllText(languagesFilePath));
var list = jsonDocument.RootElement.GetProperty("list");
foreach (var item in list.EnumerateArray())
{
if (item.TryGetProperty("extensions", out JsonElement extensionsElement))
{
foreach (var ext in extensionsElement.EnumerateArray())
{
string extension = ext.GetString() ?? string.Empty;
if (!string.IsNullOrEmpty(extension) && !extensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
extensions.Add(extension);
}
}
}
}
}
catch (Exception ex)
{
Logger.LogError("PowerPreviewModuleInterface: Failed to parse monaco_languages.json file.", ex);
}
returnLabel:
return new FileExplorerModule(
() => SettingsUtils.Default.GetSettings<PowerPreviewSettings>(PowerPreviewSettings.ModuleName).Properties.EnableMonacoPreview,
GPOWrapper.GetConfiguredMonacoPreviewEnabledValue(),
GetFileExplorerAddOnChangeSet(
FileExplorerAddOnType.PreviewHandler,
"{D8034CFA-F34B-41FE-AD45-62FCBB52A6DA}",
"MonacoPreviewHandler",
"Monaco Preview Handler",
[.. extensions.Where(ext => !extExclusions.Contains(ext))]));
}
private enum FileExplorerAddOnType
{
ThumbnailProvider,
PreviewHandler,
}
private static RegistryChangeSet GetFileExplorerAddOnChangeSet(FileExplorerAddOnType type, string handlerClsid, string className, string displayName, string[] fileTypes, string? perceivedType = null, string? fileKindType = null)
{
string installationPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
string pathToHandler = Path.Combine(installationPath, "PowerToys.FileExplorerDllExporter.dll");
string clsidPath = "Software\\Classes\\CLSID\\" + handlerClsid;
string inprocServer32Path = clsidPath + "\\InprocServer32";
string assemblyKeyValue;
int lastDotPos = className.LastIndexOf('.');
if (lastDotPos != -1)
{
assemblyKeyValue = string.Concat("PowerToys.", className.AsSpan(lastDotPos + 1));
}
else
{
assemblyKeyValue = "PowerToys." + className;
}
assemblyKeyValue += $", Version={Assembly.GetExecutingAssembly().GetName().Version!}, Culture=neutral";
List<RegistryValueChange> changes = [
new RegistryValueChange
{
KeyPath = clsidPath,
KeyName = "DisplayName",
Value = displayName,
},
new RegistryValueChange
{
KeyPath = clsidPath,
KeyName = null,
Value = className,
},
new RegistryValueChange
{
KeyPath = inprocServer32Path,
KeyName = null,
Value = pathToHandler,
},
new RegistryValueChange
{
KeyPath = inprocServer32Path,
KeyName = "Assembly",
Value = assemblyKeyValue,
},
new RegistryValueChange
{
KeyPath = inprocServer32Path,
KeyName = "Class",
Value = className,
},
new RegistryValueChange
{
KeyPath = inprocServer32Path,
KeyName = "ThreadingModel",
Value = "Apartment",
},
];
foreach (string fileType in fileTypes)
{
string fileTypePath = "Software\\Classes\\" + fileType;
string fileAssociationPath = fileTypePath + "\\shellex\\" + (type == FileExplorerAddOnType.PreviewHandler ? IPREVIEWHANDLERCLSID : ITHUMBNAILPROVIDERCLSID);
changes.Add(new RegistryValueChange
{
KeyPath = fileAssociationPath,
KeyName = null,
Value = handlerClsid,
});
if (!string.IsNullOrEmpty(fileKindType))
{
// Registering a file type as a kind needs to be done at the HKEY_LOCAL_MACHINE level.
// Make it optional as well so that we don't fail registering the handler if we can't write to HKEY_LOCAL_MACHINE.
string kindPath = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\KindMap";
changes.Add(new RegistryValueChange
{
Scope = Microsoft.Win32.RegistryHive.LocalMachine,
KeyPath = kindPath,
KeyName = fileType,
Value = fileKindType,
Required = false,
});
}
if (!string.IsNullOrEmpty(perceivedType))
{
changes.Add(new RegistryValueChange
{
KeyPath = fileTypePath,
KeyName = "PerceivedType",
Value = perceivedType,
});
}
// this regfile registry key has precedence over Software\Classes\.reg for .reg files
if (type == FileExplorerAddOnType.PreviewHandler && fileType.Equals(".reg", StringComparison.OrdinalIgnoreCase))
{
string regFilePath = "Software\\Classes\\regfile\\shellex\\" + IPREVIEWHANDLERCLSID;
changes.Add(new RegistryValueChange
{
KeyPath = regFilePath,
KeyName = null,
Value = handlerClsid,
});
}
}
if (type == FileExplorerAddOnType.PreviewHandler)
{
string previewHostClsid = "{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}";
string previewHandlerListPath = "(Software\\Microsoft\\Windows\\CurrentVersion\\PreviewHandlers)";
changes.Add(new RegistryValueChange
{
KeyPath = clsidPath,
KeyName = "AppID",
Value = previewHostClsid,
});
changes.Add(new RegistryValueChange
{
KeyPath = previewHandlerListPath,
KeyName = handlerClsid,
Value = displayName,
});
}
changes.Add(new RegistryValueChange
{
Scope = Microsoft.Win32.RegistryHive.LocalMachine,
KeyPath = "Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved",
KeyName = handlerClsid,
Value = displayName,
Required = false,
});
return new RegistryChangeSet
{
Changes = [.. changes],
};
}
private const string ITHUMBNAILPROVIDERCLSID = "{E357FCCD-A995-4576-B01F-234630154E96}";
private const string IPREVIEWHANDLERCLSID = "{8895b1c6-b41f-4c1c-a562-0d564250836f}";
public string Name => "File Explorer";
public bool Enabled => true;
public GpoRuleConfigured GpoRuleConfigured => GpoRuleConfigured.Unavailable;
public void Disable()
{
}
public void Enable()
{
OnSettingsChanged();
}
public void OnSettingsChanged()
{
foreach (FileExplorerModule submodule in _fileExplorerModules)
{
if (submodule.GpoRule == GpoRuleConfigured.Disabled)
{
submodule.RegistryChanges.UnApply();
continue;
}
if (submodule.IsEnabled() || submodule.GpoRule == GpoRuleConfigured.Enabled)
{
submodule.RegistryChanges.Apply();
}
else
{
submodule.RegistryChanges.UnApply();
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
// 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.Runtime.InteropServices;
using System.Text.Json;
using System.Threading;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class FindMyMouseModuleInterface : IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "FindMyMouse";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.FindMyMouse;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredFindMyMouseEnabledValue();
public void Disable()
{
FindMyMouseDisable();
}
public void Enable()
{
InitializeShortcuts();
var thread = new Thread(() =>
{
uint version = 0x00010008;
int hr = MddBootstrapInitialize(version, 0, IntPtr.Zero);
if (hr < 0)
{
throw new InvalidOperationException($"Windows app sdk could not be initialized for MouseJump. HR code:{hr}");
}
FindMyMouseMain();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
public void OnSettingsChanged()
{
InitializeShortcuts();
NativeMethods.PostMessageW(GetSonarHwnd(), GetWmPrivSettingsChanged(), IntPtr.Zero, IntPtr.Zero);
}
private void InitializeShortcuts()
{
Shortcuts.Clear();
Shortcuts.Add((SettingsUtils.Default.GetSettings<FindMyMouseSettings>(Name).Properties.ActivationShortcut, () =>
{
NativeMethods.PostMessageW(GetSonarHwnd(), GetWmPrivShortcut(), IntPtr.Zero, IntPtr.Zero);
}
));
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
[DllImport("PowerToys.FindMyMouse.dll")]
public static extern void FindMyMouseMain();
[DllImport("PowerToys.FindMyMouse.dll")]
public static extern void FindMyMouseDisable();
[DllImport("PowerToys.FindMyMouse.dll")]
public static extern IntPtr GetSonarHwnd();
[DllImport("PowerToys.FindMyMouse.dll")]
public static extern uint GetWmPrivShortcut();
[DllImport("PowerToys.FindMyMouse.dll")]
public static extern uint GetWmPrivSettingsChanged();
[DllImport("Microsoft.WindowsAppRuntime.Bootstrap.dll", CharSet = CharSet.Unicode)]
private static extern int MddBootstrapInitialize(uint majorMinorVersion, uint versionTag, IntPtr packageVersion);
}
}

View File

@@ -0,0 +1,35 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class HostsModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
public bool Enabled => SettingsUtils.Default.GetSettingsOrDefault<GeneralSettings>().Enabled.Hosts;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredHostsFileEditorEnabledValue();
public string Name => "Hosts";
public override string ProcessPath => "WinUI3Apps\\PowerToys.Hosts.exe";
public override string ProcessName => "PowerToys.Hosts";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SupressLaunchOnModuleEnabled | ProcessLaunchOptions.NeverExit;
public void Disable()
{
}
public void Enable()
{
}
}
}

View File

@@ -0,0 +1,38 @@
// 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.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class KeyboardManagerModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
public string Name => "Keyboard Manager";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.KeyboardManager;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredKeyboardManagerEnabledValue();
public override string ProcessPath => "KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe";
public override string ProcessName => "PowerToys.KeyboardManagerEngine";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument | ProcessLaunchOptions.RealtimePriority;
public void Disable()
{
using var terminateEvent = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.TerminateKBMSharedEvent());
terminateEvent.Set();
}
public void Enable()
{
}
}
}

View File

@@ -0,0 +1,60 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
using Settings.UI.Library;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class LightSwitchModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleShortcutsProvider
{
public string Name => "LightSwitch";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.LightSwitch;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredLightSwitchEnabledValue();
public override string ProcessPath => "LightSwitchService\\PowerToys.LightSwitchService.exe";
public override string ProcessName => "PowerToys.LightSwitchService";
public override string ProcessArguments => $"--pid {Environment.ProcessId}";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess;
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts
{
get => [(SettingsUtils.Default.GetSettings<LightSwitchSettings>(Name).Properties.ToggleThemeHotkey.Value, () =>
{
LightSwitchProperties properties = SettingsUtils.Default.GetSettings<LightSwitchSettings>(Name).Properties;
EnsureLaunched();
if (properties.ChangeSystem.Value)
{
ThemeHelper.SetSystemTheme(!ThemeHelper.GetCurrentSystemTheme());
}
if (properties.ChangeApps.Value)
{
ThemeHelper.SetAppsTheme(!ThemeHelper.GetCurrentAppsTheme());
}
using var manualOverrideEvent = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset, Constants.LightSwitchManualOverrideEvent());
manualOverrideEvent.Set();
})];
}
public void Disable()
{
}
public void Enable()
{
}
}
}

View File

@@ -0,0 +1,54 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class MeasureToolModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "Measure Tool";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.MeasureTool;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredScreenRulerEnabledValue();
public override string ProcessPath => "WinUI3Apps\\PowerToys.MeasureToolUI.exe";
public override string ProcessName => "PowerToys.MeasureToolUI";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument | ProcessLaunchOptions.SupressLaunchOnModuleEnabled;
public void Disable()
{
}
public void Enable()
{
PopulateShortcuts();
}
public void OnSettingsChanged()
{
PopulateShortcuts();
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
private void PopulateShortcuts()
{
Shortcuts.Clear();
Shortcuts.Add((SettingsUtils.Default.GetSettings<MeasureToolSettings>(Name).Properties.ActivationShortcut, () =>
{
LaunchProcess();
}
));
}
}
}

View File

@@ -0,0 +1,58 @@
// 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.Runtime.InteropServices;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed partial class MouseHighlighterModuleInterface : IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "MouseHighlighter";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.MouseHighlighter;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredMouseHighlighterEnabledValue();
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public void Disable()
{
DisableMouseHighlighter();
}
public void Enable()
{
EnableMouseHighlighter();
}
private void InitializeShortcuts()
{
Shortcuts.Clear();
Shortcuts.Add((SettingsUtils.Default.GetSettings<MouseHighlighterSettings>(Name).Properties.ActivationShortcut, ToggleMouseHighlighter));
}
public void OnSettingsChanged()
{
InitializeShortcuts();
MouseHighlighterSettingsChanged();
}
[LibraryImport("PowerToys.MouseHighlighter.dll")]
internal static partial void DisableMouseHighlighter();
[LibraryImport("PowerToys.MouseHighlighter.dll")]
internal static partial void EnableMouseHighlighter();
[LibraryImport("PowerToys.MouseHighlighter.dll")]
internal static partial void MouseHighlighterSettingsChanged();
[LibraryImport("PowerToys.MouseHighlighter.dll")]
internal static partial void ToggleMouseHighlighter();
}
}

View File

@@ -0,0 +1,58 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class MouseJumpModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "MouseJump";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.MouseJump;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredMouseJumpEnabledValue();
public override string ProcessPath => "PowerToys.MouseJumpUI.exe";
public override string ProcessName => "PowerToys.MouseJumpUI";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public void Disable()
{
using var terminateEvent = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset, Constants.TerminateMouseJumpSharedEvent());
terminateEvent.Set();
}
public void Enable()
{
PopulateShortcuts();
}
public void OnSettingsChanged()
{
PopulateShortcuts();
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public void PopulateShortcuts()
{
Shortcuts.Clear();
var settings = SettingsUtils.Default.GetSettings<MouseJumpSettings>(Name);
Shortcuts.Add((settings.Properties.ActivationShortcut, () =>
{
EnsureLaunched();
using var invokeMouseJumpEvent = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset, Constants.MouseJumpShowPreviewEvent());
invokeMouseJumpEvent.Set();
}));
}
}
}

View File

@@ -0,0 +1,65 @@
// 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.Runtime.InteropServices;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed partial class MousePointerCrosshairsModuleInterface : IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "MousePointerCrosshairs";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.MousePointerCrosshairs;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredMousePointerCrosshairsEnabledValue();
public void Disable()
{
DisableMousePointerCrosshairs();
}
public void Enable()
{
InitializeShortcuts();
EnableMousePointerCrosshairs();
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
private void InitializeShortcuts()
{
Shortcuts.Clear();
var settings = SettingsUtils.Default.GetSettings<MousePointerCrosshairsSettings>(Name).Properties;
Shortcuts.Add((settings.ActivationShortcut, OnMousePointerCrosshairsActivationShortcut));
Shortcuts.Add((settings.GlidingCursorActivationShortcut, OnMousePointerCrosshairsGlidingCursorShortcut));
}
public void OnSettingsChanged()
{
OnMousePointerCrosshairsSettingsChanged();
InitializeShortcuts();
}
[LibraryImport("PowerToys.MousePointerCrosshairs.dll")]
internal static partial void DisableMousePointerCrosshairs();
[LibraryImport("PowerToys.MousePointerCrosshairs.dll")]
internal static partial void EnableMousePointerCrosshairs();
[LibraryImport("PowerToys.MousePointerCrosshairs.dll")]
internal static partial void OnMousePointerCrosshairsSettingsChanged();
[LibraryImport("PowerToys.MousePointerCrosshairs.dll")]
internal static partial void OnMousePointerCrosshairsActivationShortcut();
[LibraryImport("PowerToys.MousePointerCrosshairs.dll")]
internal static partial void OnMousePointerCrosshairsGlidingCursorShortcut();
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class PowerAccentModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
public string Name => "PowerAccent";
public bool Enabled => SettingsUtils.Default.GetSettingsOrDefault<GeneralSettings>().Enabled.PowerAccent;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredQuickAccentEnabledValue();
public override string ProcessPath => "PowerToys.PowerAccent.exe";
public override string ProcessName => "PowerToys.PowerAccent";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public void Disable()
{
}
public void Enable()
{
}
}
}

View File

@@ -0,0 +1,59 @@
// 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.Json;
using System.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class PowerOCRModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "TextExtractor";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.PowerOcr;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredTextExtractorEnabledValue();
public override string ProcessPath => "PowerToys.PowerOCR.exe";
public override string ProcessName => "PowerToys.PowerOCR";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
private void PopulateShortcuts()
{
Shortcuts.Clear();
Shortcuts.Add((SettingsUtils.Default.GetSettings<PowerOcrSettings>(Name).Properties.ActivationShortcut, () =>
{
using var invokeOcrEvent = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowPowerOCRSharedEvent());
invokeOcrEvent.Set();
}
));
}
public void Disable()
{
using var terminateEvent = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.TerminatePowerOCRSharedEvent());
terminateEvent.Set();
}
public void Enable()
{
PopulateShortcuts();
}
public void OnSettingsChanged()
{
PopulateShortcuts();
}
}
}

View File

@@ -0,0 +1,54 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class PowerToysRunModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleShortcutsProvider
{
public string Name => "PowerToys Run";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.PowerLauncher;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredPowerLauncherEnabledValue();
public override string ProcessPath => Path.GetFullPath("PowerToys.PowerLauncher.exe");
public override string ProcessName => "PowerToys.PowerLauncher";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.ElevateIfApplicable | ProcessLaunchOptions.SingletonProcess;
public override string ProcessArguments => $"-powerToysPid {Environment.ProcessId}";
public void Disable()
{
using var terminateEvent = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset, Constants.RunExitEvent());
terminateEvent.Set();
}
public void Enable()
{
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts =>
[
(
SettingsUtils.Default.GetSettings<PowerLauncherSettings>(Name).Properties.OpenPowerLauncher,
() =>
{
EnsureLaunched();
using var invokeRunEvent = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset, Constants.PowerLauncherCentralizedHookSharedEvent());
invokeRunEvent.Set();
}
),
];
}
}

View File

@@ -0,0 +1,142 @@
// 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.Reflection;
using System.Text.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.Win32;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class RegistryPreviewModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleCustomActionsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public bool Enabled => SettingsUtils.Default.GetSettingsOrDefault<GeneralSettings>().Enabled.RegistryPreview;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredHostsFileEditorEnabledValue();
public string Name => "RegistryPreview";
public override string ProcessPath => "WinUI3Apps\\PowerToys.RegistryPreview.exe";
public override string ProcessName => "PowerToys.RegistryPreview";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SupressLaunchOnModuleEnabled | ProcessLaunchOptions.NeverExit;
public void Disable()
{
if (!RegistryPreviewChangeSet.UnApplyIfApplied())
{
Logger.LogError("Unapplying registry changes failed");
}
}
public void OnSettingsChanged()
{
bool defaultRegApp = SettingsUtils.Default.GetSettings<RegistryPreviewSettings>(Name).Properties.DefaultRegApp;
if (defaultRegApp && !RegistryPreviewSetDefaultAppChangeSet.IsApplied)
{
if (!RegistryPreviewSetDefaultAppChangeSet.Apply())
{
Logger.LogError("Applying reg default handler failed.");
}
NativeMethods.SHChangeNotify(NativeMethods.SHCNE_ASSOCCHANGED, NativeMethods.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
}
else if (!defaultRegApp && RegistryPreviewSetDefaultAppChangeSet.IsApplied)
{
if (RegistryPreviewSetDefaultAppChangeSet.UnApply())
{
Logger.LogError("Unapplying reg default handler failed.");
}
NativeMethods.SHChangeNotify(NativeMethods.SHCNE_ASSOCCHANGED, NativeMethods.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
}
}
public void Enable()
{
if (!RegistryPreviewChangeSet.ApplyIfNotApplied())
{
Logger.LogError("Applying registry changes failed");
}
OnSettingsChanged();
}
public Dictionary<string, Action> CustomActions
{
get => new() { { "Launch", () => LaunchProcess() } };
}
private RegistryChangeSet RegistryPreviewSetDefaultAppChangeSet
{
get
{
string installationDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
string appName = "Registry Preview";
string registryKeyPrefix = "Software\\Classes\\";
RegistryValueChange[] changes =
[
new RegistryValueChange
{
KeyPath = registryKeyPrefix + ProcessName + "\\Application",
KeyName = "ApplicationName",
Value = appName,
},
new RegistryValueChange
{
KeyPath = registryKeyPrefix + ProcessName + "\\DefaultIcon",
KeyName = null,
Value = installationDir + "\\WinUI3Apps\\PowerToys.RegistryPreview.exe",
},
new RegistryValueChange
{
KeyPath = registryKeyPrefix + ProcessName + "\\shell\\open\\command",
KeyName = null,
Value = installationDir + "\\WinUI3Apps\\PowerToys.RegistryPreview.exe \"----ms-protocol:ms-encodedlaunch:App?ContractId=Windows.File&Verb=open&File=%1\"",
},
new RegistryValueChange
{
KeyPath = registryKeyPrefix + ".reg\\OpenWithProgIDs",
KeyName = null,
Value = ProcessName,
}
];
return new RegistryChangeSet { Changes = changes };
}
}
private RegistryChangeSet RegistryPreviewChangeSet
{
get
{
string installationDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
RegistryValueChange[] changes =
[
new RegistryValueChange
{
KeyPath = "Software\\Classes\\regfile\\shell\\preview\\command",
KeyName = null,
Value = installationDir + "\\WinUI3Apps\\PowerToys.RegistryPreview.exe \"%1\"",
},
new RegistryValueChange
{
KeyPath = "Software\\Classes\\regfile\\shell\\preview",
KeyName = "icon",
Value = installationDir + "\\WinUI3Apps\\Assets\\RegistryPreview\\RegistryPreview.ico",
}
];
return new RegistryChangeSet() { Changes = changes };
}
}
}
}

View File

@@ -0,0 +1,52 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class WorkspacesModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleShortcutsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "Workspaces";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.Workspaces;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredWorkspacesEnabledValue();
public override string ProcessPath => "PowerToys.WorkspacesEditor.exe";
public override string ProcessName => "PowerToys.WorkspacesEditor";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SupressLaunchOnModuleEnabled | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public void Disable()
{
}
public void Enable()
{
InitializeShortcuts();
}
public void OnSettingsChanged()
{
InitializeShortcuts();
}
private void InitializeShortcuts()
{
Shortcuts.Clear();
Shortcuts.Add((SettingsUtils.Default.GetSettings<WorkspacesSettings>(Name).Properties.Hotkey, () =>
{
LaunchProcess();
}));
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
}
}

View File

@@ -0,0 +1,45 @@
// 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.Threading;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using PowerToys.Interop;
using RunnerV2.Models;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class ZoomItModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IPowerToysModuleCustomActionsProvider, IPowerToysModuleSettingsChangedSubscriber
{
public string Name => "ZoomIt";
public bool Enabled => SettingsUtils.Default.GetSettings<GeneralSettings>().Enabled.ZoomIt;
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredZoomItEnabledValue();
public override string ProcessPath => "PowerToys.ZoomIt.exe";
public override string ProcessName => "PowerToys.ZoomIt";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public void Disable()
{
}
public void Enable()
{
}
public Dictionary<string, Action> CustomActions { get => new() { { "refresh_settings", () => OnSettingsChanged() } }; }
public void OnSettingsChanged()
{
using var refreshSettingsEvent = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ZoomItRefreshSettingsEvent());
refreshSettingsEvent.Set();
}
}
}

View File

@@ -0,0 +1,246 @@
// 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.Drawing;
using System.Runtime.InteropServices;
using ManagedCommon;
using Windows.ApplicationModel;
namespace RunnerV2
{
internal static partial class NativeMethods
{
[LibraryImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool OpenProcessToken(IntPtr processHandle, uint desiredAccess, out IntPtr tokenHandle);
[LibraryImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetTokenInformation(IntPtr tokenHandle, TOKEN_INFORMATION_CLASS tokenInformationClass, ref TokenElevation tokenInformation, uint tokenInformationLength, out uint returnLength);
[LibraryImport("Kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool CloseHandle(IntPtr hObject);
internal enum TOKEN_INFORMATION_CLASS
{
TOKEN_ELEVATION = 20,
}
[StructLayout(LayoutKind.Sequential)]
internal struct TokenElevation
{
public uint TokenIsElevated;
}
internal const int TOKENQUERY = 0x0008;
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool UnregisterHotKey(IntPtr hWnd, int id);
[LibraryImport("user32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool AppendMenuW(IntPtr hMenu, uint uFlags, UIntPtr uIDNewItem, string lpNewItem);
[LibraryImport("user32.dll", SetLastError = true)]
internal static partial IntPtr CreatePopupMenu();
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetForegroundWindow(IntPtr hWnd);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool TrackPopupMenu(IntPtr hMenu, uint uFlags, int x, int y, int nReserved, IntPtr hWnd, IntPtr prcRect);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);
internal const uint NIMADD = 0x00000000;
internal const uint NIMDELETE = 0x00000002;
internal struct NOTIFYICONDATA
{
public uint CbSize;
public IntPtr HWnd;
public uint UId;
public uint UFlags;
public uint UCallbackMessage;
public IntPtr HIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string SzTip;
public uint DwState;
public uint DwStateMask;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string SzInfo;
public uint UTimeoutOrVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string SzInfoTitle;
public uint DwInfoFlags;
public Guid GuidItem;
public IntPtr HBalloonIcon;
}
[DllImport("shell32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Shell_NotifyIcon(uint dwMessage, ref NOTIFYICONDATA lpdata);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool ChangeWindowMessageFilterEx(IntPtr hWnd, uint msg, uint action, IntPtr pChangeFilterStruct);
internal const uint CSVREDRAW = 0x0001;
internal const uint CSHREDRAW = 0x0002;
internal const uint WSOVERLAPPEDWINDOW = 0x00CF0000;
internal const uint WSPOPUP = 0x80000000;
internal const int CWUSEDEFAULT = unchecked((int)0x80000000);
internal static readonly IntPtr IDCARROW = new(32512);
[DllImport("user32.dll")]
internal static extern IntPtr GetMessageW(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool TranslateMessage(ref MSG lpMsg);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DispatchMessageW(ref MSG lpMsg);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool PostMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[LibraryImport("kernel32.dll")]
internal static partial ushort AddAtomW([MarshalAs(UnmanagedType.LPWStr)] string lpString);
internal struct MSG
{
public IntPtr HWnd;
public uint Message;
public UIntPtr WParam;
public long LParam;
public ulong Time;
public Point Pt;
}
internal enum WindowMessages : uint
{
COMMAND = 0x0111,
HOTKEY = 0x0312,
ICON_NOTIFY = 0x0800,
WINDOWPOSCHANGING = 0x0046,
DESTROY = 0x0002,
REFRESH_SETTINGS = 0x0400 + 2,
}
[DllImport("user32.dll")]
internal static extern ushort RegisterClassW(ref WNDCLASS lpWndClass);
[LibraryImport("user32.dll", SetLastError = false)]
internal static partial IntPtr DefWindowProcW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[LibraryImport("user32.dll", SetLastError = true)]
internal static partial uint RegisterWindowMessageW([MarshalAs(UnmanagedType.LPWStr)] string lpString);
[LibraryImport("user32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
internal static partial nint CreateWindowExW(
uint dwExStyle,
string lpClassName,
string lpWindowName,
uint dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
internal delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct WNDCLASS
{
public uint Style;
public WndProc LpfnWndProc;
public int CbClsExtra;
public int CbWndExtra;
public IntPtr HInstance;
public IntPtr HIcon;
public IntPtr HCursor;
public IntPtr HbrBackground;
[MarshalAs(UnmanagedType.LPWStr)]
public string LpszMenuName;
[MarshalAs(UnmanagedType.LPWStr)]
public string LpszClassName;
}
[DllImport("Advapi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ConvertStringSecurityDescriptorToSecurityDescriptorW(
[MarshalAs(UnmanagedType.LPWStr)] string StringSecurityDescriptor,
uint StringSDRevision,
out IntPtr SecurityDescriptor,
out uint SecurityDescriptorSize);
[DllImport("Advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool MakeAbsoluteSD(
IntPtr pSelfRelativeSD,
IntPtr pAbsoluteSD,
ref uint lpdwAbsoluteSDSize,
IntPtr pDacl,
ref uint lpdwDaclSize,
IntPtr pSacl,
ref uint lpdwSaclSize,
IntPtr pOwner,
ref uint lpdwOwnerSize,
IntPtr pPrimaryGroup,
ref uint lpdwPrimaryGroupSize);
[DllImport("ole32.dll", SetLastError = true)]
internal static extern int CoInitializeSecurity(
IntPtr pSecDesc,
int cAuthSvc,
IntPtr asAuthSvc,
IntPtr pReserved1,
uint dwAuthnLevel,
uint dwImpLevel,
IntPtr pAuthList,
uint dwCapabilities,
IntPtr pReserved3);
[DllImport("user32.dll")]
internal static extern uint SendInput(uint nInputs, NativeKeyboardHelper.INPUT[] pInputs, int cbSize);
[DllImport("PowerToys.Interop.dll", CallingConvention = CallingConvention.Cdecl)]
internal static extern bool GetPackageNameAndVersionFromAppx(
[MarshalAs(UnmanagedType.LPWStr)] string appxPath,
[MarshalAs(UnmanagedType.LPWStr)] out string outName,
out PackageVersion outVersion);
[LibraryImport("Shell32.dll", SetLastError = true)]
internal static partial void SHChangeNotify(
uint wEventId,
uint uFlags,
IntPtr dwItem1,
IntPtr dwItem2);
internal const uint SHCNE_ASSOCCHANGED = 0x8000000;
internal const uint SHCNF_IDLIST = 0x0;
}
}

View File

@@ -0,0 +1,165 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapperProjection;
using RunnerV2;
using RunnerV2.Helpers;
using RunnerV2.Models;
using Settings.UI.Library;
internal sealed class Program
{
private static readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
internal static GeneralSettings GeneralSettings => _settingsUtils.GetSettings<GeneralSettings>();
private static void Main(string[] args)
{
Logger.InitializeLogger("\\RunnerLogs");
string securityDescriptor =
"O:BA" // Owner: Builtin (local) administrator
+ "G:BA" // Group: Builtin (local) administrator
+ "D:"
+ "(A;;0x7;;;PS)" // Access allowed on COM_RIGHTS_EXECUTE, _LOCAL, & _REMOTE for Personal self
+ "(A;;0x7;;;IU)" // Access allowed on COM_RIGHTS_EXECUTE for Interactive Users
+ "(A;;0x3;;;SY)" // Access allowed on COM_RIGHTS_EXECUTE, & _LOCAL for Local system
+ "(A;;0x7;;;BA)" // Access allowed on COM_RIGHTS_EXECUTE, _LOCAL, & _REMOTE for Builtin (local) administrator
+ "(A;;0x3;;;S-1-15-3-1310292540-1029022339-4008023048-2190398717-53961996-4257829345-603366646)" // Access allowed on COM_RIGHTS_EXECUTE, & _LOCAL for Win32WebViewHost package capability
+ "S:"
+ "(ML;;NX;;;LW)"; // Integrity label on No execute up for Low mandatory level
COMUtils.InitializeCOMSecurity(securityDescriptor);
switch (ShouldRunInSpecialMode(args))
{
case SpecialMode.None:
break;
case SpecialMode.UpdateNow:
UpdateNow();
return;
case SpecialMode.DisableCantDragElevatedNotification:
Environment.Exit(NotificationHelper.DisableToast(NotificationHelper.ToastType.ElevatedDontShowAgain) ? 1 : 0);
return;
case SpecialMode.CouldntToggleFileExplorerModulesNotification:
Environment.Exit(NotificationHelper.DisableToast(NotificationHelper.ToastType.CouldntToggleFileExplorerModules) ? 1 : 0);
return;
default:
throw new NotImplementedException("Special modes are not implemented yet.");
}
bool shouldOpenSettings = args.Any(s => s.StartsWith("--open-settings", StringComparison.InvariantCulture));
bool shouldOpenSettingsToSpecificPage = args.Any(s => s.StartsWith("--open-settings=", StringComparison.InvariantCulture));
// Check if PowerToys is already running
if (Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).Length > 1)
{
throw new NotImplementedException("Opening another instance window is not supported yet.");
}
/*
* Todo: Data diagnotics
*/
bool isElevated = ElevationHelper.IsProcessElevated();
bool hasDontElevateArgument = args.Contains("--dont-elevate");
bool runElevatedSetting = GeneralSettings.RunElevated;
bool hasRestartedElevatedArgment = args.Contains("--restartedElevated");
Action afterInitializationAction = () => { };
Version version = Assembly.GetExecutingAssembly().GetName().Version!;
if ($"v{version.Major}.{version.Minor}.{version.Build}" != _settingsUtils.GetSettings<LastVersionRunSettings>(fileName: "last_version_run.json").LastVersion && (!GeneralSettings.ShowWhatsNewAfterUpdates || GPOWrapper.GetDisableShowWhatsNewAfterUpdatesValue() != GpoRuleConfigured.Disabled))
{
afterInitializationAction += () =>
{
SettingsHelper.OpenSettingsWindow(showScoobeWindow: true);
};
}
if (!_settingsUtils.GetSettings<OOBESettings>(fileName: "oobe_settings.json").OpenedAtFirstLaunch)
{
afterInitializationAction += () =>
{
SettingsHelper.OpenSettingsWindow(showOobeWindow: true);
};
}
if (shouldOpenSettings)
{
afterInitializationAction += () =>
{
SettingsHelper.OpenSettingsWindow(additionalArguments: shouldOpenSettingsToSpecificPage ? args.First(s => s.StartsWith("--open-settings=", StringComparison.InvariantCulture)).Replace("--open-settings=", string.Empty, StringComparison.InvariantCulture) : null);
};
}
// Set last version run
_settingsUtils.SaveSettings(new LastVersionRunSettings() { LastVersion = $"v{version.Major}.{version.Minor}.{version.Build}" }.ToJsonString(), fileName: "last_version_run.json");
switch ((isElevated, hasDontElevateArgument, runElevatedSetting, hasRestartedElevatedArgment))
{
case (true, true, false, _):
// Todo: Scheudle restart as non elevated
throw new NotImplementedException();
case (true, _, _, _):
case (_, _, false, _):
case (_, true, _, _):
case (false, _, _, true):
GeneralSettings tempGeneralSettings = GeneralSettings;
tempGeneralSettings.IsElevated = isElevated;
_settingsUtils.SaveSettings(tempGeneralSettings.ToJsonString());
Runner.Run(afterInitializationAction);
break;
default:
ElevationHelper.RestartScheduled = ElevationHelper.RestartScheduledMode.RestartElevated;
break;
}
ElevationHelper.RestartIfScheudled();
}
/// <summary>
/// Returns whether the application should run in a special mode based on the provided arguments.
/// </summary>
/// <param name="args">The arguments passed to <see cref="Main(string[])"/></param>
/// <returns>The <see cref="SpecialMode"/> the app should run in.</returns>
private static SpecialMode ShouldRunInSpecialMode(string[] args)
{
if (args.Length > 0 && args[0].StartsWith("powertoys://", StringComparison.InvariantCultureIgnoreCase))
{
Uri uri = new(args[0]);
string host = uri.Host.ToLowerInvariant();
return host switch
{
"update_now" => SpecialMode.UpdateNow,
"cant_drag_elevated_disable" => SpecialMode.DisableCantDragElevatedNotification,
"couldnt_toggle_powerpreview_modules_disable" => SpecialMode.CouldntToggleFileExplorerModulesNotification,
_ => SpecialMode.None,
};
}
return SpecialMode.None;
}
/// <summary>
/// Starts the update process for PowerToys.
/// </summary>
private static void UpdateNow()
{
Process.Start(new ProcessStartInfo()
{
UseShellExecute = false,
CreateNoWindow = true,
FileName = "PowerToys.Update.exe",
Arguments = "-update_now",
});
}
}

View File

@@ -0,0 +1,313 @@
// 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.Frozen;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using ManagedCommon;
using RunnerV2.Helpers;
using RunnerV2.Models;
using RunnerV2.ModuleInterfaces;
using Update;
using static RunnerV2.NativeMethods;
namespace RunnerV2
{
internal static partial class Runner
{
/// <summary>
/// Gets the window handle for the Runner main window that hosts the tray icon and receives system messages.
/// </summary>
public static nint RunnerHwnd { get; private set; }
private const string TrayWindowClassName = "pt_tray_icon_window_class";
/// <summary>
/// Gets all the currently loaded modules.
/// </summary>
public static List<IPowerToysModule> LoadedModules { get; } = [];
private static List<IPowerToysModule> _failedModuleLoads = [];
/// <summary>
/// Gets the list of all available PowerToys modules.
/// </summary>
public static FrozenSet<IPowerToysModule> ModulesToLoad { get; } =
[
new ColorPickerModuleInterface(),
new AlwaysOnTopModuleInterface(),
new HostsModuleInterface(),
new PowerAccentModuleInterface(),
new AdvancedPasteModuleInterface(),
new AwakeModuleInterface(),
new CmdNotFoundModuleInterface(),
new CommandPaletteModuleInterface(),
new CropAndLockModuleInterface(),
new EnvironmentVariablesModuleInterface(),
new RegistryPreviewModuleInterface(),
new FileExplorerModuleInterface(),
new ZoomItModuleInterface(),
new PowerOCRModuleInterface(),
new MeasureToolModuleInterface(),
new MouseJumpModuleInterface(),
new FancyZonesModuleInterface(),
new PowerToysRunModuleInterface(),
new KeyboardManagerModuleInterface(),
new LightSwitchModuleInterface(),
new CursorWrapModuleInterface(),
new FindMyMouseModuleInterface(),
new WorkspacesModuleInterface(),
new MousePointerCrosshairsModuleInterface(),
new MouseHighlighterModuleInterface(),
];
/// <summary>
/// Runs the main message loop for Runner.
/// </summary>
/// <param name="afterInitializationAction">A function to execute after initialization.</param>
internal static void Run(Action afterInitializationAction)
{
Logger.LogInfo("Runner started");
InitializeTrayWindow();
TrayIconManager.StartTrayIcon();
Task.Run(UpdateUtilities.UninstallPreviousMsixVersions);
foreach (IPowerToysModule module in ModulesToLoad)
{
ToggleModuleStateBasedOnEnabledProperty(module);
}
CentralizedKeyboardHookManager.Start();
afterInitializationAction();
MessageLoop();
}
private static readonly uint _taskbarCreatedMessage = RegisterWindowMessageW("TaskbarCreated");
/// <summary>
/// The main message loop that processes Windows messages.
/// </summary>
[STAThread]
private static void MessageLoop()
{
while (GetMessageW(out MSG msg, IntPtr.Zero, 0, 0) != 0 || true)
{
TranslateMessage(ref msg);
DispatchMessageW(ref msg);
// Supress duplicate handling of HOTKEY messages
if (msg.Message == (uint)WindowMessages.HOTKEY)
{
continue;
}
HandleMessage(msg.HWnd, msg.Message, (nint)msg.WParam, (nint)msg.LParam);
}
Close();
}
/// <summary>
/// Closes Runner and all loaded modules.
/// </summary>
[DoesNotReturn]
internal static void Close()
{
TrayIconManager.StopTrayIcon();
SettingsHelper.CloseSettingsWindow();
ElevationHelper.RestartIfScheudled();
foreach (IPowerToysModule module in LoadedModules)
{
try
{
module.Disable();
if (module is ProcessModuleAbstractClass pmac)
{
pmac.ProcessExit();
}
}
catch (Exception e)
{
MessageBox.Show($"The module {module.Name} failed to unload: \n" + e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
Environment.Exit(0);
}
/// <summary>
/// Toggles the state of a module based on its enabled property and GPO rules.
/// </summary>
/// <param name="module">The module to toggle</param>
public static void ToggleModuleStateBasedOnEnabledProperty(IPowerToysModule module)
{
if (_failedModuleLoads.Contains(module))
{
return;
}
try
{
if ((module.Enabled && (module.GpoRuleConfigured != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)) || module.GpoRuleConfigured == PowerToys.GPOWrapper.GpoRuleConfigured.Enabled)
{
/* Todo: conflict manager */
if (!LoadedModules.Contains(module))
{
module.Enable();
if (module is ProcessModuleAbstractClass pmac)
{
pmac.LaunchProcess(true);
}
LoadedModules.Add(module);
}
CentralizedKeyboardHookManager.RemoveAllHooksFromModule(module.Name);
if (module is IPowerToysModuleShortcutsProvider shortcutsProvider)
{
foreach (var shortcut in shortcutsProvider.Shortcuts.ToArray())
{
CentralizedKeyboardHookManager.AddKeyboardHook(module.Name, shortcut.Hotkey, shortcut.Action);
}
}
return;
}
}
catch (IOException)
{
return;
}
catch (Exception e)
{
MessageBox.Show($"The module {module.Name} failed to load: \n" + e.Message, "Error: " + e.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error);
_failedModuleLoads.Add(module);
return;
}
try
{
module.Disable();
if (module is ProcessModuleAbstractClass pmac)
{
pmac.ProcessExit();
}
CentralizedKeyboardHookManager.RemoveAllHooksFromModule(module.Name);
LoadedModules.Remove(module);
}
catch (IOException)
{
}
catch (Exception e)
{
MessageBox.Show($"The module {module.Name} failed to unload: \n" + e.Message, "Error: " + e.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error);
_failedModuleLoads.Add(module);
}
}
/// <summary>
/// Initializes the tray window to receive system messages.
/// </summary>
[STAThread]
private static void InitializeTrayWindow()
{
IntPtr hInstance = Process.GetCurrentProcess().MainModule!.BaseAddress;
IntPtr hCursor = Cursors.Arrow.Handle;
IntPtr hIcon = SystemIcons.Application.Handle;
var wc = new WNDCLASS
{
HCursor = hCursor,
HInstance = hInstance,
LpszClassName = TrayWindowClassName,
Style = CSHREDRAW | CSVREDRAW,
LpfnWndProc = HandleMessage,
HIcon = hIcon,
HbrBackground = IntPtr.Zero,
LpszMenuName = string.Empty,
CbClsExtra = 0,
CbWndExtra = 0,
};
_ = RegisterClassW(ref wc);
RunnerHwnd = CreateWindowExW(
0,
wc.LpszClassName,
TrayWindowClassName,
WSOVERLAPPEDWINDOW | WSPOPUP,
CWUSEDEFAULT,
CWUSEDEFAULT,
CWUSEDEFAULT,
CWUSEDEFAULT,
IntPtr.Zero,
IntPtr.Zero,
wc.HInstance,
IntPtr.Zero);
if (RunnerHwnd == IntPtr.Zero)
{
var err = Marshal.GetLastPInvokeError();
MessageBox.Show($"CreateWindowExW failed. LastError={err}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Handles Windows messages sent to the tray window.
/// </summary>
private static IntPtr HandleMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
switch (msg)
{
case (uint)WindowMessages.ICON_NOTIFY:
TrayIconManager.ProcessTrayIconMessage(lParam);
break;
case (uint)WindowMessages.COMMAND:
TrayIconManager.ProcessTrayMenuCommand((nuint)wParam);
break;
case (uint)WindowMessages.WINDOWPOSCHANGING:
TrayIconManager.StartTrayIcon();
break;
case (uint)WindowMessages.DESTROY:
Close();
break;
case (uint)WindowMessages.REFRESH_SETTINGS:
foreach (IPowerToysModule module in ModulesToLoad)
{
ToggleModuleStateBasedOnEnabledProperty(module);
}
break;
default:
if (msg == _taskbarCreatedMessage)
{
TrayIconManager.StartTrayIcon();
}
break;
}
return DefWindowProcW(hWnd, msg, wParam, lParam);
}
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<Description>PowerToys Runner</Description>
<AssemblyName>PowerToys</AssemblyName>
<OutputPath>..\..\..\$(Platform)\$(Configuration)</OutputPath>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<ApplicationIcon>icon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
<ProjectReference Include="..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\modules\poweraccent\PowerAccent.UI\PowerAccent.UI.csproj" />
<ProjectReference Include="..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\..\Update\Update.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
<?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="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,36 +0,0 @@
#include <windows.h>
#include "resource.h"
#include "../common/version/version.h"
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@@ -1,234 +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.
#define WIN32_LEAN_AND_MEAN
#include "Generated Files/resource.h"
#include <Windows.h>
#include <shellapi.h>
#include <filesystem>
#include <string_view>
#include <common/updating/updating.h>
#include <common/updating/updateState.h>
#include <common/updating/installer.h>
#include <common/utils/elevation.h>
#include <common/utils/HttpClient.h>
#include <common/utils/process_path.h>
#include <common/utils/resources.h>
#include <common/utils/timeutil.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/logger/logger.h>
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.Storage.h>
#include <Msi.h>
#include "../runner/tray_icon.h"
#include "../runner/UpdateUtils.h"
using namespace cmdArg;
namespace fs = std::filesystem;
std::optional<fs::path> CopySelfToTempDir()
{
std::error_code error;
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
if (error)
{
return std::nullopt;
}
return std::move(dst_path);
}
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
{
using namespace updating;
isUpToDate = false;
auto state = UpdateState::read();
const auto new_version_info = get_github_version_info_async().get();
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = download_new_version(std::get<new_version_download_info>(*new_version_info)).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
else if (state.state == UpdateState::readyToInstall)
{
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
if (fs::is_regular_file(installer))
{
return std::move(installer);
}
else
{
Logger::error(L"Couldn't find a downloaded installer {}", installer.native());
return std::nullopt;
}
}
else if (state.state == UpdateState::upToDate)
{
isUpToDate = true;
return std::nullopt;
}
Logger::error("Invoked with -update_now argument, but update state was invalid");
return std::nullopt;
}
bool InstallNewVersionStage1(fs::path installer)
{
if (auto copy_in_temp = CopySelfToTempDir())
{
// Detect if PT was running
const auto pt_main_window = FindWindowW(pt_tray_icon_window_class, nullptr);
if (pt_main_window != nullptr)
{
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
}
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
arguments += L" \"";
arguments += installer.c_str();
arguments += L"\"";
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = copy_in_temp->c_str();
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = arguments.c_str();
return ShellExecuteExW(&sei) == TRUE;
}
else
{
return false;
}
}
bool InstallNewVersionStage2(std::wstring installer_path)
{
std::transform(begin(installer_path), end(installer_path), begin(installer_path), ::towlower);
bool success = true;
if (installer_path.ends_with(L".msi"))
{
success = MsiInstallProductW(installer_path.data(), nullptr) == ERROR_SUCCESS;
}
else
{
// If it's not .msi, then it's a wix bootstrapper
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE };
sei.lpFile = installer_path.c_str();
sei.nShow = SW_SHOWNORMAL;
std::wstring parameters = L"/passive /norestart";
sei.lpParameters = parameters.c_str();
success = ShellExecuteExW(&sei) == TRUE;
// Wait for the install completion
if (success)
{
WaitForSingleObject(sei.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(sei.hProcess, &exitCode);
success = exitCode == 0;
CloseHandle(sei.hProcess);
}
}
if (!success)
{
return false;
}
UpdateState::store([&](UpdateState& state) {
state = {};
state.githubUpdateLastCheckedDate.emplace(timeutil::now());
state.state = UpdateState::upToDate;
});
return true;
}
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
int nArgs = 0;
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
if (!args || nArgs < 2)
{
return 1;
}
std::wstring_view action{ args[1] };
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
logFilePath.append(LogSettings::updateLogPath);
Logger::init(LogSettings::updateLoggerName, logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location());
if (action == UPDATE_NOW_LAUNCH_STAGE1)
{
bool isUpToDate = false;
auto installerPath = ObtainInstaller(isUpToDate);
bool failed = !installerPath.has_value();
failed = failed || !InstallNewVersionStage1(std::move(*installerPath));
if (failed)
{
UpdateState::store([&](UpdateState& state) {
state = {};
state.githubUpdateLastCheckedDate.emplace(timeutil::now());
state.state = isUpToDate ? UpdateState::upToDate : UpdateState::errorDownloading;
});
}
return failed;
}
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
{
using namespace std::string_view_literals;
const bool failed = !InstallNewVersionStage2(args[2]);
if (failed)
{
UpdateState::store([&](UpdateState& state) {
state = {};
state.githubUpdateLastCheckedDate.emplace(timeutil::now());
state.state = UpdateState::errorDownloading;
});
}
return failed;
}
return 0;
}

View File

@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h PowerToys.Update.base.rc PowerToys.Update.rc" />
</Target>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{44CE9AE1-4390-42C5-BACC-0FD6B40AA203}</ProjectGuid>
<RootNamespace>Update</RootNamespace>
<ProjectName>PowerToys.Update</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<Import Project="..\..\deps\expected.props" />
<PropertyGroup>
<ConfigurationType>Application</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup>
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>../;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>WindowsApp.lib;Msi.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="PowerToys.Update.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\common\notifications\notifications.vcxproj">
<Project>{1d5be09d-78c0-4fd7-af00-ae7c1af7c525}</Project>
</ProjectReference>
<ProjectReference Include="..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\common\updating\updating.vcxproj">
<Project>{17da04df-e393-4397-9cf0-84dabe11032e}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<None Include="PowerToys.Update.base.rc" />
<ResourceCompile Include="Generated Files\PowerToys.Update.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

183
src/Update/Program.cs Normal file
View File

@@ -0,0 +1,183 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Update;
[SupportedOSPlatform("windows")]
internal sealed partial class Program
{
private static readonly string _installerPath = Path.Combine(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft",
"PowerToys",
"Updates"));
private static async Task Main(string[] args)
{
if (args.Length < 1)
{
Environment.Exit(1);
return;
}
string action = args[0];
switch (action)
{
case UpdateStage.UPDATENOWLAUNCHSTAGE1:
await PerformUpdateNowStage1();
break;
case UpdateStage.UPDATENOWLAUNCHSTAGE2:
if (args.Length < 2)
{
Environment.Exit(1);
}
await PerformUpdateNowStage2(args[1]);
break;
default:
break;
}
}
private static async Task PerformUpdateNowStage2(string installerPath)
{
Process installerProcess = new()
{
StartInfo = new ProcessStartInfo
{
FileName = installerPath,
Arguments = "/passive /norestart",
UseShellExecute = true,
},
};
installerProcess.Start();
await installerProcess.WaitForExitAsync();
if (installerProcess.ExitCode == 0)
{
UpdateSettingsHelper.ProcessNoUpdateAvailable();
}
else
{
UpdateSettingsHelper.SetUpdateState(UpdatingSettings.UpdatingState.ErrorDownloading);
}
}
private static async Task PerformUpdateNowStage1()
{
UpdateSettingsHelper.TriggerUpdateCheck();
UpdateSettingsHelper.UpdateInfo updateInfo = await UpdateSettingsHelper.GetUpdateAvailableInfo();
if (updateInfo is not UpdateSettingsHelper.UpdateInfo.UpdateAvailable ua)
{
// No update found
Environment.Exit(1);
return;
}
// Copy itsself to the temp folder
File.Copy("PowerToys.Update.exe", Path.Combine(Path.GetTempPath(), "PowerToys.Update.exe"), true);
string? installerFilePath = null;
switch (UpdateSettingsHelper.GetUpdateState())
{
case UpdatingSettings.UpdatingState.ReadyToDownload:
case UpdatingSettings.UpdatingState.ErrorDownloading:
CleanupUpdates();
installerFilePath = await DownloadFile(ua.InstallerDownloadUrl.ToString(), ua.InstallerFilename);
break;
case UpdatingSettings.UpdatingState.ReadyToInstall:
installerFilePath = Path.Combine(_installerPath, ua.InstallerFilename);
if (!File.Exists(installerFilePath))
{
// Installer not found
Environment.Exit(1);
return;
}
break;
case UpdatingSettings.UpdatingState.UpToDate:
Environment.Exit(0);
return;
}
if (installerFilePath == null)
{
UpdateSettingsHelper.SetUpdateState(UpdatingSettings.UpdatingState.ErrorDownloading);
Environment.Exit(1);
return;
}
IntPtr runnerHwnd = FindWindowW("pt_tray_icon_window_class");
if (runnerHwnd != IntPtr.Zero)
{
SendMessageW(runnerHwnd, 0x0010, IntPtr.Zero, IntPtr.Zero); // Send WM_CLOSE
}
string arguments = $"{UpdateStage.UPDATENOWLAUNCHSTAGE2} \"{installerFilePath}\"";
Process.Start(new ProcessStartInfo
{
FileName = Path.Combine(Path.GetTempPath(), "PowerToys.Update.exe"),
Arguments = arguments,
UseShellExecute = true,
CreateNoWindow = true,
WorkingDirectory = Environment.CurrentDirectory,
});
}
private static async Task<string?> DownloadFile(string downloadUri, string downloadFileName)
{
HttpClient httpClient = new();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("PowerToys Runner"); // GitHub API requires a user-agent
// 3 Attempts to download the file
for (int i = 0; i < 3; i++)
{
try
{
using FileStream fileStream = new(Path.Combine(_installerPath, downloadFileName), FileMode.Create, FileAccess.Write, FileShare.None);
await (await httpClient.GetStreamAsync(downloadUri)).CopyToAsync(fileStream);
return fileStream.Name;
}
catch
{
}
}
return null;
}
private static void CleanupUpdates()
{
if (!Path.Exists(_installerPath))
{
return;
}
foreach (string file in Directory.GetFiles(_installerPath).Where(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)))
{
File.Delete(file);
}
}
[LibraryImport("user32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
private static partial IntPtr FindWindowW(string lpClassName);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SendMessageW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
}

View File

@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DOTNET_CORE_DOWNLOAD_FAILURE" xml:space="preserve">
<value>Couldn't download .NET Core Desktop Runtime 3.1, please install it manually.</value>
</data>
<data name="DOTNET_CORE_DOWNLOAD_FAILURE_TITLE" xml:space="preserve">
<value>PowerToys installation error</value>
</data>
<data name="GITHUB_NEW_VERSION_AVAILABLE" xml:space="preserve">
<value>An update to PowerToys is available.</value>
</data>
<data name="GITHUB_NEW_VERSION_UPDATE_NOW" xml:space="preserve">
<value>Update now</value>
</data>
<data name="GITHUB_NEW_VERSION_AVAILABLE_OFFER_VISIT" xml:space="preserve">
<value>An update to PowerToys is available. Visit our GitHub page to update.</value>
</data>
<data name="GITHUB_NEW_VERSION_MORE_INFO" xml:space="preserve">
<value>More info...</value>
</data>
<data name="TOAST_TITLE" xml:space="preserve">
<value>PowerToys Update</value>
</data>
</root>

22
src/Update/Update.csproj Normal file
View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<Description>PowerToys Runner</Description>
<AssemblyName>PowerToys.Update</AssemblyName>
<OutputPath>..\..\$(Platform)\$(Configuration)</OutputPath>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<PublishAot>true</PublishAot>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
<PropertyGroup>
<_IsPublishing Condition="'$(_IsPublishing)'==''">false</_IsPublishing>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(_IsPublishing)'!='true'">
<Exec Command="dotnet publish &quot;$(ProjectPath)&quot; -c $(Configuration) -r $(RuntimeIdentifier) --self-contained -o &quot;$(OutputPath)&quot;" />
</Target>
</Project>

View File

@@ -0,0 +1,227 @@
// 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.IO;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32;
namespace Update
{
[SupportedOSPlatform("windows")]
public static class UpdateSettingsHelper
{
private static Thread? _updateThread;
private const string INSTALLERFILENAME = "powertoyssetup";
private const string USERINSTALLERFILENAME = "powertoysusersetup";
public static void TriggerUpdateCheck()
{
if (_updateThread is not null && _updateThread.IsAlive)
{
return;
}
_updateThread = new Thread(async () =>
{
UpdateInfo updateInfo = await GetUpdateAvailableInfo();
switch (updateInfo)
{
case UpdateInfo.UpdateCheckFailed ucf:
ProcessUpdateCheckFailed(ucf);
break;
case UpdateInfo.UpdateAvailable ua:
ProcessUpdateAvailable(ua);
break;
case UpdateInfo.NoUpdateAvailable:
ProcessNoUpdateAvailable();
break;
}
});
_updateThread.Start();
}
internal record UpdateInfo
{
private UpdateInfo()
{
}
public sealed record NoUpdateAvailable : UpdateInfo;
public sealed record UpdateAvailable(Uri ReleasePageUri, Version AvailableVersion, Uri InstallerDownloadUrl, string InstallerFilename) : UpdateInfo;
public sealed record UpdateCheckFailed(Exception Exception) : UpdateInfo;
}
internal static async Task<UpdateInfo> GetUpdateAvailableInfo()
{
Version? currentVersion = Assembly.GetExecutingAssembly().GetName().Version;
if (currentVersion is null)
{
// Todo: Log
return new UpdateInfo.NoUpdateAvailable();
}
if (currentVersion is { Major: 0, Minor: 0 })
{
// Pre-release or local build, skip update check
return new UpdateInfo.NoUpdateAvailable();
}
try
{
HttpClient httpClient = new();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("PowerToys Runner"); // GitHub API requires a user-agent
Stream body = await httpClient.GetStreamAsync("https://api.github.com/repos/microsoft/PowerToys/releases/latest").ConfigureAwait(false);
JsonElement releaseObject = (await JsonDocument.ParseAsync(body)).RootElement;
Version latestVersion = new(releaseObject.GetProperty("tag_name").GetString()?.TrimStart('V', 'v') ?? throw new FormatException("The \"tag_name\" field could not be found"));
string architectureString = RuntimeInformation.OSArchitecture switch
{
Architecture.X64 => "x64",
Architecture.Arm64 => "arm64",
_ => throw new InvalidDataException("Unknown architecture"),
};
if (latestVersion > currentVersion)
{
Uri releasePageUri = new(releaseObject.GetProperty("html_url").GetString() ?? throw new FormatException("The \"html_url\" field could not be found"));
string requiredFilename = GetInstallScope() == InstallScope.PerMachine ? INSTALLERFILENAME : USERINSTALLERFILENAME;
Uri? installerDownloadUrl = null;
string? installerFilename = null;
foreach (JsonElement asset in releaseObject.GetProperty("assets").EnumerateArray())
{
string? name = asset.GetProperty("name").GetString();
string? browserDownloadUrl = asset.GetProperty("browser_download_url").GetString();
if (name is null
|| browserDownloadUrl is null
|| !name.Contains(requiredFilename, StringComparison.InvariantCultureIgnoreCase)
|| !name.Contains(".exe", StringComparison.InvariantCultureIgnoreCase)
|| !name.Contains(architectureString, StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
installerDownloadUrl = new Uri(browserDownloadUrl);
installerFilename = name;
break;
}
return installerDownloadUrl is null || installerFilename is null
? new UpdateInfo.UpdateCheckFailed(new InvalidDataException("No installer found in GitHub release"))
: new UpdateInfo.UpdateAvailable(releasePageUri, latestVersion, installerDownloadUrl, installerFilename);
}
return new UpdateInfo.NoUpdateAvailable();
}
catch (Exception e)
{
return new UpdateInfo.UpdateCheckFailed(e);
}
}
private enum InstallScope
{
PerMachine,
PerUser,
}
[SupportedOSPlatform("windows")]
private static InstallScope GetInstallScope()
{
if (Registry.LocalMachine.OpenSubKey(@"Software\Classes\powertoys\", false) is not RegistryKey machineKey)
{
if (Registry.CurrentUser.OpenSubKey(@"Software\Classes\powertoys\", false) is not RegistryKey userKey)
{
// Both keys are missing
return InstallScope.PerMachine;
}
if (userKey.GetValue("InstallScope") is not string installScope)
{
userKey.Close();
return InstallScope.PerMachine;
}
userKey.Close();
return installScope.Contains("perUser") ? InstallScope.PerUser : InstallScope.PerMachine;
}
machineKey.Close();
return InstallScope.PerMachine;
}
private static readonly string _settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft",
"PowerToys");
private static readonly string _updatingSettingsFile = Path.Combine(_settingsPath, "UpdateState.json");
private static void ProcessUpdateAvailable(UpdateInfo.UpdateAvailable updateAvailable)
{
UpdatingSettings updatingSettings = UpdatingSettings.LoadSettings();
Console.WriteLine($"Update available: {updateAvailable.AvailableVersion}");
updatingSettings.State = UpdatingSettings.UpdatingState.ReadyToDownload;
updatingSettings.ReleasePageLink = updateAvailable.ReleasePageUri.ToString();
updatingSettings.DownloadedInstallerFilename = updateAvailable.InstallerFilename;
updatingSettings.ReleasePageLink = updateAvailable.ReleasePageUri.ToString();
updatingSettings.LastCheckedDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);
File.WriteAllText(_updatingSettingsFile, updatingSettings.ToJsonString());
}
internal static void ProcessNoUpdateAvailable()
{
UpdatingSettings updatingSettings = UpdatingSettings.LoadSettings();
updatingSettings.State = UpdatingSettings.UpdatingState.UpToDate;
updatingSettings.ReleasePageLink = string.Empty;
updatingSettings.DownloadedInstallerFilename = string.Empty;
updatingSettings.ReleasePageLink = string.Empty;
updatingSettings.LastCheckedDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);
File.WriteAllText(_updatingSettingsFile, updatingSettings.ToJsonString());
}
private static void ProcessUpdateCheckFailed(UpdateInfo.UpdateCheckFailed updateCheckFailed)
{
// Todo: Log failed attempt
UpdatingSettings updatingSettings = UpdatingSettings.LoadSettings();
updatingSettings.State = UpdatingSettings.UpdatingState.NetworkError;
updatingSettings.ReleasePageLink = string.Empty;
updatingSettings.DownloadedInstallerFilename = string.Empty;
updatingSettings.ReleasePageLink = string.Empty;
updatingSettings.LastCheckedDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);
File.WriteAllText(_updatingSettingsFile, updatingSettings.ToJsonString());
}
internal static void SetUpdateState(UpdatingSettings.UpdatingState state)
{
UpdatingSettings updatingSettings = UpdatingSettings.LoadSettings();
updatingSettings.State = state;
File.WriteAllText(_updatingSettingsFile, updatingSettings.ToJsonString());
}
internal static UpdatingSettings.UpdatingState GetUpdateState() => UpdatingSettings.LoadSettings().State;
}
}

16
src/Update/UpdateStage.cs Normal file
View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text;
namespace Update
{
internal static class UpdateStage
{
internal const string UPDATENOWLAUNCHSTAGE1 = "-update_now";
internal const string UPDATENOWLAUNCHSTAGE2 = "-update_now_stage_2";
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Update
{
public static class UpdateUtilities
{
public static async void UninstallPreviousMsixVersions()
{
try
{
Windows.Management.Deployment.PackageManager packageManager = new();
var packages = packageManager.FindPackagesForUser(string.Empty, "Microsoft.PowerToys", "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US");
Version? currentVersion = Assembly.GetExecutingAssembly().GetName().Version;
if (currentVersion == null)
{
return;
}
foreach (var package in packages)
{
Version msixVersion = new Version(package.Id.Version.Major, package.Id.Version.Minor, package.Id.Version.Revision);
if (msixVersion < currentVersion)
{
await packageManager.RemovePackageAsync(package.Id.FullName);
}
}
}
catch
{
}
}
}
}

View File

@@ -0,0 +1,124 @@
// 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.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Update
{
public sealed class UpdatingSettings
{
public enum UpdatingState
{
UpToDate = 0,
ErrorDownloading,
ReadyToDownload,
ReadyToInstall,
NetworkError,
}
// Gets or sets a value of the updating state
[JsonPropertyName("state")]
public UpdatingState State { get; set; }
// Gets or sets a value of the release page url
[JsonPropertyName("releasePageUrl")]
public string ReleasePageLink { get; set; } = string.Empty;
// Gets or sets a value of the github last checked date
[JsonPropertyName("githubUpdateLastCheckedDate")]
public string LastCheckedDate { get; set; } = string.Empty;
// Gets or sets a value of the updating state
[JsonPropertyName("downloadedInstallerFilename")]
public string DownloadedInstallerFilename { get; set; } = string.Empty;
// Non-localizable strings: Files
public const string SettingsFilePath = "\\Microsoft\\PowerToys\\";
public const string SettingsFile = "UpdateState.json";
public string NewVersion
{
get
{
if (ReleasePageLink == null)
{
return string.Empty;
}
try
{
string version = ReleasePageLink.Substring(ReleasePageLink.LastIndexOf('/') + 1);
return version.Trim();
}
catch (Exception)
{
}
return string.Empty;
}
}
public string LastCheckedDateLocalized
{
get
{
try
{
if (LastCheckedDate == null)
{
return string.Empty;
}
long seconds = long.Parse(LastCheckedDate, CultureInfo.CurrentCulture);
var date = DateTimeOffset.FromUnixTimeSeconds(seconds).UtcDateTime;
return date.ToLocalTime().ToString(CultureInfo.CurrentCulture);
}
catch (Exception)
{
}
return string.Empty;
}
}
public UpdatingSettings()
{
State = UpdatingState.UpToDate;
}
public static UpdatingSettings LoadSettings()
{
var localAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var file = localAppDataDir + SettingsFilePath + SettingsFile;
if (File.Exists(file))
{
try
{
FileStream inputStream = File.Open(file, FileMode.Open);
StreamReader reader = new(inputStream);
string data = reader.ReadToEnd();
inputStream.Close();
reader.Dispose();
return JsonSerializer.Deserialize(data, UpdatingsSettingsSourceGenerationContext.Default.UpdatingSettings)!;
}
catch (Exception)
{
}
}
return new UpdatingSettings();
}
public string ToJsonString()
{
return JsonSerializer.Serialize(this, UpdatingsSettingsSourceGenerationContext.Default.UpdatingSettings);
}
}
}

View File

@@ -0,0 +1,22 @@
// 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.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Tasks;
namespace Update
{
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(UpdatingSettings))]
internal sealed partial class UpdatingsSettingsSourceGenerationContext : JsonSerializerContext
{
}
}

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@@ -1,11 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by PowerToys.Update.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys Update"
#define INTERNAL_NAME "PowerToys.Update"
#define ORIGINAL_FILENAME "PowerToys.Update.exe"

View File

@@ -1,85 +0,0 @@
#include "comUtils.h"
#include <Windows.h>
#pragma warning(push)
#pragma warning(disable : 4067)
#include <Sddl.h>
#pragma warning(pop)
#include <memory>
#include <wil/resource.h>
// Helper class for various COM-related APIs, e.g working with security descriptors
template<typename T>
struct typed_storage
{
std::unique_ptr<char[]> _buffer;
inline explicit typed_storage(const DWORD size) :
_buffer{ std::make_unique<char[]>(size) }
{
}
inline operator T*()
{
return reinterpret_cast<T*>(_buffer.get());
}
};
bool initializeCOMSecurity(const wchar_t* securityDescriptor)
{
PSECURITY_DESCRIPTOR self_relative_sd{};
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(securityDescriptor, SDDL_REVISION_1, &self_relative_sd, nullptr))
{
return false;
}
auto free_relative_sd = wil::scope_exit([&] {
LocalFree(self_relative_sd);
});
DWORD absolute_sd_size = 0;
DWORD dacl_size = 0;
DWORD group_size = 0;
DWORD owner_size = 0;
DWORD sacl_size = 0;
if (!MakeAbsoluteSD(self_relative_sd, nullptr, &absolute_sd_size, nullptr, &dacl_size, nullptr, &sacl_size, nullptr, &owner_size, nullptr, &group_size))
{
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
{
return false;
}
}
typed_storage<SECURITY_DESCRIPTOR> absolute_sd{ absolute_sd_size };
typed_storage<ACL> dacl{ dacl_size };
typed_storage<ACL> sacl{ sacl_size };
typed_storage<SID> owner{ owner_size };
typed_storage<SID> group{ group_size };
if (!MakeAbsoluteSD(self_relative_sd,
absolute_sd,
&absolute_sd_size,
dacl,
&dacl_size,
sacl,
&sacl_size,
owner,
&owner_size,
group,
&group_size))
{
return false;
}
return !FAILED(CoInitializeSecurity(
absolute_sd,
-1,
nullptr,
nullptr,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IDENTIFY,
nullptr,
EOAC_DYNAMIC_CLOAKING | EOAC_DISABLE_AAA,
nullptr));
}

View File

@@ -1,3 +0,0 @@
#pragma once
bool initializeCOMSecurity(const wchar_t* securityDescriptor);

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