mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-21 11:37:20 +01:00
Compare commits
36 Commits
shawn/fixQ
...
user/yeela
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30bbcf32a4 | ||
|
|
5422bc31bb | ||
|
|
27dcd1e5bc | ||
|
|
2a0d0a1210 | ||
|
|
662bbf0033 | ||
|
|
b7a94eb48d | ||
|
|
b5e9f346da | ||
|
|
8e2123cfea | ||
|
|
a9205781d7 | ||
|
|
086e4b5676 | ||
|
|
089c5f8b50 | ||
|
|
113639a66c | ||
|
|
3c2cb4516a | ||
|
|
53c5e66cce | ||
|
|
4cde968c9b | ||
|
|
e148a89288 | ||
|
|
1dddf9fa2c | ||
|
|
0d59b9f790 | ||
|
|
e314485e85 | ||
|
|
f48c4a9a6f | ||
|
|
175403d86d | ||
|
|
f7c57b05d7 | ||
|
|
5098809e14 | ||
|
|
c8da70d6fa | ||
|
|
22ce3b81ec | ||
|
|
74448355f9 | ||
|
|
031e365f57 | ||
|
|
569b4eed62 | ||
|
|
b68b84532c | ||
|
|
8b79da5d49 | ||
|
|
ab28777514 | ||
|
|
febaec0741 | ||
|
|
c88fe1fa0e | ||
|
|
fd88fa18d4 | ||
|
|
a6b8cea7cd | ||
|
|
5f61057b38 |
12
.github/actions/spell-check/expect.txt
vendored
12
.github/actions/spell-check/expect.txt
vendored
@@ -32,6 +32,7 @@ advfirewall
|
||||
AFeature
|
||||
affordances
|
||||
AFX
|
||||
agentskills
|
||||
AGGREGATABLE
|
||||
AHK
|
||||
AHybrid
|
||||
@@ -64,6 +65,9 @@ apidl
|
||||
APIENTRY
|
||||
APIIs
|
||||
Apm
|
||||
APMPOWERSTATUSCHANGE
|
||||
APMRESUMEAUTOMATIC
|
||||
APMRESUMESUSPEND
|
||||
APPBARDATA
|
||||
APPEXECLINK
|
||||
appext
|
||||
@@ -206,14 +210,17 @@ certmgr
|
||||
cfp
|
||||
CHANGECBCHAIN
|
||||
changecursor
|
||||
checkmarks
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
cim
|
||||
CImage
|
||||
cla
|
||||
claude
|
||||
CLASSDC
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
@@ -1035,6 +1042,7 @@ mmi
|
||||
mmsys
|
||||
mobileredirect
|
||||
mockapi
|
||||
modelcontextprotocol
|
||||
MODALFRAME
|
||||
MODESPRUNED
|
||||
MONITORENUMPROC
|
||||
@@ -1343,6 +1351,7 @@ Pomodoro
|
||||
Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
POWERBROADCAST
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1731,6 +1740,7 @@ STICKYKEYS
|
||||
sticpl
|
||||
storelogo
|
||||
stprintf
|
||||
streamable
|
||||
streamjsonrpc
|
||||
STRINGIZE
|
||||
stringtable
|
||||
@@ -1750,6 +1760,7 @@ SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
sut
|
||||
swe
|
||||
svchost
|
||||
SVGIn
|
||||
SVGIO
|
||||
@@ -1956,6 +1967,7 @@ visualeffects
|
||||
vkey
|
||||
vmovl
|
||||
VMs
|
||||
vnd
|
||||
vorrq
|
||||
VOS
|
||||
vpaddlq
|
||||
|
||||
92
.github/agents/FixIssue.agent.md
vendored
Normal file
92
.github/agents/FixIssue.agent.md
vendored
Normal 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
65
.github/agents/PlanIssue.agent.md
vendored
Normal 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.
|
||||
9
.github/copilot-instructions.md
vendored
9
.github/copilot-instructions.md
vendored
@@ -6,15 +6,8 @@ description: 'PowerToys AI contributor guidance'
|
||||
|
||||
Concise guidance for AI contributions. For complete details, see [AGENTS.md](../AGENTS.md).
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Build**: `tools\build\build-essentials.cmd` (first time), then `tools\build\build.cmd`
|
||||
- **Tests**: Find `<Product>*UnitTests` project, build it, run via VS Test Explorer
|
||||
- **Exit code 0 = success** – do not proceed if build fails
|
||||
|
||||
## Key Rules
|
||||
|
||||
- One terminal per operation (build → test)
|
||||
- Atomic PRs: one logical change, no drive-by refactors
|
||||
- Add tests when changing behavior
|
||||
- Keep hot paths quiet (no logging in hooks/tight loops)
|
||||
@@ -39,7 +32,5 @@ These are auto-applied based on file location:
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
- [AGENTS.md](../AGENTS.md) – Full contributor guide
|
||||
- [Build Guidelines](../tools/build/BUILD-GUIDELINES.md)
|
||||
- [Architecture](../doc/devdocs/core/architecture.md)
|
||||
- [Coding Style](../doc/devdocs/development/style.md)
|
||||
|
||||
261
.github/instructions/agent-skills.instructions.md
vendored
Normal file
261
.github/instructions/agent-skills.instructions.md
vendored
Normal 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)
|
||||
228
.github/instructions/typescript-mcp-server.instructions.md
vendored
Normal file
228
.github/instructions/typescript-mcp-server.instructions.md
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate an 80-character git commit title for the local diff'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/create-pr-summary.prompt.md
vendored
1
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/fix-issue.prompt.md
vendored
1
.github/prompts/fix-issue.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Execute the fix for a GitHub issue using the previously generated implementation plan'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/fix-spelling.prompt.md
vendored
1
.github/prompts/fix-spelling.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
---
|
||||
|
||||
|
||||
11
.github/prompts/review-issue.prompt.md
vendored
11
.github/prompts/review-issue.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Review a GitHub issue, score it (0-100), and generate an implementation plan'
|
||||
---
|
||||
|
||||
@@ -15,8 +14,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
|
||||
|
||||
1
.github/prompts/review-pr.prompt.md
vendored
1
.github/prompts/review-pr.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Perform a comprehensive PR review with per-step Markdown and machine-readable outputs'
|
||||
---
|
||||
|
||||
|
||||
201
.github/skills/release-note-generation/LICENSE.txt
vendored
Normal file
201
.github/skills/release-note-generation/LICENSE.txt
vendored
Normal 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.
|
||||
132
.github/skills/release-note-generation/SKILL.md
vendored
Normal file
132
.github/skills/release-note-generation/SKILL.md
vendored
Normal 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.1–2.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.1–2.4 | Label PRs | Auto-suggest + human label low-confidence |
|
||||
| 3.1–3.3 | Reviews & Grouping | Request Copilot reviews → refresh → group by label |
|
||||
| 4.1–4.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 |
|
||||
9
.github/skills/release-note-generation/references/SampleOutput.md
vendored
Normal file
9
.github/skills/release-note-generation/references/SampleOutput.md
vendored
Normal 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)!
|
||||
143
.github/skills/release-note-generation/references/step1-collection.md
vendored
Normal file
143
.github/skills/release-note-generation/references/step1-collection.md
vendored
Normal 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
|
||||
```
|
||||
131
.github/skills/release-note-generation/references/step2-labeling.md
vendored
Normal file
131
.github/skills/release-note-generation/references/step2-labeling.md
vendored
Normal 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.
|
||||
37
.github/skills/release-note-generation/references/step3-review-grouping.md
vendored
Normal file
37
.github/skills/release-note-generation/references/step3-review-grouping.md
vendored
Normal 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)).
|
||||
88
.github/skills/release-note-generation/references/step4-summarization.md
vendored
Normal file
88
.github/skills/release-note-generation/references/step4-summarization.md
vendored
Normal 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, “That’s exactly what I need” or “Yes, that’s 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
|
||||
...
|
||||
```
|
||||
90
.github/skills/release-note-generation/scripts/apply-labels.ps1
vendored
Normal file
90
.github/skills/release-note-generation/scripts/apply-labels.ps1
vendored
Normal 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"
|
||||
172
.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1
vendored
Normal file
172
.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1
vendored
Normal 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
|
||||
100
.github/skills/release-note-generation/scripts/diff_prs.ps1
vendored
Normal file
100
.github/skills/release-note-generation/scripts/diff_prs.ps1
vendored
Normal 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
|
||||
}
|
||||
344
.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1
vendored
Normal file
344
.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1
vendored
Normal 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
|
||||
80
.github/skills/release-note-generation/scripts/find-commit-by-title.ps1
vendored
Normal file
80
.github/skills/release-note-generation/scripts/find-commit-by-title.ps1
vendored
Normal 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
|
||||
}
|
||||
85
.github/skills/release-note-generation/scripts/group-prs-by-label.ps1
vendored
Normal file
85
.github/skills/release-note-generation/scripts/group-prs-by-label.ps1
vendored
Normal 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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -359,3 +359,4 @@ src/common/Telemetry/*.etl
|
||||
|
||||
# PowerToysInstaller Build Temp Files
|
||||
installer/*/*.wxs.bk
|
||||
/src/modules/awake/.claude
|
||||
|
||||
@@ -504,6 +504,14 @@ jobs:
|
||||
Remove-Item -Force -Recurse "$(JobOutputDirectory)/_appx" -ErrorAction:Ignore
|
||||
displayName: Re-pack the new CmdPal package after signing
|
||||
|
||||
- pwsh: |
|
||||
$testsPath = "$(Build.SourcesDirectory)/$(BuildPlatform)/$(BuildConfiguration)/tests"
|
||||
if (Test-Path $testsPath) {
|
||||
Remove-Item -Path $testsPath -Recurse -Force
|
||||
Write-Host "Removed tests folder to reduce signing workload: $testsPath"
|
||||
}
|
||||
displayName: Remove tests folder before signing
|
||||
|
||||
- template: steps-esrp-signing.yml
|
||||
parameters:
|
||||
displayName: Sign Core PowerToys
|
||||
|
||||
@@ -10,7 +10,7 @@ parameters:
|
||||
default: {}
|
||||
|
||||
steps:
|
||||
- task: EsrpCodeSigning@5
|
||||
- task: EsrpCodeSigning@6
|
||||
displayName: 🔏 ${{ parameters.displayName }}
|
||||
inputs:
|
||||
ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }}
|
||||
|
||||
13
.vscode/mcp.json
vendored
Normal file
13
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"servers": {
|
||||
"github-artifacts": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"tools/mcp/github-artifacts/launch.js"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_TOKEN": "${env:GITHUB_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,14 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.Uninstall_Success</td>
|
||||
<td>Logs when PowerToys is successfully uninstalled (who would do such a thing!).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.UpdateCheck_Completed</td>
|
||||
<td>Logs when an auto-update check completes, including success status, whether an update is available, and version information.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.UpdateDownload_Completed</td>
|
||||
<td>Logs when an update download completes, including success status and version.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### OOBE (Out-of-box experience)
|
||||
@@ -570,7 +578,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FindMyMouse_MousePointerFocused</td>
|
||||
<td>Occurs when the mouse pointer is focused using Find My Mouse.</td>
|
||||
<td>Occurs when the mouse pointer is focused using Find My Mouse, including the activation method (double-tap left/right Ctrl, shake mouse, or shortcut).</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
<!-- Pin the SixLabors.ImageSharp version (a transitive dependency of CoenM.ImageSharp.ImageHash) to restore functionality and apply patches. -->
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.12" />
|
||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250402" />
|
||||
@@ -24,7 +26,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||
|
||||
@@ -665,6 +665,7 @@
|
||||
</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>
|
||||
|
||||
291
README.md
291
README.md
@@ -48,22 +48,22 @@ But to get started quickly, choose one of the installation methods below:
|
||||
<details open>
|
||||
<summary><strong>Download .exe from GitHub</strong></summary>
|
||||
<br/>
|
||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-arm64.exe
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysUserSetup-0.97.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysUserSetup-0.97.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysSetup-0.97.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysSetup-0.97.0-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.96.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.96.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.96.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.96.1-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.97.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.97.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.97.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.97.0-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -83,7 +83,7 @@ You can easily install PowerToys from the Microsoft Store:
|
||||
<details>
|
||||
<summary><strong>WinGet</strong></summary>
|
||||
<br/>
|
||||
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
Download PowerToys from <a href="https://github.com/microsoft/winget-cli#installing-the-client">WinGet</a>. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
*User scope installer [default]*
|
||||
```powershell
|
||||
@@ -99,138 +99,197 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
<details>
|
||||
<summary><strong>Other methods</strong></summary>
|
||||
<br/>
|
||||
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.96 (November 2025)**
|
||||
**Version 0.97 (January 2026)**
|
||||
|
||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
**✨ Highlights**
|
||||
- Advanced Paste now supports multiple online and on-device AI model providers: Azure OpenAI, OpenAI, Google Gemini, Mistral, Foundry Local and Ollama.
|
||||
- Command Palette received extensive improvements including file search filters, better clipboard history metadata, context-menu styling, and dozens of bug fixes and enhancements.
|
||||
- PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`.
|
||||
- **Command Palette**: Major expansion with PowerToys extension (Windows 11 only), Remote Desktop built-in extension, theme customization, drag-and-drop support, fallback ranking controls, sections/separators for pages, pinyin Chinese matching, and many UX refinements.
|
||||
- **Settings**: Quick Access flyout is now a standalone process for significantly faster startup, theme-adaptive tray icon, AOT serialization, and multiple UI/accessibility fixes
|
||||
- **CursorWrap (New!)**: New mouse utility that lets your cursor wrap around screen edges, making multi-monitor navigation faster and more seamless.
|
||||
- **Advanced Paste**: Image input for AI, color detection in clipboard history, Foundry Local improvements, Azure AI icons, and multiple bug fixes
|
||||
- **CLI Support Expanded**: FancyZones, Image Resizer, and File Locksmith can now be controlled from the command line for layout management, batch image resizing, and file lock inspection.
|
||||
- **LightSwitch**: Added support for automatically following Windows Night Light mode.
|
||||
- **Release Experience & Quality**: Refreshed "What’s new" dialog, plus many performance improvements, stability fixes, and refinements across PowerToys.
|
||||
|
||||
### Advanced Paste
|
||||
- Advanced Paste now lets you connect to multiple AI providers instead of being limited to a single OpenAI provider. See [Advanced Paste documentation](https://learn.microsoft.com/windows/powertoys/advanced-paste) for usage.
|
||||
## Advanced Paste
|
||||
|
||||
### Awake
|
||||
- The Awake countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed Awake context menu positioning. The fix removed the conversion of the mouse cursor from screen to client-window coordinates, instead using the raw screen coordinates returned by GetCursorPos; the context menu now appears at the correct screen position. Thanks [@lzandman](https://github.com/lzandman)!
|
||||
- Added hex color previews in clipboard history. Thanks [@crramirez](https://github.com/crramirez)!
|
||||
- Added automatic placeholder endpoints when required fields are left empty.
|
||||
- Fixed a grammar issue in the AI settings description. Thanks [@erik-anderson](https://github.com/erik-anderson)!
|
||||
- Fixed loading order so custom action hotkeys are read correctly.
|
||||
- Updated Advanced Paste descriptions to reflect support for online and local models.
|
||||
- Fixed clipboard history item selection so it doesn’t duplicate entries.
|
||||
- Prevented placeholder endpoints from being saved for providers that don’t need them.
|
||||
- Added image input support for AI transforms and improved clipboard change tracking.
|
||||
|
||||
### Command Palette
|
||||
- The search field in context menus now matches the look of the Command Palette, with a smoke backdrop and improved padding.
|
||||
- Fallback items such as math calculations or the Run command now appear in results more quickly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured the command bar updates correctly after navigating to another page and commands are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- The Command Palette settings page has been reorganized. Activation-key options are grouped under an expander and extension settings are framed for improved readability.
|
||||
- When you modify a command, its alias, hotkey, and tags now update in the top-level list, keeping the displayed information in sync. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Press `Ctrl + ,` to open Command Palette settings from anywhere. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- You can use `Page Up` and `Page Down` to navigate the list while focus is in the search box. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- Fixed an issue where the search box could disappear when navigating pages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured search text is selected when *Go home when activated* and *Highlight search on activate* are both enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed an issue where Command Palette window occasionally appeared on the taskbar under certain Windows settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured that labels and icons of list items and menu items update when they change. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed visibility of list filters when navigating to a content page. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
- Added search to the extension list and a link to extensions on the Microsoft Store. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added options to open the Command Palette window at its last position or re-center it.
|
||||
- The Command Palette now remembers its window size after restarting.
|
||||
- Added a global error handler that logs fatal errors and provides feedback when unexpected failures force Command Palette to close. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed forms and extension settings not showing on some machines due to a missing VC++ runtime.
|
||||
- Restored ranking of fallback commands for built-in extensions (Sleep, Shutdown, Windows settings, Web search, etc.). Thanks [@jiripolasek](https://github.com/jiripolasek).
|
||||
- Improved and unified labels and texts across the application!
|
||||
- Maintainance: Resolved numerous build warnings in Command Palette projects; no user-visible impact. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Maintainance: Fixed a logging issue so exception messages are properly recorded instead of placeholder text, improving troubleshooting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
## Awake
|
||||
|
||||
### Command Palette Extensions
|
||||
- Bookmarks: Added hints about bookmark placeholders to the Add/Edit Bookmark form. — Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Bookmarks: Improved migration of bookmarks from older versions and fixed an issue where aliases or keyboard shortcuts could be lost after restart. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Clipboard history: Items shown in Command Palette’s clipboard history now include helpful metadata. For example, image items show dimensions, text files show names and sizes, web links include page titles, and text entries display word counts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- File search: Added filter buttons to show *all items*, *files only*, or *folders only*. Selecting a filter adds `kind:folders` or `kind:not folders` to narrow results.
|
||||
- System commands: Replaced the `:red_circle:` placeholder with an actual red-circle emoji so the correct icon appears in the UI. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- WinGet: Search performance feels more responsive because typed input is now processed via a task queue rather than complex cancellation tokens!
|
||||
- Window Walker: UWP apps no longer show a "not responding" label when suspended. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Window Walker: Now displays the actual icon of each window rather than using the process icon, improving recognition of PWAs and Python GUIs. Thanks [@Lee-WonJun](https://github.com/Lee-WonJun)!
|
||||
- Windows Terminal profiles: Fixed a rare crash in the Windows Terminal extension when the `LOCALAPPDATA` environment variable was missing. The path is now retrieved via a reliable API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed Awake CLI so help, errors, and logs appear correctly in the console. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Find My Mouse
|
||||
- Activating Find My Mouse no longer makes the cursor change to the busy (hourglass) icon or steals focus from your active application.
|
||||
## Command Palette
|
||||
|
||||
### Hosts File Editor
|
||||
- Added customizable backup settings allowing users to configure backup frequency, location, and auto-deletion policies. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed background image loading in BlurImageControl. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed SDK packaging paths and added a CI SDK build stage.
|
||||
- Aligned naming and spell-checking with .NET conventions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added drag-and-drop support for Command Palette items. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a PowerToys Command Palette extension to discover and launch PowerToys utilities.
|
||||
- Fixed grid view bindings and layout issues. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed a line-break issue in RDC extension toast messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made the Settings button text localizable. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Hid the RDC fallback on the home page and fixed MSTSC working directory handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Optimized result list merging for better performance. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added Small/Medium/Large detail sizes in the extensions API. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
- Hid fallback commands on the home page when no query is entered. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added back navigation support in the Settings window. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a Command Palette solution filter. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated Extension SDK documentation links to Microsoft Learn. Thanks [@RubenFricke](https://github.com/RubenFricke)!
|
||||
- Added a custom search engine URL setting for Web Search. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added pinyin matching for Chinese input. Thanks [@frg2089](https://github.com/frg2089)!
|
||||
- Bumped Command Palette version to 0.8.
|
||||
- Removed subtitles from built-in top-level commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Refined separator styling in the details pane. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a built-in Remote Desktop extension.
|
||||
- Added a Peek command to the Indexer extension.
|
||||
- Improved default browser detection using the Windows Shell API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Escape key behavior options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added theme and background customization options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved WinGet package app matching. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added an auto-return-home delay setting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added fallback ranking and global results settings.
|
||||
- Removed the selection indicator in the context menu list. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a developer ribbon with build and log info. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated the “Learn more” string for Command Palette. Thanks [@pratnala](https://github.com/pratnala)!
|
||||
- Added arrow-key navigation for grid views. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- Fixed version display when running unpackaged. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a native debugging launch profile. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Reduced redundant property change notifications in the SDK. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved section readability and accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made gallery spacing uniform. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added sections and separators for list and grid pages. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
|
||||
### Image Resizer
|
||||
- Fixed settings consistency during batch resize operations by capturing settings once before processing. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
## Crop & Lock
|
||||
|
||||
### Light Switch
|
||||
- Introduced new UI to allow users to manually enter their latitude and longitude in Sunrise to Sunset mode.
|
||||
- Refactored service with cleaner state management for stability.
|
||||
- Removed logs from every tick, only logging key events to largely reduce log size.
|
||||
- Added a screenshot mode that freezes a cropped region into its own window. Thanks [@fm-sys](https://github.com/fm-sys)!
|
||||
|
||||
### Mouse Pointer Crosshairs
|
||||
- Enabled switching between Mouse Pointer Crosshairs and Gliding Cursor modes. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
## Cursor Wrap
|
||||
|
||||
### Mouse Without Borders
|
||||
- Added horizontal scrolling support. Thanks [@MasonBergstrom](https://github.com/MasonBergstrom)!
|
||||
- Improved Cursor Wrap behavior on multi-monitor setups by wrapping only at outer edges. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Peek
|
||||
- Fixed media files remaining locked after preview window closes. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a command-line interface for file previewing. See the [Peek documentation](https://learn.microsoft.com/windows/powertoys/peek) for usage. Thanks [@prochan2](https://github.com/prochan2)!
|
||||
## FancyZones
|
||||
|
||||
### PowerRename
|
||||
- PowerRename no longer crashes due to a missing resources file.
|
||||
- Added photo metadata extraction support using EXIF and XMP for pattern-based renaming with camera info, GPS coordinates, and date taken. See [PowerRename Documentation](https://learn.microsoft.com/en-us/windows/powertoys/powerrename).
|
||||
- Fixed editor overlay positioning on mixed-DPI multi-monitor setups. Thanks [@Memphizzz](https://github.com/Memphizzz)!
|
||||
- Added a FancyZones CLI for command-line layout management.
|
||||
|
||||
### PowerToys Run
|
||||
- Added retry logic with exponential backoff to handle DWM composition errors during theme changes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated OneNote icons to reflect new Microsoft 365 design. Thanks [@trevorNgo](https://github.com/trevorNgo)!
|
||||
## File Locksmith
|
||||
|
||||
### Quick Accent
|
||||
- Added diameter symbol (⌀) for Shift+O in Special Characters mode, thanks to [@anselumjuju](https://github.com/anselumjuju)!
|
||||
- Added a File Locksmith CLI for querying, waiting on, or killing file locks.
|
||||
|
||||
### Zoomit
|
||||
- Smoothed out zoom-animation in ZoomIt by coalescing mouse-move and timer events, thanks to [@foxmsft](https://github.com/foxmsft)!
|
||||
- Enabled GIF support for ZoomIt, thanks to [@MarioHewardt](https://github.com/MarioHewardt)!
|
||||
- Fixed spelling mistakes, and refactored some literal strings to string constants, thanks to [@lzandman](https://github.com/lzandman)!
|
||||
- Fixed inaccurate "actual size" screenshots in ZoomIt and resolves a GDI handle leak, improving capture fidelity and long-session stability. thanks to [@daverayment](https://github.com/daverayment)!
|
||||
## Find My Mouse
|
||||
|
||||
### Settings
|
||||
- Fixed title bar overlapping issue at smaller window sizes.
|
||||
- Refined shortcut control visual design with improved consistency and spacing.
|
||||
- Added dashboard utilities sorting by name or status.
|
||||
- Made update notification InfoBar in flyout clickable for direct navigation to update page.
|
||||
- Expanded installation instructions by default in README.
|
||||
- Improved accessibility for shortcut conflict button with static resource-based automation properties.
|
||||
- Added ScrollViewer to Command Palette page in PowerToys Settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed module list glitches and Sort Status checkmark issue. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Improved spotlight edge rendering for clearer Find My Mouse visuals.
|
||||
- Added telemetry to track how Find My Mouse is triggered.
|
||||
|
||||
### Development
|
||||
- Fixed accessibility by associating controls with labels for screen readers.
|
||||
- Added accessible name to Shortcut Conflicts button for screen readers.
|
||||
- Excluded TitleBars from tab navigation across multiple utilities. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Migrated build infrastructure from Windows Server 2019 to Server 2022 with improved failure logging and predictable NuGet package paths.
|
||||
- Configured build agents to use larger P: drive for release builds to address disk space constraints.
|
||||
- Enhanced DSC v3 support by organizing resource manifests in a dedicated subfolder with PATH configuration.
|
||||
- Reduced installer bundle size by 6-7MB through centralized Hybrid CRT configuration across all C++ projects.
|
||||
- Updated .NET packages to version 9.0.10 for security fixes. Thanks [@snickler](https://github.com/snickler)!
|
||||
- Fixed spell check dictionary entries for consistency.
|
||||
- Restored accidentally deleted NuGet configuration file for Command Palette extensions.
|
||||
- Fixed package identity build by updating AppxManifest entry points to use PowerShell Core.
|
||||
- Optimized CI pipeline by replacing file copy operations with hard links and moves, reducing build time and disk usage by 10-15GB.
|
||||
- Updated Copilot guidance and PR prompt workflow.
|
||||
- Included high-volume bugs in issue template header. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed incorrect HRESULT logging for inner exceptions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced shared sparse package identity for PowerToys Win32 components to enable access to Windows platform APIs.
|
||||
- Consolidated installer builds to produce both machine and user installers simultaneously, reducing build time and complexity.
|
||||
- Migrated exclusively to WiX v5 installer infrastructure, removing legacy WiX v3 support.
|
||||
- Temporarily removed PowerToys installer path from PATH environment variable to prevent application crashes.
|
||||
- Added complete OCR UI test coverage with automated tests for activation, settings, language selection, and text extraction.
|
||||
- Fixed test input for drive path normalization in bookmark resolver unit tests.
|
||||
- Fixed Peek UI tests by restoring Ctrl+Space activation shortcut for test scenarios.
|
||||
- Hided apps in PowerToys.SpareApps package from Start Menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
## Image Resizer
|
||||
|
||||
- Fixed Fill mode cropping when Shrink Only is enabled. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a dedicated Image Resizer CLI for scripted resizing.
|
||||
|
||||
## Light Switch
|
||||
|
||||
- Added telemetry events for Light Switch usage and settings changes.
|
||||
- Added a Follow Night Light mode to sync theme changes with Night Light.
|
||||
- Clarified LightSwitchService and LightSwitchStateManager roles in docs.
|
||||
- Added a Quick Access dashboard button to toggle Light Switch quickly.
|
||||
- Ensured Light Switch honors GPO policy states with clear status messaging.
|
||||
|
||||
## Mouse Without Borders
|
||||
|
||||
- Continued refactoring Mouse Without Borders by splitting the large Common class into focused components. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
- Completed the Common class refactor with Core and IPC helper extraction. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
## Peek
|
||||
|
||||
- Hardened Peek previews with strict resource filtering and safer external link warnings.
|
||||
- Improved SVG preview compatibility by rendering via WebView2.
|
||||
|
||||
## PowerRename
|
||||
|
||||
- Added HEIF/AVIF EXIF metadata extraction and extension status guidance for related previews.
|
||||
- Fixed undefined behavior in file time handling. Thanks [@safocl](https://github.com/safocl)!
|
||||
- Optimized memory allocation for depth-based rename processing.
|
||||
- Fixed Unicode normalization and non‑breaking space matching. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed date token replacements followed by capital letters. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
## PowerToys Run Plugins
|
||||
|
||||
- Fixed a plugin name typo and added Project Launcher to the third‑party list. Thanks [@artickc](https://github.com/artickc)!
|
||||
- Added the Open With Antigravity plugin to the third‑party list. Thanks [@artickc](https://github.com/artickc)!
|
||||
|
||||
## PowerToys Run
|
||||
|
||||
- Avoided unnecessary hotkey conflict checks when settings change.
|
||||
- Added QuickAI to the third-party PowerToys Run plugin list. Thanks [@ruslanlap](https://github.com/ruslanlap)!
|
||||
|
||||
## Quick Accent
|
||||
|
||||
- Added localized quotation marks to Quick Accent. Thanks [@warquys](https://github.com/warquys)!
|
||||
- Fixed duplicate and redundant characters in Quick Accent sets. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Fixed DPI positioning issues for Quick Accent on mixed-DPI setups. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
|
||||
## Settings
|
||||
|
||||
- Added a new tray icon that adapts to theme changes. Thanks [@HO-COOH](https://github.com/HO-COOH)!
|
||||
- Centralized module enable/disable logic for cleaner Settings UI updates.
|
||||
- Simplified Settings utilities by removing ISettingsUtils/ISettingsPath interfaces. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Improved Settings UI consistency and disabled-state visuals.
|
||||
- Added semantic headings to the Dashboard for better accessibility.
|
||||
- Introduced Quick Access as a standalone host with updated Settings integration.
|
||||
- Fixed Dashboard toggle flicker and sort menu checkmarks. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added Native AOT-compatible settings serialization.
|
||||
- Standardized mouse tool description text. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a global SettingsUtils singleton to reduce repeated initialization.
|
||||
|
||||
## Development
|
||||
|
||||
- Fixed broken devdocs links to the coding style guide. Thanks [@RubenFricke](https://github.com/RubenFricke)!
|
||||
- Migrated main and installer solutions to .slnx for improved build tooling.
|
||||
- Restored local installer builds after the WiX v5 upgrade with signing and versioning fixes.
|
||||
- Added incremental review tooling and structured AI prompts for PR/issue reviews.
|
||||
- Documented bot commands and cleaned up devdocs structure. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Updated WinAppSDK pipeline defaults to 1.8 and fixed restore handling.
|
||||
- Updated the COMMUNITY list to reflect current roles.
|
||||
- Maintained community member ordering and added a new entry.
|
||||
- Re-enabled centralized PackageReference for native projects with VS auto-restore.
|
||||
- Disabled MSBuild caching by default in CI to avoid build instability.
|
||||
- Updated the latest WinAppSDK daily pipeline for split-dependency restores.
|
||||
- Suppressed experimental build warnings and aligned WrapPanel stretch handling.
|
||||
- Reordered the spell-check expect list for consistent automation.
|
||||
- Migrated native projects to centralized PackageReference management.
|
||||
- Cleaned spell-check dictionary entries and capitalization.
|
||||
- Synced commit/PR prompts and wired VS Code to repo prompt files.
|
||||
- Added VS Code build tasks and improved build script path handling.
|
||||
- Updated Windows App SDK package versions in central package management.
|
||||
- Migrated cmdpal extension native project to PackageReference and fixed outputs.
|
||||
- Reverted PackageReference changes back to packages.config where needed.
|
||||
- Bypassed a release version check for a failing DLL to keep pipelines green.
|
||||
- Consolidated Copilot instructions and fixed prompt frontmatter.
|
||||
- Added signing entries for new Quick Access binaries and CLI version metadata.
|
||||
- Fixed install scope detection to avoid mixed per-user/per-machine installs.
|
||||
- Added a Module Loader tool to quickly test PowerToys modules without full builds. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added update telemetry to understand auto-update checks and downloads.
|
||||
- Updated the telemetry package for new compliance requirements. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
|
||||
- Documented missing telemetry events in DATA_AND_PRIVACY.
|
||||
- Fixed UI test pipeline restores for .slnx solutions.
|
||||
- Added UI automation coverage for Advanced Paste clipboard history flows.
|
||||
- Stabilized FancyZones UI tests with more reliable selectors and screen recordings.
|
||||
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][github-next-release-work]!
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.97][github-next-release-work]!
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
|
||||
|
||||
@@ -96,3 +96,40 @@ The Shell Process Debugging Tool is a Visual Studio extension that helps debug m
|
||||
- Logs are stored in the local app directory: `%LOCALAPPDATA%\Microsoft\PowerToys`
|
||||
- Check Event Viewer for application crashes related to `PowerToys.Settings.exe`
|
||||
- Crash dumps can be obtained from Event Viewer
|
||||
|
||||
## Troubleshooting Build Errors
|
||||
|
||||
### Missing Image Files or Corrupted Build State
|
||||
|
||||
If you encounter build errors about missing image files (e.g., `.png`, `.ico`, or other assets), this typically indicates a corrupted build state. To resolve:
|
||||
|
||||
1. **Clean the solution in Visual Studio**: Build > Clean Solution
|
||||
|
||||
Or from the command line (Developer Command Prompt for VS 2022):
|
||||
```pwsh
|
||||
msbuild PowerToys.slnx /t:Clean /p:Platform=x64 /p:Configuration=Debug
|
||||
```
|
||||
|
||||
2. **Delete build output and package folders** from the repository root:
|
||||
- `x64/`
|
||||
- `ARM64/`
|
||||
- `Debug/`
|
||||
- `Release/`
|
||||
- `packages/`
|
||||
|
||||
3. **Rebuild the solution**
|
||||
|
||||
#### Helper Script
|
||||
|
||||
A PowerShell script is available to automate this cleanup:
|
||||
|
||||
```pwsh
|
||||
.\tools\build\clean-artifacts.ps1
|
||||
```
|
||||
|
||||
This script will run MSBuild Clean and remove the build folders listed above. Use `-SkipMSBuildClean` if you only want to delete the folders without running MSBuild Clean.
|
||||
|
||||
After cleaning, rebuild with:
|
||||
```pwsh
|
||||
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=x64 -m PowerToys.slnx
|
||||
```
|
||||
|
||||
@@ -83,14 +83,40 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
|
||||
1. A local clone of the PowerToys repository
|
||||
1. Enable long paths in Windows (see [Enable Long Paths](https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation#enabling-long-paths-in-windows-10-version-1607-and-later) for details)
|
||||
|
||||
### Install Visual Studio dependencies
|
||||
### Automated Setup (Recommended)
|
||||
|
||||
Run the setup script to automatically configure your development environment:
|
||||
|
||||
```powershell
|
||||
.\tools\build\setup-dev-environment.ps1
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Enable Windows long path support (requires administrator privileges)
|
||||
- Enable Windows Developer Mode (requires administrator privileges)
|
||||
- Guide you through installing required Visual Studio components from `.vsconfig`
|
||||
- Initialize git submodules
|
||||
|
||||
Run with `-Help` to see all available options:
|
||||
|
||||
```powershell
|
||||
.\tools\build\setup-dev-environment.ps1 -Help
|
||||
```
|
||||
|
||||
### Manual Setup
|
||||
|
||||
If you prefer to set up manually, follow these steps:
|
||||
|
||||
#### Install Visual Studio dependencies
|
||||
|
||||
1. Open the `PowerToys.slnx` file.
|
||||
1. If you see a dialog that says `install extra components` in the solution explorer pane, click `install`
|
||||
|
||||
### Get Submodules to compile
|
||||
Alternatively, import the `.vsconfig` file from the repository root using Visual Studio Installer to install all required workloads.
|
||||
|
||||
We have submodules that need to be initialized before you can compile most parts of PowerToys. This should be a one-time step.
|
||||
#### Get Submodules to compile
|
||||
|
||||
We have submodules that need to be initialized before you can compile most parts of PowerToys. This should be a one-time step.
|
||||
|
||||
1. Open a terminal
|
||||
1. Navigate to the folder you cloned PowerToys to.
|
||||
@@ -98,12 +124,32 @@ We have submodules that need to be initialized before you can compile most parts
|
||||
|
||||
### Compiling Source Code
|
||||
|
||||
#### Using Visual Studio
|
||||
|
||||
- Open `PowerToys.slnx` in Visual Studio.
|
||||
- In the `Solutions Configuration` drop-down menu select `Release` or `Debug`.
|
||||
- From the `Build` menu choose `Build Solution`, or press <kbd>Control</kbd>+<kbd>Shift</kbd>+<kbd>b</kbd> on your keyboard.
|
||||
- The build process may take several minutes depending on your computer's performance. Once it completes, the PowerToys binaries will be in your repo under `x64\Release\`.
|
||||
- You can run `x64\Release\PowerToys.exe` directly without installing PowerToys, but some modules (i.e. PowerRename, ImageResizer, File Explorer extension etc.) will not be available unless you also build the installer and install PowerToys.
|
||||
|
||||
#### Using Command Line
|
||||
|
||||
You can also build from the command line using the provided scripts in `tools\build\`:
|
||||
|
||||
```powershell
|
||||
# Build the full solution (auto-detects platform)
|
||||
.\tools\build\build.ps1
|
||||
|
||||
# Build with specific configuration
|
||||
.\tools\build\build.ps1 -Platform x64 -Configuration Release
|
||||
|
||||
# Build only essential projects (runner + settings) for faster iteration
|
||||
.\tools\build\build-essentials.ps1
|
||||
|
||||
# Build everything including the installer (Release only)
|
||||
.\tools\build\build-installer.ps1
|
||||
```
|
||||
|
||||
## Compile the installer
|
||||
|
||||
Our installer is two parts, an EXE and an MSI. The EXE (Bootstrapper) contains the MSI and handles more complex installation logic.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
last-update: 7-16-2024
|
||||
last-update: 1-18-2026
|
||||
---
|
||||
|
||||
# PowerToys Awake Changelog
|
||||
@@ -12,6 +12,7 @@ The build ID moniker is made up of two components - a reference to a [Halo](http
|
||||
|
||||
| Build ID | Build Date |
|
||||
|:-------------------------------------------------------------------|:------------------|
|
||||
| [`DIDACT_01182026`](#DIDACT_01182026-january-18-2026) | January 18, 2026 |
|
||||
| [`TILLSON_11272024`](#TILLSON_11272024-november-27-2024) | November 27, 2024 |
|
||||
| [`PROMETHEAN_09082024`](#PROMETHEAN_09082024-september-8-2024) | September 8, 2024 |
|
||||
| [`VISEGRADRELAY_08152024`](#VISEGRADRELAY_08152024-august-15-2024) | August 15, 2024 |
|
||||
@@ -20,6 +21,22 @@ The build ID moniker is made up of two components - a reference to a [Halo](http
|
||||
| [`LIBRARIAN_03202022`](#librarian_03202022-march-20-2022) | March 20, 2022 |
|
||||
| `ARBITER_01312022` | January 31, 2022 |
|
||||
|
||||
### `DIDACT_01182026` (January 18, 2026)
|
||||
|
||||
>[!NOTE]
|
||||
>See pull request: [Awake - `DIDACT_01182026`](https://github.com/microsoft/PowerToys/pull/44795)
|
||||
|
||||
- [#32544](https://github.com/microsoft/PowerToys/issues/32544) Fixed an issue where Awake settings became non-functional after the PC wakes from sleep. Added `WM_POWERBROADCAST` handling to detect system resume events (`PBT_APMRESUMEAUTOMATIC`, `PBT_APMRESUMESUSPEND`) and re-apply `SetThreadExecutionState` to restore the awake state.
|
||||
- [#36150](https://github.com/microsoft/PowerToys/issues/36150) Fixed an issue where Awake would not prevent sleep when AC power is connected. Added `PBT_APMPOWERSTATUSCHANGE` handling to re-apply `SetThreadExecutionState` when the power source changes (AC/battery transitions).
|
||||
- Fixed an issue where toggling "Keep screen on" during an active timed session would disrupt the countdown timer. The display setting now updates directly without restarting the timer, preserving the exact remaining time.
|
||||
- [#41918](https://github.com/microsoft/PowerToys/issues/41918) Fixed `WM_COMMAND` message processing flaw in `TrayHelper.WndProc` that incorrectly compared enum values against enum count. Added proper bounds checking for custom tray time entries.
|
||||
- Investigated [#44134](https://github.com/microsoft/PowerToys/issues/44134) - documented that `ES_DISPLAY_REQUIRED` (used when "Keep display on" is enabled) blocks Task Scheduler idle detection, preventing scheduled maintenance tasks like SSD TRIM. Workaround: disable "Keep display on" or manually run `Optimize-Volume -DriveLetter C -ReTrim`. Additional investigation needed for potential "idle window" feature.
|
||||
- [#41738](https://github.com/microsoft/PowerToys/issues/41738) Fixed `--display-on` CLI flag default from `true` to `false` to align with documentation and PowerToys settings behavior. This is a breaking change for scripts relying on the undocumented default.
|
||||
- [#41674](https://github.com/microsoft/PowerToys/issues/41674) Fixed silent failure when `SetThreadExecutionState` fails. The monitor thread now handles the return value, logs an error, and reverts to passive mode with updated tray icon.
|
||||
- [#38770](https://github.com/microsoft/PowerToys/issues/38770) Fixed tray icon failing to appear after Windows updates. Increased retry attempts and delays for icon Add operations (10 attempts, up to ~15.5 seconds total) while keeping existing fast retry behavior for Update/Delete operations.
|
||||
- [#40501](https://github.com/microsoft/PowerToys/issues/40501) Fixed tray icon not disappearing when Awake is disabled. The `SetShellIcon` function was incorrectly requiring an icon for Delete operations, causing the `NIM_DELETE` message to never be sent.
|
||||
- [#40659](https://github.com/microsoft/PowerToys/issues/40659) Fixed potential stack overflow crash in EXPIRABLE mode. Added early return after SaveSettings when correcting past expiration times, matching the pattern used by other mode handlers to prevent reentrant execution.
|
||||
|
||||
### `TILLSON_11272024` (November 27, 2024)
|
||||
|
||||
>[!NOTE]
|
||||
|
||||
@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
}
|
||||
processes.resize(bytes / sizeof(processes[0]));
|
||||
|
||||
std::array<std::wstring_view, 42> processesToTerminate = {
|
||||
std::array<std::wstring_view, 44> processesToTerminate = {
|
||||
L"PowerToys.PowerLauncher.exe",
|
||||
L"PowerToys.Settings.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
@@ -1584,12 +1584,14 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.MouseWithoutBordersService.exe",
|
||||
L"PowerToys.CropAndLock.exe",
|
||||
L"PowerToys.EnvironmentVariables.exe",
|
||||
L"PowerToys.QuickAccess.exe",
|
||||
L"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
L"PowerToys.WorkspacesLauncher.exe",
|
||||
L"PowerToys.WorkspacesLauncherUI.exe",
|
||||
L"PowerToys.WorkspacesEditor.exe",
|
||||
L"PowerToys.WorkspacesWindowArranger.exe",
|
||||
L"Microsoft.CmdPal.UI.exe",
|
||||
L"Microsoft.CmdPal.Ext.PowerToys.exe",
|
||||
L"PowerToys.ZoomIt.exe",
|
||||
L"PowerToys.exe",
|
||||
};
|
||||
|
||||
@@ -61,6 +61,16 @@
|
||||
</RegistryKey>
|
||||
<File Source="$(var.RepoDir)\Notice.md" Id="Notice.md" />
|
||||
</Component>
|
||||
<Directory Id="SvgsFolder" Name="svgs">
|
||||
<Component Id="svgs_icons" Guid="A9B7C5D3-E1F2-4A6B-8C9D-0E1F2A3B4C5D" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="svgs_icons" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="icon.ico" Source="$(var.BinDir)svgs\icon.ico" />
|
||||
<File Id="PowerToysWhite.ico" Source="$(var.BinDir)svgs\PowerToysWhite.ico" />
|
||||
<File Id="PowerToysDark.ico" Source="$(var.BinDir)svgs\PowerToysDark.ico" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</DirectoryRef>
|
||||
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
@@ -112,6 +122,7 @@
|
||||
<RemoveFolder Id="RemoveBaseApplicationsAssetsFolder" Directory="BaseApplicationsAssetsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveWinUI3AppsInstallFolder" Directory="WinUI3AppsInstallFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveWinUI3AppsAssetsFolder" Directory="WinUI3AppsAssetsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveSvgsFolder" Directory="SvgsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall" />
|
||||
</Component>
|
||||
<ComponentRef Id="powertoys_exe" />
|
||||
@@ -120,6 +131,7 @@
|
||||
<ComponentRef Id="powertoys_toast_clsid" />
|
||||
<ComponentRef Id="License_rtf" />
|
||||
<ComponentRef Id="Notice_md" />
|
||||
<ComponentRef Id="svgs_icons" />
|
||||
<ComponentRef Id="DesktopShortcut" />
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<ComponentRef Id="powertoys_env_path_user" />
|
||||
|
||||
@@ -66,5 +66,10 @@ namespace PowerToys.GPOWrapperProjection
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
|
||||
}
|
||||
|
||||
public static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue()
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredLightSwitchEnabledValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ManagedCommon
|
||||
{
|
||||
public static bool IsWindows10()
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Minor < 22000;
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build < 22000;
|
||||
}
|
||||
|
||||
public static bool IsWindows11()
|
||||
|
||||
@@ -466,39 +466,27 @@
|
||||
TextChanged="EditVariableDialogValueTxtBox_TextChanged"
|
||||
TextWrapping="Wrap" />
|
||||
<MenuFlyoutSeparator Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<ListView
|
||||
<ItemsControl
|
||||
x:Name="EditVariableValuesList"
|
||||
Margin="0,-8,0,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
AllowDrop="True"
|
||||
CanDragItems="True"
|
||||
CanReorderItems="True"
|
||||
DragItemsCompleted="EditVariableValuesList_DragItemsCompleted"
|
||||
ItemsSource="{Binding ValuesList, Mode=TwoWay}"
|
||||
Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ListView.ItemTemplate>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="40" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
LostFocus="EditVariableValuesListTextBox_LostFocus"
|
||||
Text="{Binding Text}" />
|
||||
<Button
|
||||
x:Uid="More_Options_Button"
|
||||
Grid.Column="2"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Content=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
@@ -535,8 +523,8 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentDialog>
|
||||
|
||||
@@ -16,8 +16,6 @@ namespace EnvironmentVariablesUILib
|
||||
{
|
||||
public sealed partial class EnvironmentVariablesMainPage : Page
|
||||
{
|
||||
private const string ValueListSeparator = ";";
|
||||
|
||||
private sealed class RelayCommandParameter
|
||||
{
|
||||
public RelayCommandParameter(Variable variable, VariablesSet set)
|
||||
@@ -442,7 +440,7 @@ namespace EnvironmentVariablesUILib
|
||||
variable.ValuesList.Move(index, index - 1);
|
||||
}
|
||||
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -463,7 +461,7 @@ namespace EnvironmentVariablesUILib
|
||||
variable.ValuesList.Move(index, index + 1);
|
||||
}
|
||||
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -478,7 +476,7 @@ namespace EnvironmentVariablesUILib
|
||||
var variable = EditVariableDialog.DataContext as Variable;
|
||||
variable.ValuesList.Remove(listItem);
|
||||
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -494,7 +492,7 @@ namespace EnvironmentVariablesUILib
|
||||
var index = variable.ValuesList.IndexOf(listItem);
|
||||
variable.ValuesList.Insert(index, new Variable.ValuesListItem { Text = string.Empty });
|
||||
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -512,7 +510,7 @@ namespace EnvironmentVariablesUILib
|
||||
var index = variable.ValuesList.IndexOf(listItem);
|
||||
variable.ValuesList.Insert(index + 1, new Variable.ValuesListItem { Text = string.Empty });
|
||||
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -534,7 +532,7 @@ namespace EnvironmentVariablesUILib
|
||||
listItem.Text = (sender as TextBox)?.Text;
|
||||
var variable = EditVariableDialog.DataContext as Variable;
|
||||
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -550,16 +548,5 @@ namespace EnvironmentVariablesUILib
|
||||
CancelAddVariable();
|
||||
ConfirmAddVariableBtn.IsEnabled = false;
|
||||
}
|
||||
|
||||
private void EditVariableValuesList_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
if (EditVariableDialog.DataContext is Variable variable && variable.ValuesList != null)
|
||||
{
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList.Select(x => x.Text));
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace Hosts.UITests
|
||||
|
||||
this.Find<NavigationViewItem>("Hosts File Editor").Click();
|
||||
|
||||
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
|
||||
this.Find<ToggleSwitch>("Hosts File Editor").Toggle(true);
|
||||
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin);
|
||||
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);
|
||||
|
||||
|
||||
123
src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj
Normal file
123
src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj
Normal file
@@ -0,0 +1,123 @@
|
||||
<?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')" />
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{79267138-2895-4346-9021-21408d65379f}</ProjectGuid>
|
||||
<RootNamespace>LightSwitchLib</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
|
||||
<ProjectName>LightSwitchLib</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<AdditionalIncludeDirectories>
|
||||
./;
|
||||
..\..\..\common;
|
||||
..\..\..\common\logger;
|
||||
..\..\..\common\utils;
|
||||
..\..\..\..\deps\spdlog\include;
|
||||
%(AdditionalIncludeDirectories)
|
||||
</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
</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')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ThemeHelper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThemeHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,9 +1,6 @@
|
||||
#include <windows.h>
|
||||
#include <logger/logger_settings.h>
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "pch.h"
|
||||
#include "ThemeHelper.h"
|
||||
#include <SettingsConstants.h>
|
||||
#include <logger/logger.h>
|
||||
|
||||
// Controls changing the themes.
|
||||
|
||||
@@ -63,7 +60,7 @@ void SetSystemTheme(bool mode)
|
||||
if (mode) // if are changing to light mode
|
||||
{
|
||||
ResetColorPrevalence();
|
||||
Logger::info(L"[LightSwitchService] Reset ColorPrevalence to default when switching to light mode.");
|
||||
Logger::info(L"[LightSwitchLib] Reset ColorPrevalence to default when switching to light mode.");
|
||||
}
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
@@ -136,4 +133,4 @@ bool IsNightLightEnabled()
|
||||
|
||||
RegCloseKey(hKey);
|
||||
return data[23] == 0x10 && data[24] == 0x00;
|
||||
}
|
||||
}
|
||||
10
src/modules/LightSwitch/LightSwitchLib/ThemeHelper.h
Normal file
10
src/modules/LightSwitch/LightSwitchLib/ThemeHelper.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
inline constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
inline constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";
|
||||
|
||||
void SetSystemTheme(bool isLight);
|
||||
void SetAppsTheme(bool isLight);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
bool IsNightLightEnabled();
|
||||
1
src/modules/LightSwitch/LightSwitchLib/pch.cpp
Normal file
1
src/modules/LightSwitch/LightSwitchLib/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
5
src/modules/LightSwitch/LightSwitchLib/pch.h
Normal file
5
src/modules/LightSwitch/LightSwitchLib/pch.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
||||
#include <windows.h>
|
||||
#include <vector>
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "pch.h"
|
||||
#include "ThemeHelper.h"
|
||||
|
||||
extern "C" __declspec(dllexport) void __cdecl LightSwitch_SetSystemTheme(bool isLight)
|
||||
{
|
||||
SetSystemTheme(isLight);
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) void __cdecl LightSwitch_SetAppsTheme(bool isLight)
|
||||
{
|
||||
SetAppsTheme(isLight);
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) bool __cdecl LightSwitch_GetCurrentSystemTheme()
|
||||
{
|
||||
return GetCurrentSystemTheme();
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) bool __cdecl LightSwitch_GetCurrentAppsTheme()
|
||||
{
|
||||
return GetCurrentAppsTheme();
|
||||
}
|
||||
@@ -166,17 +166,17 @@
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>..\LightSwitchLib;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
<ClCompile Include="ExportedFunctions.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
@@ -187,7 +187,6 @@
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">pch.h</PrecompiledHeaderFile>
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="trace.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -203,6 +202,9 @@
|
||||
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\LightSwitchLib\LightSwitchLib.vcxproj">
|
||||
<Project>{79267138-2895-4346-9021-21408d65379f}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include <windows.h>
|
||||
#include "ThemeHelper.h"
|
||||
|
||||
// Controls changing the themes.
|
||||
static void ResetColorPrevalence()
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD value = 0; // back to default value
|
||||
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void SetAppsTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD value = mode;
|
||||
RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void SetSystemTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD value = mode;
|
||||
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
if (mode) // if are changing to light mode
|
||||
{
|
||||
ResetColorPrevalence();
|
||||
}
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool GetCurrentSystemTheme()
|
||||
{
|
||||
HKEY hKey;
|
||||
DWORD value = 1; // default = light
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
|
||||
return value == 1; // true = light, false = dark
|
||||
}
|
||||
|
||||
bool GetCurrentAppsTheme()
|
||||
{
|
||||
HKEY hKey;
|
||||
DWORD value = 1;
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
|
||||
return value == 1; // true = light, false = dark
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
@@ -55,6 +55,7 @@
|
||||
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>
|
||||
./../;
|
||||
..\LightSwitchLib;
|
||||
..\..\..\common;
|
||||
..\..\..\common\logger;
|
||||
..\..\..\common\utils;
|
||||
@@ -78,7 +79,6 @@
|
||||
<ClCompile Include="LightSwitchStateManager.cpp" />
|
||||
<ClCompile Include="NightLightRegistryObserver.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
<ClCompile Include="trace.cpp" />
|
||||
<ClCompile Include="WinHookEventIDs.cpp" />
|
||||
@@ -93,7 +93,6 @@
|
||||
<ClInclude Include="NightLightRegistryObserver.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
<ClInclude Include="ThemeScheduler.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="WinHookEventIDs.h" />
|
||||
@@ -111,6 +110,9 @@
|
||||
<ProjectReference Include="..\..\..\common\Telemetry\EtwTrace\EtwTrace.vcxproj">
|
||||
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\LightSwitchLib\LightSwitchLib.vcxproj">
|
||||
<Project>{79267138-2895-4346-9021-21408d65379f}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
@@ -118,4 +120,4 @@
|
||||
<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')" />
|
||||
<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')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -12,6 +12,3 @@ enum class SettingId
|
||||
ChangeSystem,
|
||||
ChangeApps
|
||||
};
|
||||
|
||||
constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
bool IsNightLightEnabled();
|
||||
@@ -18,5 +18,16 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj">
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\LightSwitchModuleInterface\LightSwitchModuleInterface.vcxproj">
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyNativeDll" AfterTargets="Build">
|
||||
<Copy SourceFiles="$(SolutionDir)$(Platform)\$(Configuration)\PowerToys.LightSwitchModuleInterface.dll" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" Condition="Exists('$(SolutionDir)$(Platform)\$(Configuration)\PowerToys.LightSwitchModuleInterface.dll')" />
|
||||
<Copy SourceFiles="$(SolutionDir)$(Platform)\$(Configuration)\LightSwitchLib\LightSwitchLib.lib" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" Condition="Exists('$(SolutionDir)$(Platform)\$(Configuration)\LightSwitchLib\LightSwitchLib.lib')" ContinueOnError="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
@@ -17,6 +18,20 @@ namespace LightSwitch.UITests
|
||||
{
|
||||
private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
|
||||
|
||||
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void LightSwitch_SetSystemTheme(bool isLight);
|
||||
|
||||
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void LightSwitch_SetAppsTheme(bool isLight);
|
||||
|
||||
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
private static extern bool LightSwitch_GetCurrentSystemTheme();
|
||||
|
||||
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
private static extern bool LightSwitch_GetCurrentAppsTheme();
|
||||
|
||||
/// <summary>
|
||||
/// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
|
||||
/// </summary>
|
||||
@@ -127,8 +142,7 @@ namespace LightSwitch.UITests
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
public static void CleanupTest(UITestBase testBase)
|
||||
{
|
||||
// TODO: Make sure the task kills?
|
||||
// CloseLightSwitch(testBase);
|
||||
CloseLightSwitch(testBase);
|
||||
|
||||
// Ensure we're attached to settings after cleanup
|
||||
try
|
||||
@@ -141,6 +155,51 @@ namespace LightSwitch.UITests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch to white/light theme for both system and apps
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
public static void CloseLightSwitch(UITestBase testBase)
|
||||
{
|
||||
// Kill LightSwitch process before setting themes
|
||||
KillLightSwitchProcess();
|
||||
|
||||
// Set both themes to light (white)
|
||||
SetSystemTheme(true);
|
||||
SetAppsTheme(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kill the LightSwitch service process if it's running
|
||||
/// </summary>
|
||||
private static void KillLightSwitchProcess()
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = System.Diagnostics.Process.GetProcessesByName("PowerToys.LightSwitchService");
|
||||
foreach (var process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit(2000);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors killing individual processes
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors enumerating processes
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a update time test operation
|
||||
/// </summary>
|
||||
@@ -408,24 +467,22 @@ namespace LightSwitch.UITests
|
||||
/* Helpers */
|
||||
private static int GetSystemTheme()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
if (key is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (int)key.GetValue("SystemUsesLightTheme", 1);
|
||||
return LightSwitch_GetCurrentSystemTheme() ? 1 : 0;
|
||||
}
|
||||
|
||||
private static int GetAppsTheme()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
if (key is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return LightSwitch_GetCurrentAppsTheme() ? 1 : 0;
|
||||
}
|
||||
|
||||
return (int)key.GetValue("AppsUseLightTheme", 1);
|
||||
private static void SetSystemTheme(bool isLight)
|
||||
{
|
||||
LightSwitch_SetSystemTheme(isLight);
|
||||
}
|
||||
|
||||
private static void SetAppsTheme(bool isLight)
|
||||
{
|
||||
LightSwitch_SetAppsTheme(isLight);
|
||||
}
|
||||
|
||||
private static string GetHelpTextValue(string helpText, string key)
|
||||
|
||||
@@ -238,12 +238,30 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
auto settingsObject = settings.get_raw_json();
|
||||
FindMyMouseSettings findMyMouseSettings;
|
||||
if (settingsObject.GetView().Size())
|
||||
|
||||
if (!settingsObject.GetView().Size())
|
||||
{
|
||||
Logger::info("Find My Mouse settings are empty");
|
||||
m_findMyMouseSettings = findMyMouseSettings;
|
||||
return;
|
||||
}
|
||||
|
||||
// Early exit if no properties object exists
|
||||
if (!settingsObject.HasKey(JSON_KEY_PROPERTIES))
|
||||
{
|
||||
Logger::info("Find My Mouse settings have no properties");
|
||||
m_findMyMouseSettings = findMyMouseSettings;
|
||||
return;
|
||||
}
|
||||
|
||||
auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
|
||||
// Parse Activation Method
|
||||
if (properties.HasKey(JSON_KEY_ACTIVATION_METHOD))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse Activation Method
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_METHOD);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_METHOD);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value < static_cast<int>(FindMyMouseActivationMethod::EnumElements) && value >= 0)
|
||||
{
|
||||
@@ -266,34 +284,50 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Activation Method from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Include Win Key
|
||||
if (properties.HasKey(JSON_KEY_INCLUDE_WIN_KEY))
|
||||
{
|
||||
try
|
||||
{
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_INCLUDE_WIN_KEY);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_INCLUDE_WIN_KEY);
|
||||
findMyMouseSettings.includeWinKey = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to get 'include windows key with ctrl' setting");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Do Not Activate On Game Mode
|
||||
if (properties.HasKey(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE))
|
||||
{
|
||||
try
|
||||
{
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE);
|
||||
findMyMouseSettings.doNotActivateOnGameMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to get 'do not activate on game mode' setting");
|
||||
}
|
||||
// Colors + legacy overlay opacity migration
|
||||
// Desired behavior:
|
||||
// - Old schema: colors stored as RGB (no alpha) + separate overlay_opacity (0-100). We should migrate by applying that opacity as alpha.
|
||||
// - New schema: colors stored as ARGB (alpha embedded). Ignore overlay_opacity even if still present.
|
||||
int legacyOverlayOpacity = -1;
|
||||
bool backgroundColorHadExplicitAlpha = false;
|
||||
bool spotlightColorHadExplicitAlpha = false;
|
||||
}
|
||||
|
||||
// Colors + legacy overlay opacity migration
|
||||
// Desired behavior:
|
||||
// - Old schema: colors stored as RGB (no alpha) + separate overlay_opacity (0-100). We should migrate by applying that opacity as alpha.
|
||||
// - New schema: colors stored as ARGB (alpha embedded). Ignore overlay_opacity even if still present.
|
||||
int legacyOverlayOpacity = -1;
|
||||
bool backgroundColorHadExplicitAlpha = false;
|
||||
bool spotlightColorHadExplicitAlpha = false;
|
||||
|
||||
// Parse Legacy Overlay Opacity (may not exist in newer settings)
|
||||
if (properties.HasKey(JSON_KEY_OVERLAY_OPACITY))
|
||||
{
|
||||
try
|
||||
{
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_OVERLAY_OPACITY);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_OVERLAY_OPACITY);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 0 && value <= 100)
|
||||
{
|
||||
@@ -302,11 +336,16 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// overlay_opacity may not exist anymore
|
||||
// overlay_opacity may have invalid data
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Background Color
|
||||
if (properties.HasKey(JSON_KEY_BACKGROUND_COLOR))
|
||||
{
|
||||
try
|
||||
{
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_BACKGROUND_COLOR);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_BACKGROUND_COLOR);
|
||||
auto backgroundColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
|
||||
uint8_t a = 255, r, g, b;
|
||||
bool parsed = false;
|
||||
@@ -333,9 +372,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize background color from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Spotlight Color
|
||||
if (properties.HasKey(JSON_KEY_SPOTLIGHT_COLOR))
|
||||
{
|
||||
try
|
||||
{
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_COLOR);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_COLOR);
|
||||
auto spotlightColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
|
||||
uint8_t a = 255, r, g, b;
|
||||
bool parsed = false;
|
||||
@@ -362,10 +406,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize spotlight color from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Spotlight Radius
|
||||
if (properties.HasKey(JSON_KEY_SPOTLIGHT_RADIUS))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse Spotlight Radius
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_RADIUS);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_RADIUS);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 0)
|
||||
{
|
||||
@@ -380,10 +428,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Spotlight Radius from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Animation Duration
|
||||
if (properties.HasKey(JSON_KEY_ANIMATION_DURATION_MS))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse Animation Duration
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ANIMATION_DURATION_MS);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ANIMATION_DURATION_MS);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 0)
|
||||
{
|
||||
@@ -398,10 +450,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Animation Duration from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Spotlight Initial Zoom
|
||||
if (properties.HasKey(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse Spotlight Initial Zoom
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 0)
|
||||
{
|
||||
@@ -416,10 +472,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Spotlight Initial Zoom from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Excluded Apps
|
||||
if (properties.HasKey(JSON_KEY_EXCLUDED_APPS))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse Excluded Apps
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_EXCLUDED_APPS);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_EXCLUDED_APPS);
|
||||
std::wstring apps = jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE).c_str();
|
||||
std::vector<std::wstring> excludedApps;
|
||||
auto excludedUppercase = apps;
|
||||
@@ -441,10 +501,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Excluded Apps from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Shaking Minimum Distance
|
||||
if (properties.HasKey(JSON_KEY_SHAKING_MINIMUM_DISTANCE))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse Shaking Minimum Distance
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHAKING_MINIMUM_DISTANCE);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_MINIMUM_DISTANCE);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 0)
|
||||
{
|
||||
@@ -459,10 +523,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Shaking Minimum Distance from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Shaking Interval Milliseconds
|
||||
if (properties.HasKey(JSON_KEY_SHAKING_INTERVAL_MS))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse Shaking Interval Milliseconds
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHAKING_INTERVAL_MS);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_INTERVAL_MS);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 0)
|
||||
{
|
||||
@@ -477,10 +545,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Shaking Interval Milliseconds from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Shaking Factor
|
||||
if (properties.HasKey(JSON_KEY_SHAKING_FACTOR))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse Shaking Factor
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHAKING_FACTOR);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_FACTOR);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 0)
|
||||
{
|
||||
@@ -495,11 +567,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Shaking Factor from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse HotKey
|
||||
if (properties.HasKey(JSON_KEY_ACTIVATION_SHORTCUT))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse HotKey
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
|
||||
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
|
||||
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
|
||||
m_hotkey = HotkeyEx();
|
||||
if (hotkey.win_pressed())
|
||||
@@ -528,18 +603,15 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
Logger::warn("Failed to initialize Activation Shortcut from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_hotkey.modifiersMask)
|
||||
{
|
||||
Logger::info("Using default Activation Shortcut");
|
||||
m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
|
||||
m_hotkey.vkCode = 0x46; // F key
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!m_hotkey.modifiersMask)
|
||||
{
|
||||
Logger::info("Find My Mouse settings are empty");
|
||||
Logger::info("Using default Activation Shortcut");
|
||||
m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
|
||||
m_hotkey.vkCode = 0x46; // F key
|
||||
}
|
||||
|
||||
m_findMyMouseSettings = findMyMouseSettings;
|
||||
}
|
||||
|
||||
|
||||
@@ -456,10 +456,11 @@ namespace MouseUtils.UITests
|
||||
var groupAppearanceBehavior = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseAppearanceBehavior));
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
// groupAppearanceBehavior.Click();
|
||||
if (foundCustom.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseOverlayOpacity)).Count == 0)
|
||||
var expandState = groupAppearanceBehavior.Selected;
|
||||
if (!expandState)
|
||||
{
|
||||
groupAppearanceBehavior.Click();
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
// Set the BackGround color
|
||||
@@ -541,15 +542,6 @@ namespace MouseUtils.UITests
|
||||
Task.Delay(500).Wait();
|
||||
spotlightColorButton.Click(false, 500, 1500);
|
||||
|
||||
// Set the overlay opacity to overlayOpacity%
|
||||
var overlayOpacitySlider = foundCustom.Find<Slider>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseOverlayOpacity));
|
||||
Assert.IsNotNull(overlayOpacitySlider);
|
||||
Assert.IsNotNull(settings.OverlayOpacity);
|
||||
int overlayOpacityValue = int.Parse(settings.OverlayOpacity, CultureInfo.InvariantCulture);
|
||||
overlayOpacitySlider.QuickSetValue(overlayOpacityValue);
|
||||
Assert.AreEqual(settings.OverlayOpacity, overlayOpacitySlider.Text);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Set the Fade Initial zoom to 0
|
||||
var spotlightInitialZoomSlider = foundCustom.Find<Slider>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseSpotlightZoom));
|
||||
Assert.IsNotNull(spotlightInitialZoomSlider);
|
||||
@@ -592,7 +584,7 @@ namespace MouseUtils.UITests
|
||||
// Assert.IsNull(animationDisabledWarning);
|
||||
if (foundElements.Count != 0)
|
||||
{
|
||||
var openSettingsLink = foundCustom.Find<Element>("Open settings");
|
||||
var openSettingsLink = foundCustom.Find<Element>("Open animation settings");
|
||||
Assert.IsNotNull(openSettingsLink);
|
||||
openSettingsLink.Click(false, 500, 3000);
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ namespace MouseUtils.UITests
|
||||
public const string FindMyMouseExcludedApps = "MouseUtils_FindMyMouseExcludedAppsId";
|
||||
public const string FindMyMouseBackgroundColor = "MouseUtils_FindMyMouseBackgroundColorId";
|
||||
public const string FindMyMouseSpotlightColor = "MouseUtils_FindMyMouseSpotlightColorId";
|
||||
public const string FindMyMouseOverlayOpacity = "MouseUtils_FindMyMouseOverlayOpacityId";
|
||||
public const string FindMyMouseSpotlightZoom = "MouseUtils_FindMyMouseSpotlightZoomId";
|
||||
public const string FindMyMouseSpotlightRadius = "MouseUtils_FindMyMouseSpotlightRadiusId";
|
||||
public const string FindMyMouseAnimationDuration = "MouseUtils_FindMyMouseAnimationDurationId";
|
||||
@@ -72,10 +71,10 @@ namespace MouseUtils.UITests
|
||||
|
||||
private static readonly Dictionary<MouseUtils, string> MouseUtilUIToggleMap = new()
|
||||
{
|
||||
[MouseUtils.MouseHighlighter] = @"Enable Mouse Highlighter",
|
||||
[MouseUtils.FindMyMouse] = @"Enable Find My Mouse",
|
||||
[MouseUtils.MousePointerCrosshairs] = @"Enable Mouse Pointer Crosshairs",
|
||||
[MouseUtils.MouseJump] = @"Enable Mouse Jump",
|
||||
[MouseUtils.MouseHighlighter] = @"Mouse Highlighter",
|
||||
[MouseUtils.FindMyMouse] = @"Find My Mouse",
|
||||
[MouseUtils.MousePointerCrosshairs] = @"Mouse Pointer Crosshairs",
|
||||
[MouseUtils.MouseJump] = @"Mouse Jump",
|
||||
};
|
||||
|
||||
public static string GetMouseUtilUIName(MouseUtils element)
|
||||
|
||||
@@ -10,10 +10,10 @@ public struct ApplicationWrapper
|
||||
{
|
||||
public struct WindowPositionWrapper
|
||||
{
|
||||
[JsonPropertyName("x")]
|
||||
[JsonPropertyName("X")]
|
||||
public int X { get; set; }
|
||||
|
||||
[JsonPropertyName("y")]
|
||||
[JsonPropertyName("Y")]
|
||||
public int Y { get; set; }
|
||||
|
||||
[JsonPropertyName("width")]
|
||||
|
||||
@@ -57,7 +57,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
GoToSettingsPageAndEnable();
|
||||
|
||||
// Find the enable toggle
|
||||
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
|
||||
var enableToggle = Find<ToggleSwitch>("Workspaces");
|
||||
Assert.IsNotNull(enableToggle, "Enable Workspaces toggle should exist");
|
||||
|
||||
Assert.IsTrue(enableToggle.IsOn, "Enable Workspaces toggle should be in the 'on' state");
|
||||
@@ -80,7 +80,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
public void TestLaunchEditorByActivationShortcut()
|
||||
{
|
||||
// Ensure module is enabled
|
||||
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
|
||||
var enableToggle = Find<ToggleSwitch>("Workspaces");
|
||||
if (!enableToggle.IsOn)
|
||||
{
|
||||
enableToggle.Click();
|
||||
@@ -109,7 +109,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
public void TestDisabledModuleDoesNotLaunchByShortcut()
|
||||
{
|
||||
// Disable the module
|
||||
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
|
||||
var enableToggle = Find<ToggleSwitch>("Workspaces");
|
||||
if (enableToggle.IsOn)
|
||||
{
|
||||
enableToggle.Click();
|
||||
@@ -131,7 +131,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
RestartScopeExe();
|
||||
NavigateToWorkspacesSettings();
|
||||
|
||||
enableToggle = Find<ToggleSwitch>("Enable Workspaces");
|
||||
enableToggle = Find<ToggleSwitch>("Workspaces");
|
||||
if (!enableToggle.IsOn)
|
||||
{
|
||||
enableToggle.Click();
|
||||
@@ -174,7 +174,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
|
||||
this.Find<NavigationViewItem>("Workspaces").Click();
|
||||
|
||||
var enableButton = this.Find<ToggleSwitch>("Enable Workspaces");
|
||||
var enableButton = this.Find<ToggleSwitch>("Workspaces");
|
||||
Assert.IsNotNull(enableButton, "Enable Workspaces toggle should exist");
|
||||
|
||||
if (!enableButton.IsOn)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Common.UI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using PowerToys.ModuleContracts;
|
||||
|
||||
@@ -82,10 +83,9 @@ public sealed class AwakeService : ModuleServiceBase, IAwakeService
|
||||
return UpdateSettingsAsync(
|
||||
settings =>
|
||||
{
|
||||
var totalMinutes = Math.Min(minutes, int.MaxValue);
|
||||
settings.Properties.Mode = AwakeMode.TIMED;
|
||||
settings.Properties.IntervalHours = (uint)(totalMinutes / 60);
|
||||
settings.Properties.IntervalMinutes = (uint)(totalMinutes % 60);
|
||||
settings.Properties.IntervalHours = (uint)(minutes / 60);
|
||||
settings.Properties.IntervalMinutes = (uint)(minutes % 60);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -130,8 +130,9 @@ public sealed class AwakeService : ModuleServiceBase, IAwakeService
|
||||
{
|
||||
return Process.GetProcessesByName("PowerToys.Awake").Length > 0;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to check Awake process status: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -143,8 +144,9 @@ public sealed class AwakeService : ModuleServiceBase, IAwakeService
|
||||
var settingsUtils = SettingsUtils.Default;
|
||||
return settingsUtils.GetSettingsOrDefault<AwakeSettings>(AwakeSettings.ModuleName);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to read Awake settings: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ namespace Awake.Core
|
||||
// Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY
|
||||
// is representative of the date when the last change was made before
|
||||
// the pull request is issued.
|
||||
internal const string BuildId = "TILLSON_11272024";
|
||||
internal const string BuildId = "DIDACT_01182026";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,14 +22,13 @@ namespace Awake.Core
|
||||
|
||||
public static string ToHumanReadableString(this TimeSpan timeSpan)
|
||||
{
|
||||
// Get days, hours, minutes, and seconds from the TimeSpan
|
||||
int days = timeSpan.Days;
|
||||
int hours = timeSpan.Hours;
|
||||
int minutes = timeSpan.Minutes;
|
||||
int seconds = timeSpan.Seconds;
|
||||
// Format as H:MM:SS or M:SS depending on total hours
|
||||
if (timeSpan.TotalHours >= 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalHours}:{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}";
|
||||
}
|
||||
|
||||
// Format the string based on the presence of days, hours, minutes, and seconds
|
||||
return $"{days:D2}{Properties.Resources.AWAKE_LABEL_DAYS} {hours:D2}{Properties.Resources.AWAKE_LABEL_HOURS} {minutes:D2}{Properties.Resources.AWAKE_LABEL_MINUTES} {seconds:D2}{Properties.Resources.AWAKE_LABEL_SECONDS}";
|
||||
return $"{timeSpan.Minutes}:{timeSpan.Seconds:D2}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Awake.Core
|
||||
|
||||
internal static SettingsUtils? ModuleSettings { get; set; }
|
||||
|
||||
private static AwakeMode CurrentOperatingMode { get; set; }
|
||||
internal static AwakeMode CurrentOperatingMode { get; private set; }
|
||||
|
||||
private static bool IsDisplayOn { get; set; }
|
||||
|
||||
@@ -54,11 +54,12 @@ namespace Awake.Core
|
||||
private static readonly CompositeFormat AwakeHour = CompositeFormat.Parse(Resources.AWAKE_HOUR);
|
||||
private static readonly CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS);
|
||||
private static readonly BlockingCollection<ExecutionState> _stateQueue;
|
||||
private static CancellationTokenSource _tokenSource;
|
||||
private static CancellationTokenSource _monitorTokenSource;
|
||||
private static IDisposable? _timerSubscription;
|
||||
|
||||
static Manager()
|
||||
{
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
_monitorTokenSource = new CancellationTokenSource();
|
||||
_stateQueue = [];
|
||||
ModuleSettings = SettingsUtils.Default;
|
||||
}
|
||||
@@ -68,18 +69,36 @@ namespace Awake.Core
|
||||
Thread monitorThread = new(() =>
|
||||
{
|
||||
Thread.CurrentThread.IsBackground = false;
|
||||
while (true)
|
||||
try
|
||||
{
|
||||
ExecutionState state = _stateQueue.Take();
|
||||
while (!_monitorTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
ExecutionState state = _stateQueue.Take(_monitorTokenSource.Token);
|
||||
|
||||
Logger.LogInfo($"Setting state to {state}");
|
||||
Logger.LogInfo($"Setting state to {state}");
|
||||
|
||||
SetAwakeState(state);
|
||||
if (!SetAwakeState(state))
|
||||
{
|
||||
Logger.LogError($"Failed to set execution state to {state}. Reverting to passive mode.");
|
||||
CurrentOperatingMode = AwakeMode.PASSIVE;
|
||||
SetModeShellIcon();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogInfo("Monitor thread received cancellation signal. Exiting gracefully.");
|
||||
}
|
||||
});
|
||||
monitorThread.Start();
|
||||
}
|
||||
|
||||
internal static void StopMonitor()
|
||||
{
|
||||
_monitorTokenSource.Cancel();
|
||||
_monitorTokenSource.Dispose();
|
||||
}
|
||||
|
||||
internal static void SetConsoleControlHandler(ConsoleEventHandler handler, bool addHandler)
|
||||
{
|
||||
Bridge.SetConsoleCtrlHandler(handler, addHandler);
|
||||
@@ -110,8 +129,9 @@ namespace Awake.Core
|
||||
ExecutionState stateResult = Bridge.SetThreadExecutionState(state);
|
||||
return stateResult != 0;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to set awake state to {state}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -123,26 +143,34 @@ namespace Awake.Core
|
||||
: ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-applies the current awake state after a power event.
|
||||
/// Called when WM_POWERBROADCAST indicates system wake or power source change.
|
||||
/// </summary>
|
||||
internal static void ReapplyAwakeState()
|
||||
{
|
||||
if (CurrentOperatingMode == AwakeMode.PASSIVE)
|
||||
{
|
||||
// No need to reapply in passive mode
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Power event received. Reapplying awake state for mode: {CurrentOperatingMode}");
|
||||
_stateQueue.Add(ComputeAwakeState(IsDisplayOn));
|
||||
}
|
||||
|
||||
internal static void CancelExistingThread()
|
||||
{
|
||||
Logger.LogInfo("Ensuring the thread is properly cleaned up...");
|
||||
Logger.LogInfo("Canceling existing timer and resetting state...");
|
||||
|
||||
// Reset the thread state and handle cancellation.
|
||||
// Reset the thread state.
|
||||
_stateQueue.Add(ExecutionState.ES_CONTINUOUS);
|
||||
|
||||
if (_tokenSource != null)
|
||||
{
|
||||
_tokenSource.Cancel();
|
||||
_tokenSource.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Token source is null.");
|
||||
}
|
||||
// Dispose the timer subscription to stop any running timer.
|
||||
_timerSubscription?.Dispose();
|
||||
_timerSubscription = null;
|
||||
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
|
||||
Logger.LogInfo("New token source and thread token instantiated.");
|
||||
Logger.LogInfo("Timer subscription disposed.");
|
||||
}
|
||||
|
||||
internal static void SetModeShellIcon(bool forceAdd = false)
|
||||
@@ -153,25 +181,25 @@ namespace Awake.Core
|
||||
switch (CurrentOperatingMode)
|
||||
{
|
||||
case AwakeMode.INDEFINITE:
|
||||
string processText = ProcessId == 0
|
||||
string pidLine = ProcessId == 0
|
||||
? string.Empty
|
||||
: $" - {Resources.AWAKE_TRAY_TEXT_PID_BINDING}: {ProcessId}";
|
||||
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_INDEFINITE}{processText}][{ScreenStateString}]";
|
||||
: $"\nPID: {ProcessId}";
|
||||
iconText = $"{Constants.FullAppName}\n{Resources.AWAKE_TRAY_TEXT_INDEFINITE}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}{pidLine}";
|
||||
icon = TrayHelper.IndefiniteIcon;
|
||||
break;
|
||||
|
||||
case AwakeMode.PASSIVE:
|
||||
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_OFF}]";
|
||||
iconText = $"{Constants.FullAppName}\n{Resources.AWAKE_SCREEN_OFF}";
|
||||
icon = TrayHelper.DisabledIcon;
|
||||
break;
|
||||
|
||||
case AwakeMode.EXPIRABLE:
|
||||
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_EXPIRATION}][{ScreenStateString}][{ExpireAt:yyyy-MM-dd HH:mm:ss}]";
|
||||
iconText = $"{Constants.FullAppName}\n{Resources.AWAKE_TRAY_UNTIL} {ExpireAt:MMM d, h:mm tt}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}";
|
||||
icon = TrayHelper.ExpirableIcon;
|
||||
break;
|
||||
|
||||
case AwakeMode.TIMED:
|
||||
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}]";
|
||||
iconText = $"{Constants.FullAppName}\n{Resources.AWAKE_TRAY_TEXT_TIMED}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}";
|
||||
icon = TrayHelper.TimedIcon;
|
||||
break;
|
||||
}
|
||||
@@ -280,9 +308,8 @@ namespace Awake.Core
|
||||
|
||||
TimeSpan remainingTime = expireAt - DateTimeOffset.Now;
|
||||
|
||||
Observable.Timer(remainingTime).Subscribe(
|
||||
_ => HandleTimerCompletion("expirable"),
|
||||
_tokenSource.Token);
|
||||
_timerSubscription = Observable.Timer(remainingTime).Subscribe(
|
||||
_ => HandleTimerCompletion("expirable"));
|
||||
}
|
||||
|
||||
internal static void SetTimedKeepAwake(uint seconds, bool keepDisplayOn = true, [CallerMemberName] string callerName = "")
|
||||
@@ -300,6 +327,8 @@ namespace Awake.Core
|
||||
TimeSpan timeSpan = TimeSpan.FromSeconds(seconds);
|
||||
|
||||
uint totalHours = (uint)timeSpan.TotalHours;
|
||||
|
||||
// Round up partial minutes to prevent timer from expiring before intended duration
|
||||
uint remainingMinutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60);
|
||||
|
||||
bool settingsChanged = currentSettings.Properties.Mode != AwakeMode.TIMED ||
|
||||
@@ -336,7 +365,7 @@ namespace Awake.Core
|
||||
|
||||
var targetExpiryTime = DateTimeOffset.Now.AddSeconds(seconds);
|
||||
|
||||
Observable.Interval(TimeSpan.FromSeconds(1))
|
||||
_timerSubscription = Observable.Interval(TimeSpan.FromSeconds(1))
|
||||
.Select(_ => targetExpiryTime - DateTimeOffset.Now)
|
||||
.TakeWhile(remaining => remaining.TotalSeconds > 0)
|
||||
.Subscribe(
|
||||
@@ -346,12 +375,11 @@ namespace Awake.Core
|
||||
|
||||
TrayHelper.SetShellIcon(
|
||||
TrayHelper.WindowHandle,
|
||||
$"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}][{remainingTimeSpan.ToHumanReadableString()}]",
|
||||
$"{Constants.FullAppName}\n{remainingTimeSpan.ToHumanReadableString()} {Resources.AWAKE_TRAY_REMAINING}\n{Resources.AWAKE_TRAY_DISPLAY}: {ScreenStateString}",
|
||||
TrayHelper.TimedIcon,
|
||||
TrayIconAction.Update);
|
||||
},
|
||||
() => HandleTimerCompletion("timed"),
|
||||
_tokenSource.Token);
|
||||
() => HandleTimerCompletion("timed"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -384,6 +412,16 @@ namespace Awake.Core
|
||||
{
|
||||
SetPassiveKeepAwake(updateSettings: false);
|
||||
|
||||
// Stop the monitor thread gracefully
|
||||
StopMonitor();
|
||||
|
||||
// Dispose the timer subscription
|
||||
_timerSubscription?.Dispose();
|
||||
_timerSubscription = null;
|
||||
|
||||
// Dispose tray icons
|
||||
TrayHelper.DisposeIcons();
|
||||
|
||||
if (TrayHelper.WindowHandle != IntPtr.Zero)
|
||||
{
|
||||
// Delete the icon.
|
||||
@@ -496,15 +534,21 @@ namespace Awake.Core
|
||||
AwakeSettings currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
|
||||
currentSettings.Properties.KeepDisplayOn = !currentSettings.Properties.KeepDisplayOn;
|
||||
|
||||
// We want to make sure that if the display setting changes (e.g., through the tray)
|
||||
// then we do not reset the counter from zero. Because the settings are only storing
|
||||
// hours and minutes, we round up the minutes value up when changes occur.
|
||||
// For TIMED mode: update state directly without restarting timer
|
||||
// This preserves the existing timer Observable subscription and targetExpiryTime
|
||||
if (CurrentOperatingMode == AwakeMode.TIMED && TimeRemaining > 0)
|
||||
{
|
||||
TimeSpan timeSpan = TimeSpan.FromSeconds(TimeRemaining);
|
||||
// Update internal state
|
||||
IsDisplayOn = currentSettings.Properties.KeepDisplayOn;
|
||||
|
||||
currentSettings.Properties.IntervalHours = (uint)timeSpan.TotalHours;
|
||||
currentSettings.Properties.IntervalMinutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60);
|
||||
// Update execution state without canceling timer
|
||||
_stateQueue.Add(ComputeAwakeState(IsDisplayOn));
|
||||
|
||||
// Save settings - ProcessSettings will skip reinitialization
|
||||
// since we're already in TIMED mode
|
||||
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
|
||||
|
||||
@@ -15,6 +15,12 @@ namespace Awake.Core.Native
|
||||
internal const int WM_DESTROY = 0x0002;
|
||||
internal const int WM_LBUTTONDOWN = 0x0201;
|
||||
internal const int WM_RBUTTONDOWN = 0x0204;
|
||||
internal const uint WM_POWERBROADCAST = 0x0218;
|
||||
|
||||
// Power Broadcast Event Types
|
||||
internal const int PBT_APMRESUMEAUTOMATIC = 0x0012;
|
||||
internal const int PBT_APMRESUMESUSPEND = 0x0007;
|
||||
internal const int PBT_APMPOWERSTATUSCHANGE = 0x000A;
|
||||
|
||||
// Menu Flags
|
||||
internal const uint MF_BYPOSITION = 1024;
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Awake.Core.Threading
|
||||
{
|
||||
internal sealed class SingleThreadSynchronizationContext : SynchronizationContext
|
||||
{
|
||||
private readonly Queue<Tuple<SendOrPostCallback, object?>?> queue = new();
|
||||
private readonly Queue<(SendOrPostCallback Callback, object? State)?> queue = new();
|
||||
|
||||
public override void Post(SendOrPostCallback d, object? state)
|
||||
{
|
||||
@@ -19,7 +19,7 @@ namespace Awake.Core.Threading
|
||||
|
||||
lock (queue)
|
||||
{
|
||||
queue.Enqueue(Tuple.Create(d, state));
|
||||
queue.Enqueue((d, state));
|
||||
Monitor.Pulse(queue);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ namespace Awake.Core.Threading
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Tuple<SendOrPostCallback, object?>? work;
|
||||
(SendOrPostCallback Callback, object? State)? work;
|
||||
lock (queue)
|
||||
{
|
||||
while (queue.Count == 0)
|
||||
@@ -46,7 +46,7 @@ namespace Awake.Core.Threading
|
||||
|
||||
try
|
||||
{
|
||||
work.Item1(work.Item2);
|
||||
work.Value.Callback(work.Value.State);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -45,12 +45,26 @@ namespace Awake.Core
|
||||
internal static readonly Icon IndefiniteIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/indefinite.ico"));
|
||||
internal static readonly Icon DisabledIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/disabled.ico"));
|
||||
|
||||
private const int TrayIconId = 1000;
|
||||
|
||||
static TrayHelper()
|
||||
{
|
||||
TrayMenu = IntPtr.Zero;
|
||||
WindowHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of all icon resources to prevent GDI handle leaks.
|
||||
/// </summary>
|
||||
internal static void DisposeIcons()
|
||||
{
|
||||
DefaultAwakeIcon?.Dispose();
|
||||
TimedIcon?.Dispose();
|
||||
ExpirableIcon?.Dispose();
|
||||
IndefiniteIcon?.Dispose();
|
||||
DisabledIcon?.Dispose();
|
||||
}
|
||||
|
||||
private static void ShowContextMenu(IntPtr hWnd)
|
||||
{
|
||||
if (TrayMenu == IntPtr.Zero)
|
||||
@@ -172,7 +186,11 @@ namespace Awake.Core
|
||||
|
||||
internal static void SetShellIcon(IntPtr hWnd, string text, Icon? icon, TrayIconAction action = TrayIconAction.Add, [CallerMemberName] string callerName = "")
|
||||
{
|
||||
if (hWnd != IntPtr.Zero && icon != null)
|
||||
// For Delete operations, we don't need an icon - only hWnd is required
|
||||
// For Add/Update operations, we need both hWnd and icon
|
||||
bool canProceed = hWnd != IntPtr.Zero && (action == TrayIconAction.Delete || icon != null);
|
||||
|
||||
if (canProceed)
|
||||
{
|
||||
int message = Native.Constants.NIM_ADD;
|
||||
|
||||
@@ -195,7 +213,7 @@ namespace Awake.Core
|
||||
{
|
||||
CbSize = Marshal.SizeOf<NotifyIconData>(),
|
||||
HWnd = hWnd,
|
||||
UId = 1000,
|
||||
UId = TrayIconId,
|
||||
UFlags = Native.Constants.NIF_ICON | Native.Constants.NIF_TIP | Native.Constants.NIF_MESSAGE,
|
||||
UCallbackMessage = (int)Native.Constants.WM_USER,
|
||||
HIcon = icon?.Handle ?? IntPtr.Zero,
|
||||
@@ -208,29 +226,54 @@ namespace Awake.Core
|
||||
{
|
||||
CbSize = Marshal.SizeOf<NotifyIconData>(),
|
||||
HWnd = hWnd,
|
||||
UId = 1000,
|
||||
UId = TrayIconId,
|
||||
UFlags = 0,
|
||||
};
|
||||
}
|
||||
|
||||
for (int attempt = 1; attempt <= 3; attempt++)
|
||||
// Retry configuration based on action type
|
||||
// Add operations need longer delays as Explorer may still be initializing after Windows updates
|
||||
int maxRetryAttempts;
|
||||
int baseDelayMs;
|
||||
|
||||
if (action == TrayIconAction.Add)
|
||||
{
|
||||
maxRetryAttempts = 10;
|
||||
baseDelayMs = 500; // 500, 1000, 2000, 2000, 2000... (capped)
|
||||
}
|
||||
else
|
||||
{
|
||||
maxRetryAttempts = 3;
|
||||
baseDelayMs = 100; // 100, 200, 400 (existing behavior)
|
||||
}
|
||||
|
||||
const int maxDelayMs = 2000; // Cap delay at 2 seconds
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetryAttempts; attempt++)
|
||||
{
|
||||
if (Bridge.Shell_NotifyIcon(message, ref _notifyIconData))
|
||||
{
|
||||
if (attempt > 1)
|
||||
{
|
||||
Logger.LogInfo($"Successfully set shell icon on attempt {attempt}. Action: {action}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
int errorCode = Marshal.GetLastWin32Error();
|
||||
Logger.LogInfo($"Could not set the shell icon. Action: {action}, error code: {errorCode}. HIcon handle is {icon?.Handle} and HWnd is {hWnd}. Invoked by {callerName}.");
|
||||
Logger.LogInfo($"Could not set the shell icon. Action: {action}, error code: {errorCode}, attempt: {attempt}/{maxRetryAttempts}. HIcon handle is {icon?.Handle} and HWnd is {hWnd}. Invoked by {callerName}.");
|
||||
|
||||
if (attempt == 3)
|
||||
if (attempt == maxRetryAttempts)
|
||||
{
|
||||
Logger.LogError($"Failed to change tray icon after 3 attempts. Action: {action} and error code: {errorCode}. Invoked by {callerName}.");
|
||||
Logger.LogError($"Failed to change tray icon after {maxRetryAttempts} attempts. Action: {action} and error code: {errorCode}. Invoked by {callerName}.");
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(100);
|
||||
// Exponential backoff with cap
|
||||
int delayMs = Math.Min(baseDelayMs * (1 << (attempt - 1)), maxDelayMs);
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +284,7 @@ namespace Awake.Core
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo($"Cannot set the shell icon - parent window handle is zero or icon is not available. Text: {text} Action: {action}");
|
||||
Logger.LogInfo($"Cannot set the shell icon - parent window handle is zero{(action != TrayIconAction.Delete && icon == null ? " or icon is not available" : string.Empty)}. Text: {text} Action: {action}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,11 +323,9 @@ namespace Awake.Core
|
||||
Bridge.PostQuitMessage(0);
|
||||
break;
|
||||
case Native.Constants.WM_COMMAND:
|
||||
int trayCommandsSize = Enum.GetNames<TrayCommands>().Length;
|
||||
long targetCommandValue = wParam.ToInt64() & 0xFFFF;
|
||||
|
||||
long targetCommandIndex = wParam.ToInt64() & 0xFFFF;
|
||||
|
||||
switch (targetCommandIndex)
|
||||
switch (targetCommandValue)
|
||||
{
|
||||
case (uint)TrayCommands.TC_EXIT:
|
||||
{
|
||||
@@ -300,7 +341,7 @@ namespace Awake.Core
|
||||
|
||||
case (uint)TrayCommands.TC_MODE_INDEFINITE:
|
||||
{
|
||||
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
|
||||
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
|
||||
Manager.SetIndefiniteKeepAwake(keepDisplayOn: settings.Properties.KeepDisplayOn);
|
||||
break;
|
||||
}
|
||||
@@ -313,23 +354,43 @@ namespace Awake.Core
|
||||
|
||||
default:
|
||||
{
|
||||
if (targetCommandIndex >= trayCommandsSize)
|
||||
// Custom tray time commands start at TC_TIME and increment by 1 for each entry.
|
||||
// Check if this command falls within the custom time range.
|
||||
if (targetCommandValue >= (uint)TrayCommands.TC_TIME)
|
||||
{
|
||||
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
|
||||
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
|
||||
if (settings.Properties.CustomTrayTimes.Count == 0)
|
||||
{
|
||||
settings.Properties.CustomTrayTimes.AddRange(Manager.GetDefaultTrayOptions());
|
||||
}
|
||||
|
||||
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
|
||||
uint targetTime = settings.Properties.CustomTrayTimes.ElementAt(index).Value;
|
||||
Manager.SetTimedKeepAwake(targetTime, keepDisplayOn: settings.Properties.KeepDisplayOn);
|
||||
int index = (int)targetCommandValue - (int)TrayCommands.TC_TIME;
|
||||
|
||||
if (index >= 0 && index < settings.Properties.CustomTrayTimes.Count)
|
||||
{
|
||||
uint targetTime = settings.Properties.CustomTrayTimes.Values.Skip(index).First();
|
||||
Manager.SetTimedKeepAwake(targetTime, keepDisplayOn: settings.Properties.KeepDisplayOn);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Custom tray time index {index} is out of range. Available entries: {settings.Properties.CustomTrayTimes.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case Native.Constants.WM_POWERBROADCAST:
|
||||
int eventType = wParam.ToInt32();
|
||||
if (eventType == Native.Constants.PBT_APMRESUMEAUTOMATIC ||
|
||||
eventType == Native.Constants.PBT_APMRESUMESUSPEND ||
|
||||
eventType == Native.Constants.PBT_APMPOWERSTATUSCHANGE)
|
||||
{
|
||||
Manager.ReapplyAwakeState();
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
if (message == _taskbarCreatedMessage)
|
||||
@@ -357,7 +418,7 @@ namespace Awake.Core
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Error: " + e.Message);
|
||||
Logger.LogError($"Error in tray thread execution: {e.Message}");
|
||||
}
|
||||
},
|
||||
null);
|
||||
@@ -439,9 +500,11 @@ namespace Awake.Core
|
||||
private static void CreateAwakeTimeSubMenu(Dictionary<string, uint> trayTimeShortcuts, bool isChecked = false)
|
||||
{
|
||||
nint awakeTimeMenu = Bridge.CreatePopupMenu();
|
||||
for (int i = 0; i < trayTimeShortcuts.Count; i++)
|
||||
int i = 0;
|
||||
foreach (var shortcut in trayTimeShortcuts)
|
||||
{
|
||||
Bridge.InsertMenu(awakeTimeMenu, (uint)i, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, trayTimeShortcuts.ElementAt(i).Key);
|
||||
Bridge.InsertMenu(awakeTimeMenu, (uint)i, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, shortcut.Key);
|
||||
i++;
|
||||
}
|
||||
|
||||
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_POPUP | (isChecked ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)awakeTimeMenu, Resources.AWAKE_KEEP_ON_INTERVAL);
|
||||
|
||||
@@ -39,18 +39,20 @@ namespace Awake
|
||||
|
||||
private static FileSystemWatcher? _watcher;
|
||||
private static SettingsUtils? _settingsUtils;
|
||||
private static EventWaitHandle? _exitEventHandle;
|
||||
private static RegisteredWaitHandle? _registeredWaitHandle;
|
||||
|
||||
private static bool _startedFromPowerToys;
|
||||
|
||||
public static Mutex? LockMutex { get; set; }
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
private static ConsoleEventHandler _handler;
|
||||
private static ConsoleEventHandler? _handler;
|
||||
private static SystemPowerCapabilities _powerCapabilities;
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger(Path.Combine("\\", Core.Constants.AppName, "Logs"));
|
||||
|
||||
var rootCommand = BuildRootCommand();
|
||||
|
||||
Bridge.AttachConsole(Core.Native.Constants.ATTACH_PARENT_PROCESS);
|
||||
@@ -73,8 +75,6 @@ namespace Awake
|
||||
|
||||
LockMutex = new Mutex(true, Core.Constants.AppName, out bool instantiated);
|
||||
|
||||
Logger.InitializeLogger(Path.Combine("\\", Core.Constants.AppName, "Logs"));
|
||||
|
||||
try
|
||||
{
|
||||
string appLanguage = LanguageHelper.LoadLanguage();
|
||||
@@ -140,7 +140,7 @@ namespace Awake
|
||||
IsRequired = false,
|
||||
};
|
||||
|
||||
Option<bool> displayOption = new(_aliasesDisplayOption, () => true, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION)
|
||||
Option<bool> displayOption = new(_aliasesDisplayOption, () => false, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION)
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
@@ -235,10 +235,23 @@ namespace Awake
|
||||
private static void Exit(string message, int exitCode)
|
||||
{
|
||||
_etwTrace?.Dispose();
|
||||
DisposeFileSystemWatcher();
|
||||
_registeredWaitHandle?.Unregister(null);
|
||||
_exitEventHandle?.Dispose();
|
||||
Logger.LogInfo(message);
|
||||
Manager.CompleteExit(exitCode);
|
||||
}
|
||||
|
||||
private static void DisposeFileSystemWatcher()
|
||||
{
|
||||
if (_watcher != null)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
_watcher.Dispose();
|
||||
_watcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ProcessExists(int processId)
|
||||
{
|
||||
if (processId <= 0)
|
||||
@@ -252,8 +265,15 @@ namespace Awake
|
||||
using var p = Process.GetProcessById(processId);
|
||||
return !p.HasExited;
|
||||
}
|
||||
catch
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Process with the specified ID is not running
|
||||
return false;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// Process has exited or cannot be accessed
|
||||
Logger.LogInfo($"Process {processId} cannot be accessed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -282,12 +302,13 @@ namespace Awake
|
||||
// Start the monitor thread that will be used to track the current state.
|
||||
Manager.StartMonitor();
|
||||
|
||||
EventWaitHandle eventHandle = new(false, EventResetMode.ManualReset, PowerToys.Interop.Constants.AwakeExitEvent());
|
||||
new Thread(() =>
|
||||
{
|
||||
WaitHandle.WaitAny([eventHandle]);
|
||||
Exit(Resources.AWAKE_EXIT_SIGNAL_MESSAGE, 0);
|
||||
}).Start();
|
||||
_exitEventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, PowerToys.Interop.Constants.AwakeExitEvent());
|
||||
_registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(
|
||||
_exitEventHandle,
|
||||
(state, timedOut) => Exit(Resources.AWAKE_EXIT_SIGNAL_MESSAGE, 0),
|
||||
null,
|
||||
Timeout.Infinite,
|
||||
executeOnlyOnce: true);
|
||||
|
||||
if (usePtConfig)
|
||||
{
|
||||
@@ -432,7 +453,7 @@ namespace Awake
|
||||
{
|
||||
Manager.AllocateConsole();
|
||||
|
||||
_handler += new ConsoleEventHandler(ExitHandler);
|
||||
_handler = new ConsoleEventHandler(ExitHandler);
|
||||
Manager.SetConsoleControlHandler(_handler, true);
|
||||
|
||||
Trace.Listeners.Add(new ConsoleTraceListener());
|
||||
@@ -528,6 +549,11 @@ namespace Awake
|
||||
{
|
||||
settings.Properties.ExpirationDateTime = DateTimeOffset.Now.AddMinutes(5);
|
||||
_settingsUtils.SaveSettings(JsonSerializer.Serialize(settings), Core.Constants.AppName);
|
||||
|
||||
// Return here - the FileSystemWatcher will re-trigger ProcessSettings
|
||||
// with the corrected expiration time, which will then call SetExpirableKeepAwake.
|
||||
// This matches the pattern used by mode setters (e.g., SetExpirableKeepAwake line 292).
|
||||
return;
|
||||
}
|
||||
|
||||
Manager.SetExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn);
|
||||
|
||||
@@ -60,15 +60,6 @@ namespace Awake.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Checked.
|
||||
/// </summary>
|
||||
internal static string AWAKE_CHECKED {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_CHECKED", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Specifies whether Awake will be using the PowerToys configuration file for managing the state..
|
||||
/// </summary>
|
||||
@@ -240,42 +231,6 @@ namespace Awake.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to d.
|
||||
/// </summary>
|
||||
internal static string AWAKE_LABEL_DAYS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_LABEL_DAYS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to h.
|
||||
/// </summary>
|
||||
internal static string AWAKE_LABEL_HOURS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_LABEL_HOURS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to m.
|
||||
/// </summary>
|
||||
internal static string AWAKE_LABEL_MINUTES {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_LABEL_MINUTES", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to s.
|
||||
/// </summary>
|
||||
internal static string AWAKE_LABEL_SECONDS {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_LABEL_SECONDS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} minute.
|
||||
/// </summary>
|
||||
@@ -320,7 +275,16 @@ namespace Awake.Properties {
|
||||
return ResourceManager.GetString("AWAKE_SCREEN_ON", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Screen.
|
||||
/// </summary>
|
||||
internal static string AWAKE_TRAY_DISPLAY {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_DISPLAY", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Expiring.
|
||||
/// </summary>
|
||||
@@ -329,7 +293,7 @@ namespace Awake.Properties {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_TEXT_EXPIRATION", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Indefinite.
|
||||
/// </summary>
|
||||
@@ -338,7 +302,7 @@ namespace Awake.Properties {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_TEXT_INDEFINITE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passive.
|
||||
/// </summary>
|
||||
@@ -347,31 +311,31 @@ namespace Awake.Properties {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_TEXT_OFF", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bound to.
|
||||
/// </summary>
|
||||
internal static string AWAKE_TRAY_TEXT_PID_BINDING {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_TEXT_PID_BINDING", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Interval.
|
||||
/// Looks up a localized string similar to Timed.
|
||||
/// </summary>
|
||||
internal static string AWAKE_TRAY_TEXT_TIMED {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_TEXT_TIMED", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unchecked.
|
||||
/// Looks up a localized string similar to Until.
|
||||
/// </summary>
|
||||
internal static string AWAKE_UNCHECKED {
|
||||
internal static string AWAKE_TRAY_UNTIL {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_UNCHECKED", resourceCulture);
|
||||
return ResourceManager.GetString("AWAKE_TRAY_UNTIL", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to remaining.
|
||||
/// </summary>
|
||||
internal static string AWAKE_TRAY_REMAINING {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_REMAINING", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +117,6 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AWAKE_CHECKED" xml:space="preserve">
|
||||
<value>Checked</value>
|
||||
</data>
|
||||
<data name="AWAKE_EXIT" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
</data>
|
||||
@@ -158,9 +155,6 @@
|
||||
<value>Off (keep using the selected power plan)</value>
|
||||
<comment>Don't keep the system awake, use the selected system power plan</comment>
|
||||
</data>
|
||||
<data name="AWAKE_UNCHECKED" xml:space="preserve">
|
||||
<value>Unchecked</value>
|
||||
</data>
|
||||
<data name="AWAKE_CMD_HELP_CONFIG_OPTION" xml:space="preserve">
|
||||
<value>Specifies whether Awake will be using the PowerToys configuration file for managing the state.</value>
|
||||
</data>
|
||||
@@ -195,31 +189,11 @@
|
||||
<value>Passive</value>
|
||||
</data>
|
||||
<data name="AWAKE_TRAY_TEXT_TIMED" xml:space="preserve">
|
||||
<value>Interval</value>
|
||||
</data>
|
||||
<data name="AWAKE_LABEL_DAYS" xml:space="preserve">
|
||||
<value>d</value>
|
||||
<comment>Used to display number of days in the system tray tooltip.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_LABEL_HOURS" xml:space="preserve">
|
||||
<value>h</value>
|
||||
<comment>Used to display number of hours in the system tray tooltip.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_LABEL_MINUTES" xml:space="preserve">
|
||||
<value>m</value>
|
||||
<comment>Used to display number of minutes in the system tray tooltip.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_LABEL_SECONDS" xml:space="preserve">
|
||||
<value>s</value>
|
||||
<comment>Used to display number of seconds in the system tray tooltip.</comment>
|
||||
<value>Timed</value>
|
||||
</data>
|
||||
<data name="AWAKE_CMD_PARENT_PID_OPTION" xml:space="preserve">
|
||||
<value>Uses the parent process as the bound target - once the process terminates, Awake stops.</value>
|
||||
</data>
|
||||
<data name="AWAKE_TRAY_TEXT_PID_BINDING" xml:space="preserve">
|
||||
<value>Bound to</value>
|
||||
<comment>Describes the process ID Awake is bound to when running.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_SCREEN_ON" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
@@ -235,4 +209,16 @@
|
||||
<data name="AWAKE_EXIT_BIND_TO_SELF_FAILURE_MESSAGE" xml:space="preserve">
|
||||
<value>Exiting because the provided process ID is Awake's own.</value>
|
||||
</data>
|
||||
<data name="AWAKE_TRAY_DISPLAY" xml:space="preserve">
|
||||
<value>Screen</value>
|
||||
<comment>Label for the screen/display line in tray tooltip.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_TRAY_UNTIL" xml:space="preserve">
|
||||
<value>Until</value>
|
||||
<comment>Label for expiration mode showing end date/time.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_TRAY_REMAINING" xml:space="preserve">
|
||||
<value>remaining</value>
|
||||
<comment>Suffix for timed mode showing time remaining, e.g. "1:30:00 remaining".</comment>
|
||||
</data>
|
||||
</root>
|
||||
168
src/modules/awake/README.md
Normal file
168
src/modules/awake/README.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# PowerToys Awake Module
|
||||
|
||||
A PowerToys utility that prevents Windows from sleeping and/or turning off the display.
|
||||
|
||||
**Author:** [Den Delimarsky](https://den.dev)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Awake Website](https://awake.den.dev) - Official documentation and guides
|
||||
- [Microsoft Learn Documentation](https://learn.microsoft.com/windows/powertoys/awake) - Usage instructions and feature overview
|
||||
- [GitHub Issues](https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+label%3AProduct-Awake) - Report bugs or request features
|
||||
|
||||
## Overview
|
||||
|
||||
The Awake module consists of three projects:
|
||||
|
||||
| Project | Purpose |
|
||||
|---------|---------|
|
||||
| `Awake/` | Main WinExe application with CLI support |
|
||||
| `Awake.ModuleServices/` | Service layer for PowerToys integration |
|
||||
| `AwakeModuleInterface/` | C++ native module bridge |
|
||||
|
||||
## How It Works
|
||||
|
||||
The module uses the Win32 `SetThreadExecutionState()` API to signal Windows that the system should remain awake:
|
||||
|
||||
- `ES_SYSTEM_REQUIRED` - Prevents system sleep
|
||||
- `ES_DISPLAY_REQUIRED` - Prevents display sleep
|
||||
- `ES_CONTINUOUS` - Maintains state until explicitly changed
|
||||
|
||||
## Operating Modes
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| **PASSIVE** | Normal power behavior (off) |
|
||||
| **INDEFINITE** | Keep awake until manually stopped |
|
||||
| **TIMED** | Keep awake for a specified duration |
|
||||
| **EXPIRABLE** | Keep awake until a specific date/time |
|
||||
|
||||
## Command-Line Usage
|
||||
|
||||
Awake can be run standalone with the following options:
|
||||
|
||||
```
|
||||
PowerToys.Awake.exe [options]
|
||||
|
||||
Options:
|
||||
-c, --use-pt-config Use PowerToys configuration file
|
||||
-d, --display-on Keep display on (default: false)
|
||||
-t, --time-limit Time limit in seconds
|
||||
-p, --pid Process ID to bind to
|
||||
-e, --expire-at Expiration date/time
|
||||
-u, --use-parent-pid Bind to parent process
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
Keep system awake indefinitely:
|
||||
```powershell
|
||||
PowerToys.Awake.exe
|
||||
```
|
||||
|
||||
Keep awake for 1 hour with display on:
|
||||
```powershell
|
||||
PowerToys.Awake.exe --time-limit 3600 --display-on
|
||||
```
|
||||
|
||||
Keep awake until a specific time:
|
||||
```powershell
|
||||
PowerToys.Awake.exe --expire-at "2024-12-31 23:59:59"
|
||||
```
|
||||
|
||||
Keep awake while another process is running:
|
||||
```powershell
|
||||
PowerToys.Awake.exe --pid 1234
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Design Highlights
|
||||
|
||||
1. **Pure Win32 API for Tray UI** - No WPF/WinForms dependencies, keeping the binary small. Uses direct `Shell_NotifyIcon` API for tray icon management.
|
||||
|
||||
2. **Reactive Extensions (Rx.NET)** - Used for timed operations via `Observable.Interval()` and `Observable.Timer()`. File system watching uses 25ms throttle to debounce rapid config changes.
|
||||
|
||||
3. **Custom SynchronizationContext** - Queue-based message dispatch ensures tray operations run on a dedicated thread for thread-safe UI updates.
|
||||
|
||||
4. **Dual-Mode Operation**
|
||||
- Standalone: Command-line arguments only
|
||||
- Integrated: PowerToys settings file + process binding
|
||||
|
||||
5. **Process Binding** - The `--pid` parameter keeps the system awake only while a target process runs, with auto-exit when the parent PowerToys runner terminates.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Program.cs` | Entry point & CLI parsing |
|
||||
| `Core/Manager.cs` | State orchestration & power management |
|
||||
| `Core/TrayHelper.cs` | System tray UI management |
|
||||
| `Core/Native/Bridge.cs` | Win32 P/Invoke declarations |
|
||||
| `Core/Threading/SingleThreadSynchronizationContext.cs` | Threading utilities |
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Visual Studio 2022 with C++ and .NET workloads
|
||||
- Windows SDK 10.0.26100.0 or later
|
||||
|
||||
### Build Commands
|
||||
|
||||
From the `src/modules/awake` directory:
|
||||
|
||||
```powershell
|
||||
# Using the build script
|
||||
.\scripts\Build-Awake.ps1
|
||||
|
||||
# Or with specific configuration
|
||||
.\scripts\Build-Awake.ps1 -Configuration Debug -Platform x64
|
||||
```
|
||||
|
||||
Or using MSBuild directly:
|
||||
|
||||
```powershell
|
||||
msbuild Awake\Awake.csproj /p:Configuration=Release /p:Platform=x64
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **System.CommandLine** - Command-line parsing
|
||||
- **System.Reactive** - Rx.NET for timer management
|
||||
- **PowerToys.ManagedCommon** - Shared PowerToys utilities
|
||||
- **PowerToys.Settings.UI.Lib** - Settings integration
|
||||
- **PowerToys.Interop** - Native interop layer
|
||||
|
||||
## Configuration
|
||||
|
||||
When running with PowerToys (`--use-pt-config`), settings are stored in:
|
||||
```
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\Awake\settings.json
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### Task Scheduler Idle Detection ([#44134](https://github.com/microsoft/PowerToys/issues/44134))
|
||||
|
||||
When "Keep display on" is enabled, Awake uses the `ES_DISPLAY_REQUIRED` flag which blocks Windows Task Scheduler from detecting the system as idle. This prevents scheduled maintenance tasks (like SSD TRIM, disk defragmentation, and other idle-triggered tasks) from running.
|
||||
|
||||
Per [Microsoft's documentation](https://learn.microsoft.com/en-us/windows/win32/taskschd/task-idle-conditions):
|
||||
|
||||
> "An exception would be for any presentation type application that sets the ES_DISPLAY_REQUIRED flag. This flag forces Task Scheduler to not consider the system as being idle, regardless of user activity or resource consumption."
|
||||
|
||||
**Workarounds:**
|
||||
|
||||
1. **Disable "Keep display on"** - With this setting off, Awake only uses `ES_SYSTEM_REQUIRED` which still prevents sleep but allows Task Scheduler to detect idle state.
|
||||
|
||||
2. **Manually run maintenance tasks** - For example, to run TRIM manually:
|
||||
```powershell
|
||||
# Run as Administrator
|
||||
Optimize-Volume -DriveLetter C -ReTrim -Verbose
|
||||
```
|
||||
|
||||
## Telemetry
|
||||
|
||||
The module emits telemetry events for:
|
||||
- Keep-awake mode changes (indefinite, timed, expirable, passive)
|
||||
- Privacy-compliant event tagging via `Microsoft.PowerToys.Telemetry`
|
||||
456
src/modules/awake/scripts/Build-Awake.ps1
Normal file
456
src/modules/awake/scripts/Build-Awake.ps1
Normal file
@@ -0,0 +1,456 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the PowerToys Awake module.
|
||||
|
||||
.DESCRIPTION
|
||||
This script builds the Awake module and its dependencies using MSBuild.
|
||||
It automatically locates the Visual Studio installation and uses the
|
||||
appropriate MSBuild version.
|
||||
|
||||
.PARAMETER Configuration
|
||||
The build configuration. Valid values are 'Debug' or 'Release'.
|
||||
Default: Release
|
||||
|
||||
.PARAMETER Platform
|
||||
The target platform. Valid values are 'x64' or 'ARM64'.
|
||||
Default: x64
|
||||
|
||||
.PARAMETER Clean
|
||||
If specified, cleans the build output before building.
|
||||
|
||||
.PARAMETER Restore
|
||||
If specified, restores NuGet packages before building.
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Awake.ps1
|
||||
Builds Awake in Release configuration for x64.
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Awake.ps1 -Configuration Debug
|
||||
Builds Awake in Debug configuration for x64.
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Awake.ps1 -Clean -Restore
|
||||
Cleans, restores packages, and builds Awake.
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Awake.ps1 -Platform ARM64
|
||||
Builds Awake for ARM64 architecture.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Debug', 'Release')]
|
||||
[string]$Configuration = 'Release',
|
||||
|
||||
[ValidateSet('x64', 'ARM64')]
|
||||
[string]$Platform = 'x64',
|
||||
|
||||
[switch]$Clean,
|
||||
|
||||
[switch]$Restore
|
||||
)
|
||||
|
||||
# Force UTF-8 output for Unicode characters
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$script:StartTime = Get-Date
|
||||
|
||||
# Get script directory and project paths
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ModuleDir = Split-Path -Parent $ScriptDir
|
||||
$RepoRoot = Resolve-Path (Join-Path $ModuleDir "..\..\..") | Select-Object -ExpandProperty Path
|
||||
$AwakeProject = Join-Path $ModuleDir "Awake\Awake.csproj"
|
||||
$ModuleServicesProject = Join-Path $ModuleDir "Awake.ModuleServices\Awake.ModuleServices.csproj"
|
||||
|
||||
# ============================================================================
|
||||
# Modern UI Components
|
||||
# ============================================================================
|
||||
|
||||
$script:Colors = @{
|
||||
Primary = "Cyan"
|
||||
Success = "Green"
|
||||
Error = "Red"
|
||||
Warning = "Yellow"
|
||||
Muted = "DarkGray"
|
||||
Accent = "Magenta"
|
||||
White = "White"
|
||||
}
|
||||
|
||||
# Box drawing characters (not emojis)
|
||||
$script:UI = @{
|
||||
BoxH = [char]0x2500 # Horizontal line
|
||||
BoxV = [char]0x2502 # Vertical line
|
||||
BoxTL = [char]0x256D # Top-left corner (rounded)
|
||||
BoxTR = [char]0x256E # Top-right corner (rounded)
|
||||
BoxBL = [char]0x2570 # Bottom-left corner (rounded)
|
||||
BoxBR = [char]0x256F # Bottom-right corner (rounded)
|
||||
TreeL = [char]0x2514 # Tree last item
|
||||
TreeT = [char]0x251C # Tree item
|
||||
}
|
||||
|
||||
# Braille spinner frames (the npm-style spinner)
|
||||
$script:SpinnerFrames = @(
|
||||
[char]0x280B, # ⠋
|
||||
[char]0x2819, # ⠙
|
||||
[char]0x2839, # ⠹
|
||||
[char]0x2838, # ⠸
|
||||
[char]0x283C, # ⠼
|
||||
[char]0x2834, # ⠴
|
||||
[char]0x2826, # ⠦
|
||||
[char]0x2827, # ⠧
|
||||
[char]0x2807, # ⠇
|
||||
[char]0x280F # ⠏
|
||||
)
|
||||
|
||||
function Get-ElapsedTime {
|
||||
$elapsed = (Get-Date) - $script:StartTime
|
||||
if ($elapsed.TotalSeconds -lt 60) {
|
||||
return "$([math]::Round($elapsed.TotalSeconds, 1))s"
|
||||
} else {
|
||||
return "$([math]::Floor($elapsed.TotalMinutes))m $($elapsed.Seconds)s"
|
||||
}
|
||||
}
|
||||
|
||||
function Write-Header {
|
||||
Write-Host ""
|
||||
Write-Host " Awake Build" -ForegroundColor $Colors.White
|
||||
Write-Host " $Platform / $Configuration" -ForegroundColor $Colors.Muted
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Write-Phase {
|
||||
param([string]$Name)
|
||||
Write-Host ""
|
||||
Write-Host " $Name" -ForegroundColor $Colors.Accent
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Write-Task {
|
||||
param([string]$Name, [switch]$Last)
|
||||
$tree = if ($Last) { $UI.TreeL } else { $UI.TreeT }
|
||||
Write-Host " $tree$($UI.BoxH)$($UI.BoxH) " -NoNewline -ForegroundColor $Colors.Muted
|
||||
Write-Host $Name -NoNewline -ForegroundColor $Colors.White
|
||||
}
|
||||
|
||||
function Write-TaskStatus {
|
||||
param([string]$Status, [string]$Time, [switch]$Failed)
|
||||
if ($Failed) {
|
||||
Write-Host " FAIL" -ForegroundColor $Colors.Error
|
||||
} else {
|
||||
Write-Host " " -NoNewline
|
||||
Write-Host $Status -NoNewline -ForegroundColor $Colors.Success
|
||||
if ($Time) {
|
||||
Write-Host " ($Time)" -ForegroundColor $Colors.Muted
|
||||
} else {
|
||||
Write-Host ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Write-BuildTree {
|
||||
param([string[]]$Items)
|
||||
$count = $Items.Count
|
||||
for ($i = 0; $i -lt $count; $i++) {
|
||||
$isLast = ($i -eq $count - 1)
|
||||
$tree = if ($isLast) { $UI.TreeL } else { $UI.TreeT }
|
||||
Write-Host " $tree$($UI.BoxH) " -NoNewline -ForegroundColor $Colors.Muted
|
||||
Write-Host $Items[$i] -ForegroundColor $Colors.Muted
|
||||
}
|
||||
}
|
||||
|
||||
function Write-SuccessBox {
|
||||
param([string]$Time, [string]$Output, [string]$Size)
|
||||
|
||||
$width = 44
|
||||
$lineChar = [string]$UI.BoxH
|
||||
$line = $lineChar * ($width - 2)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " $($UI.BoxTL)$line$($UI.BoxTR)" -ForegroundColor $Colors.Success
|
||||
|
||||
# Title row
|
||||
$title = " BUILD SUCCESSFUL"
|
||||
$titlePadding = $width - 2 - $title.Length
|
||||
Write-Host " $($UI.BoxV)" -NoNewline -ForegroundColor $Colors.Success
|
||||
Write-Host $title -NoNewline -ForegroundColor $Colors.White
|
||||
Write-Host (" " * $titlePadding) -NoNewline
|
||||
Write-Host "$($UI.BoxV)" -ForegroundColor $Colors.Success
|
||||
|
||||
# Empty row
|
||||
Write-Host " $($UI.BoxV)" -NoNewline -ForegroundColor $Colors.Success
|
||||
Write-Host (" " * ($width - 2)) -NoNewline
|
||||
Write-Host "$($UI.BoxV)" -ForegroundColor $Colors.Success
|
||||
|
||||
# Time row
|
||||
$timeText = " Completed in $Time"
|
||||
$timePadding = $width - 2 - $timeText.Length
|
||||
Write-Host " $($UI.BoxV)" -NoNewline -ForegroundColor $Colors.Success
|
||||
Write-Host $timeText -NoNewline -ForegroundColor $Colors.Muted
|
||||
Write-Host (" " * $timePadding) -NoNewline
|
||||
Write-Host "$($UI.BoxV)" -ForegroundColor $Colors.Success
|
||||
|
||||
# Output row
|
||||
$outText = " Output: $Output ($Size)"
|
||||
if ($outText.Length -gt ($width - 2)) {
|
||||
$outText = $outText.Substring(0, $width - 5) + "..."
|
||||
}
|
||||
$outPadding = $width - 2 - $outText.Length
|
||||
if ($outPadding -lt 0) { $outPadding = 0 }
|
||||
Write-Host " $($UI.BoxV)" -NoNewline -ForegroundColor $Colors.Success
|
||||
Write-Host $outText -NoNewline -ForegroundColor $Colors.Muted
|
||||
Write-Host (" " * $outPadding) -NoNewline
|
||||
Write-Host "$($UI.BoxV)" -ForegroundColor $Colors.Success
|
||||
|
||||
Write-Host " $($UI.BoxBL)$line$($UI.BoxBR)" -ForegroundColor $Colors.Success
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Write-ErrorBox {
|
||||
param([string]$Message)
|
||||
|
||||
$width = 44
|
||||
$lineChar = [string]$UI.BoxH
|
||||
$line = $lineChar * ($width - 2)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " $($UI.BoxTL)$line$($UI.BoxTR)" -ForegroundColor $Colors.Error
|
||||
$title = " BUILD FAILED"
|
||||
$titlePadding = $width - 2 - $title.Length
|
||||
Write-Host " $($UI.BoxV)" -NoNewline -ForegroundColor $Colors.Error
|
||||
Write-Host $title -NoNewline -ForegroundColor $Colors.White
|
||||
Write-Host (" " * $titlePadding) -NoNewline
|
||||
Write-Host "$($UI.BoxV)" -ForegroundColor $Colors.Error
|
||||
Write-Host " $($UI.BoxBL)$line$($UI.BoxBR)" -ForegroundColor $Colors.Error
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Build Functions
|
||||
# ============================================================================
|
||||
|
||||
function Find-MSBuild {
|
||||
$vsWherePath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
|
||||
if (Test-Path $vsWherePath) {
|
||||
$vsPath = & $vsWherePath -latest -requires Microsoft.Component.MSBuild -property installationPath
|
||||
if ($vsPath) {
|
||||
$msbuildPath = Join-Path $vsPath "MSBuild\Current\Bin\MSBuild.exe"
|
||||
if (Test-Path $msbuildPath) {
|
||||
return $msbuildPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$commonPaths = @(
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe",
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe",
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe",
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe"
|
||||
)
|
||||
|
||||
foreach ($path in $commonPaths) {
|
||||
if (Test-Path $path) {
|
||||
return $path
|
||||
}
|
||||
}
|
||||
|
||||
throw "MSBuild not found. Please install Visual Studio 2022."
|
||||
}
|
||||
|
||||
function Invoke-BuildWithSpinner {
|
||||
param(
|
||||
[string]$TaskName,
|
||||
[string]$MSBuildPath,
|
||||
[string[]]$Arguments,
|
||||
[switch]$ShowProjects,
|
||||
[switch]$IsLast
|
||||
)
|
||||
|
||||
$taskStart = Get-Date
|
||||
$isInteractive = [Environment]::UserInteractive -and -not [Console]::IsOutputRedirected
|
||||
|
||||
# Only write initial task line in interactive mode (will be overwritten by spinner)
|
||||
if ($isInteractive) {
|
||||
Write-Task $TaskName -Last:$IsLast
|
||||
Write-Host " " -NoNewline
|
||||
}
|
||||
|
||||
# Start MSBuild process
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $MSBuildPath
|
||||
$psi.Arguments = $Arguments -join " "
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.WorkingDirectory = $RepoRoot
|
||||
|
||||
$process = New-Object System.Diagnostics.Process
|
||||
$process.StartInfo = $psi
|
||||
|
||||
# Collect output asynchronously
|
||||
$outputBuilder = [System.Text.StringBuilder]::new()
|
||||
$errorBuilder = [System.Text.StringBuilder]::new()
|
||||
|
||||
$outputHandler = {
|
||||
if (-not [String]::IsNullOrEmpty($EventArgs.Data)) {
|
||||
$Event.MessageData.AppendLine($EventArgs.Data)
|
||||
}
|
||||
}
|
||||
|
||||
$outputEvent = Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -Action $outputHandler -MessageData $outputBuilder
|
||||
$errorEvent = Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -Action $outputHandler -MessageData $errorBuilder
|
||||
|
||||
$process.Start() | Out-Null
|
||||
$process.BeginOutputReadLine()
|
||||
$process.BeginErrorReadLine()
|
||||
|
||||
# Animate spinner while process is running
|
||||
$frameIndex = 0
|
||||
|
||||
while (-not $process.HasExited) {
|
||||
if ($isInteractive) {
|
||||
$frame = $script:SpinnerFrames[$frameIndex]
|
||||
Write-Host "`r $($UI.TreeL)$($UI.BoxH)$($UI.BoxH) $TaskName $frame " -NoNewline
|
||||
$frameIndex = ($frameIndex + 1) % $script:SpinnerFrames.Count
|
||||
}
|
||||
Start-Sleep -Milliseconds 80
|
||||
}
|
||||
|
||||
$process.WaitForExit()
|
||||
|
||||
Unregister-Event -SourceIdentifier $outputEvent.Name
|
||||
Unregister-Event -SourceIdentifier $errorEvent.Name
|
||||
Remove-Job -Name $outputEvent.Name -Force -ErrorAction SilentlyContinue
|
||||
Remove-Job -Name $errorEvent.Name -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$exitCode = $process.ExitCode
|
||||
$output = $outputBuilder.ToString() -split "`n"
|
||||
$errors = $errorBuilder.ToString()
|
||||
|
||||
$taskElapsed = (Get-Date) - $taskStart
|
||||
$elapsed = "$([math]::Round($taskElapsed.TotalSeconds, 1))s"
|
||||
|
||||
# Write final status line
|
||||
$tree = if ($IsLast) { $UI.TreeL } else { $UI.TreeT }
|
||||
if ($isInteractive) {
|
||||
Write-Host "`r" -NoNewline
|
||||
}
|
||||
Write-Host " $tree$($UI.BoxH)$($UI.BoxH) " -NoNewline -ForegroundColor $Colors.Muted
|
||||
Write-Host $TaskName -NoNewline -ForegroundColor $Colors.White
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
Write-TaskStatus "FAIL" -Failed
|
||||
Write-Host ""
|
||||
foreach ($line in $output) {
|
||||
if ($line -match "error\s+\w+\d*:") {
|
||||
Write-Host " x $line" -ForegroundColor $Colors.Error
|
||||
}
|
||||
}
|
||||
return @{ Success = $false; Output = $output; ExitCode = $exitCode }
|
||||
}
|
||||
|
||||
Write-TaskStatus "done" $elapsed
|
||||
|
||||
# Show built projects
|
||||
if ($ShowProjects) {
|
||||
$projects = @()
|
||||
foreach ($line in $output) {
|
||||
if ($line -match "^\s*(\S+)\s+->\s+(.+)$") {
|
||||
$project = $Matches[1]
|
||||
$fileName = Split-Path $Matches[2] -Leaf
|
||||
$projects += "$project -> $fileName"
|
||||
}
|
||||
}
|
||||
if ($projects.Count -gt 0) {
|
||||
Write-BuildTree $projects
|
||||
}
|
||||
}
|
||||
|
||||
return @{ Success = $true; Output = $output; ExitCode = 0 }
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
# Verify project exists
|
||||
if (-not (Test-Path $AwakeProject)) {
|
||||
Write-Host ""
|
||||
Write-Host " x Project not found: $AwakeProject" -ForegroundColor $Colors.Error
|
||||
exit 1
|
||||
}
|
||||
|
||||
$MSBuild = Find-MSBuild
|
||||
|
||||
# Display header
|
||||
Write-Header
|
||||
|
||||
# Build arguments base
|
||||
$BaseArgs = @(
|
||||
"/p:Configuration=$Configuration",
|
||||
"/p:Platform=$Platform",
|
||||
"/v:minimal",
|
||||
"/nologo",
|
||||
"/m"
|
||||
)
|
||||
|
||||
# Clean phase
|
||||
if ($Clean) {
|
||||
Write-Phase "Cleaning"
|
||||
$cleanArgs = @($AwakeProject) + $BaseArgs + @("/t:Clean")
|
||||
$result = Invoke-BuildWithSpinner -TaskName "Build artifacts" -MSBuildPath $MSBuild -Arguments $cleanArgs -IsLast
|
||||
if (-not $result.Success) {
|
||||
Write-ErrorBox
|
||||
exit $result.ExitCode
|
||||
}
|
||||
}
|
||||
|
||||
# Restore phase
|
||||
if ($Restore) {
|
||||
Write-Phase "Restoring"
|
||||
$restoreArgs = @($AwakeProject) + $BaseArgs + @("/t:Restore")
|
||||
$result = Invoke-BuildWithSpinner -TaskName "NuGet packages" -MSBuildPath $MSBuild -Arguments $restoreArgs -IsLast
|
||||
if (-not $result.Success) {
|
||||
Write-ErrorBox
|
||||
exit $result.ExitCode
|
||||
}
|
||||
}
|
||||
|
||||
# Build phase
|
||||
Write-Phase "Building"
|
||||
|
||||
$hasModuleServices = Test-Path $ModuleServicesProject
|
||||
|
||||
# Build Awake
|
||||
$awakeArgs = @($AwakeProject) + $BaseArgs + @("/t:Build")
|
||||
$result = Invoke-BuildWithSpinner -TaskName "Awake" -MSBuildPath $MSBuild -Arguments $awakeArgs -ShowProjects -IsLast:(-not $hasModuleServices)
|
||||
if (-not $result.Success) {
|
||||
Write-ErrorBox
|
||||
exit $result.ExitCode
|
||||
}
|
||||
|
||||
# Build ModuleServices
|
||||
if ($hasModuleServices) {
|
||||
$servicesArgs = @($ModuleServicesProject) + $BaseArgs + @("/t:Build")
|
||||
$result = Invoke-BuildWithSpinner -TaskName "Awake.ModuleServices" -MSBuildPath $MSBuild -Arguments $servicesArgs -ShowProjects -IsLast
|
||||
if (-not $result.Success) {
|
||||
Write-ErrorBox
|
||||
exit $result.ExitCode
|
||||
}
|
||||
}
|
||||
|
||||
# Summary
|
||||
$OutputDir = Join-Path $RepoRoot "$Platform\$Configuration"
|
||||
$AwakeDll = Join-Path $OutputDir "PowerToys.Awake.dll"
|
||||
$elapsed = Get-ElapsedTime
|
||||
|
||||
if (Test-Path $AwakeDll) {
|
||||
$size = "$([math]::Round((Get-Item $AwakeDll).Length / 1KB, 1)) KB"
|
||||
Write-SuccessBox -Time $elapsed -Output "PowerToys.Awake.dll" -Size $size
|
||||
} else {
|
||||
Write-SuccessBox -Time $elapsed -Output $OutputDir -Size "N/A"
|
||||
}
|
||||
@@ -486,6 +486,11 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
{
|
||||
if (!item.SafeSlowInit())
|
||||
{
|
||||
// Even if initialization fails, we need to hide any previously shown details
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -547,6 +547,15 @@ public partial class MainListPage : DynamicListPage,
|
||||
// above "git" from "whatever"
|
||||
max = max + extensionTitleMatch;
|
||||
|
||||
// Apply a penalty to fallback items so they rank below direct matches.
|
||||
// Fallbacks that dynamically match queries (like RDP connections) should
|
||||
// appear after apps and direct command matches.
|
||||
if (isFallback && max > 1)
|
||||
{
|
||||
// Reduce fallback scores by 50% to prioritize direct matches
|
||||
max = max * 0.5;
|
||||
}
|
||||
|
||||
var matchSomething = max
|
||||
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
@@ -18,19 +17,41 @@ internal static class BuildInfo
|
||||
// Runtime AOT detection
|
||||
public static bool IsNativeAot => !RuntimeFeature.IsDynamicCodeSupported;
|
||||
|
||||
// From assembly metadata (build-time values)
|
||||
public static bool PublishTrimmed => GetBoolMetadata("PublishTrimmed", false);
|
||||
// build-time values
|
||||
public static bool PublishTrimmed
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_PUBLISH_TRIMMED
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// From assembly metadata (build-time values)
|
||||
public static bool PublishAot => GetBoolMetadata("PublishAot", false);
|
||||
// build-time values
|
||||
public static bool PublishAot
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_PUBLISH_AOT
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsCiBuild => GetBoolMetadata("CIBuild", false);
|
||||
|
||||
private static string? GetMetadata(string key) =>
|
||||
Assembly.GetExecutingAssembly()
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.FirstOrDefault(a => a.Key == key)?.Value;
|
||||
|
||||
private static bool GetBoolMetadata(string key, bool defaultValue) =>
|
||||
bool.TryParse(GetMetadata(key), out var result) ? result : defaultValue;
|
||||
public static bool IsCiBuild
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_CIBUILD
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- This disables the auto-generated main, so we can be single-instanced -->
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- BODGY: XES Versioning and WinAppSDK get into a fight about the app manifest, which breaks WinAppSDK. -->
|
||||
@@ -291,24 +291,15 @@
|
||||
</ItemGroup>
|
||||
<!-- </AdaptiveCardsWorkaround> -->
|
||||
|
||||
<!-- Metadata for build information -->
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>PublishTrimmed</_Parameter1>
|
||||
<_Parameter2>$(PublishTrimmed)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>PublishAot</_Parameter1>
|
||||
<_Parameter2>$(PublishAot)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CIBuild</_Parameter1>
|
||||
<_Parameter2>$(CIBuild)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CommandPaletteBranding</_Parameter1>
|
||||
<_Parameter2>$(CommandPaletteBranding)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<!-- Build information -->
|
||||
<PropertyGroup Condition=" '$(PublishAot)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_AOT</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(PublishTrimmed)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_TRIMMED</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(CIBuild)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_CIBUILD</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -372,7 +372,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Windows Command Palette</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_About_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>© 2025. All rights reserved.</value>
|
||||
<value>© 2026. All rights reserved.</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_About_GithubLink_Hyperlink.Content" xml:space="preserve">
|
||||
<value>View GitHub repository</value>
|
||||
|
||||
@@ -55,7 +55,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
|
||||
SetTimeAndDaterExtensionSearchBox("year");
|
||||
|
||||
Assert.IsNotNull(this.Find<NavigationViewItem>("2025"));
|
||||
Assert.IsNotNull(this.Find<NavigationViewItem>("2026"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -1545,13 +1545,12 @@ public class SpongebotPage : Microsoft.CommandPalette.Extensions.Toolkit.Markdow
|
||||
this.Name = "";
|
||||
this.Icon = new("https://imgflip.com/s/meme/Mocking-Spongebob.jpg");
|
||||
}
|
||||
public void IFallbackHandler.UpdateQuery(string query) {
|
||||
public void UpdateQuery(string query) {
|
||||
if (string.IsNullOrEmpty(query)) {
|
||||
this.Name = "";
|
||||
} else {
|
||||
this.Name = ConvertToAlternatingCase(query);
|
||||
}
|
||||
return Task.CompletedTask.AsAsyncCommand();
|
||||
}
|
||||
static string ConvertToAlternatingCase(string input) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -41,15 +42,19 @@ internal sealed partial class FancyZonesMonitorListItem : ListItem
|
||||
public static Details BuildMonitorDetails(FancyZonesMonitorDescriptor monitor)
|
||||
{
|
||||
var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString();
|
||||
|
||||
// Calculate physical resolution from logical pixels and DPI
|
||||
var scaleFactor = monitor.Data.Dpi > 0 ? monitor.Data.Dpi / 96.0 : 1.0;
|
||||
var physicalWidth = (int)Math.Round(monitor.Data.MonitorWidth * scaleFactor);
|
||||
var physicalHeight = (int)Math.Round(monitor.Data.MonitorHeight * scaleFactor);
|
||||
var resolution = $"{physicalWidth}\u00D7{physicalHeight}";
|
||||
|
||||
var tags = new List<IDetailsElement>
|
||||
{
|
||||
DetailTag(Resources.FancyZones_Monitor, monitor.Data.Monitor),
|
||||
DetailTag(Resources.FancyZones_Instance, monitor.Data.MonitorInstanceId),
|
||||
DetailTag(Resources.FancyZones_Serial, monitor.Data.MonitorSerialNumber),
|
||||
DetailTag(Resources.FancyZones_Number, monitor.Data.MonitorNumber.ToString(CultureInfo.InvariantCulture)),
|
||||
DetailTag(Resources.FancyZones_VirtualDesktop, currentVirtualDesktop),
|
||||
DetailTag(Resources.FancyZones_WorkArea, $"{monitor.Data.LeftCoordinate},{monitor.Data.TopCoordinate} {monitor.Data.WorkAreaWidth}\u00D7{monitor.Data.WorkAreaHeight}"),
|
||||
DetailTag(Resources.FancyZones_Resolution, $"{monitor.Data.MonitorWidth}\u00D7{monitor.Data.MonitorHeight}"),
|
||||
DetailTag(Resources.FancyZones_Resolution, resolution),
|
||||
DetailTag(Resources.FancyZones_DPI, monitor.Data.Dpi.ToString(CultureInfo.InvariantCulture)),
|
||||
};
|
||||
|
||||
|
||||
@@ -41,16 +41,13 @@ internal static class FancyZonesDataService
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(FZPaths.EditorParameters))
|
||||
{
|
||||
error = Resources.FancyZones_MonitorDataNotFound;
|
||||
Logger.LogWarning($"TryGetMonitors: File not found. Path={FZPaths.EditorParameters}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"TryGetMonitors: File exists, reading...");
|
||||
var editorParams = FancyZonesDataIO.ReadEditorParameters();
|
||||
Logger.LogInfo($"TryGetMonitors: ReadEditorParameters returned. Monitors={editorParams.Monitors?.Count ?? -1}");
|
||||
// Request FancyZones to save current monitor configuration.
|
||||
// The editor-parameters.json file is only written when:
|
||||
// 1. Opening the FancyZones Editor
|
||||
// 2. Receiving the WM_PRIV_SAVE_EDITOR_PARAMETERS message
|
||||
// Without this, monitor changes (plug/unplug) won't be reflected in the file.
|
||||
var editorParams = ReadEditorParametersWithRefresh();
|
||||
Logger.LogInfo($"TryGetMonitors: ReadEditorParametersWithRefreshWithRefresh returned. Monitors={editorParams.Monitors?.Count ?? -1}");
|
||||
|
||||
var editorMonitors = editorParams.Monitors;
|
||||
if (editorMonitors is null || editorMonitors.Count == 0)
|
||||
@@ -74,6 +71,23 @@ internal static class FancyZonesDataService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests FancyZones to save the current monitor configuration and reads the file.
|
||||
/// This is a best-effort approach for performance: we send the save request and immediately
|
||||
/// read the file without waiting. If the file hasn't been updated yet, the next call will
|
||||
/// see the updated data since FancyZones processes the message asynchronously.
|
||||
/// </summary>
|
||||
private static EditorParameters.ParamsWrapper ReadEditorParametersWithRefresh()
|
||||
{
|
||||
// Request FancyZones to save the current monitor configuration.
|
||||
// This is fire-and-forget for performance - we don't wait for the save to complete.
|
||||
// If this is the first call after a monitor change, we may read stale data, but the
|
||||
// next call will see the updated file since FancyZones will have processed the message.
|
||||
FancyZonesNotifier.NotifySaveEditorParameters();
|
||||
|
||||
return FancyZonesDataIO.ReadEditorParameters();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<FancyZonesLayoutDescriptor> GetLayouts()
|
||||
{
|
||||
Logger.LogInfo($"GetLayouts: Starting. LayoutTemplatesPath={FZPaths.LayoutTemplates} CustomLayoutsPath={FZPaths.CustomLayouts}");
|
||||
|
||||
@@ -19,8 +19,12 @@ internal readonly record struct FancyZonesMonitorDescriptor(
|
||||
{
|
||||
get
|
||||
{
|
||||
var size = $"{Data.MonitorWidth}×{Data.MonitorHeight}";
|
||||
var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(Data.Dpi * 100 / 96.0)) : "n/a";
|
||||
// MonitorWidth/Height are logical (DPI-scaled) pixels, calculate physical resolution
|
||||
var scaleFactor = Data.Dpi > 0 ? Data.Dpi / 96.0 : 1.0;
|
||||
var physicalWidth = (int)Math.Round(Data.MonitorWidth * scaleFactor);
|
||||
var physicalHeight = (int)Math.Round(Data.MonitorHeight * scaleFactor);
|
||||
var size = $"{physicalWidth}×{physicalHeight}";
|
||||
var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(scaleFactor * 100)) : "n/a";
|
||||
return $"{size} \u2022 {scaling}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,25 @@ namespace PowerToysExtension.Helpers;
|
||||
internal static class FancyZonesNotifier
|
||||
{
|
||||
private const string AppliedLayoutsFileUpdateMessage = "{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}";
|
||||
private const string SaveEditorParametersMessage = "{d8f9c0e3-5d77-4e83-8a4f-7c704c2bfb4a}";
|
||||
|
||||
private static readonly uint WmPrivAppliedLayoutsFileUpdate = RegisterWindowMessageW(AppliedLayoutsFileUpdateMessage);
|
||||
private static readonly uint WmPrivSaveEditorParameters = RegisterWindowMessageW(SaveEditorParametersMessage);
|
||||
|
||||
public static void NotifyAppliedLayoutsChanged()
|
||||
{
|
||||
_ = PostMessageW(new IntPtr(0xFFFF), WmPrivAppliedLayoutsFileUpdate, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies FancyZones to save the current monitor configuration to editor-parameters.json.
|
||||
/// This is needed because FancyZones only writes this file when opening the editor or when explicitly requested.
|
||||
/// </summary>
|
||||
public static void NotifySaveEditorParameters()
|
||||
{
|
||||
_ = PostMessageW(new IntPtr(0xFFFF), WmPrivSaveEditorParameters, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern uint RegisterWindowMessageW(string lpString);
|
||||
|
||||
|
||||
@@ -485,12 +485,21 @@ internal static class FancyZonesThumbnailRenderer
|
||||
|
||||
private static List<NormalizedRect> GetFocusRects(int zoneCount)
|
||||
{
|
||||
// Focus layout parameters from FancyZonesEditor CanvasLayoutModel:
|
||||
// - DefaultOffset = 100px from top-left (normalized: ~0.05 for typical screen)
|
||||
// - OffsetShift = 50px per zone (normalized: ~0.025)
|
||||
// - ZoneSizeMultiplier = 0.4 (zones are 40% of screen)
|
||||
zoneCount = Math.Clamp(zoneCount, 1, 8);
|
||||
var rects = new List<NormalizedRect>(zoneCount);
|
||||
|
||||
const float defaultOffset = 0.05f; // ~100px on 1920px screen
|
||||
const float offsetShift = 0.025f; // ~50px on 1920px screen
|
||||
const float zoneSize = 0.4f; // 40% of screen
|
||||
|
||||
for (var i = 0; i < zoneCount; i++)
|
||||
{
|
||||
var offset = i * 0.06f;
|
||||
rects.Add(new NormalizedRect(0.1f + offset, 0.1f + offset, 0.8f, 0.8f));
|
||||
var offset = i * offsetShift;
|
||||
rects.Add(new NormalizedRect(defaultOffset + offset, defaultOffset + offset, zoneSize, zoneSize));
|
||||
}
|
||||
|
||||
return rects;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>PowerToysExtension</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>..\..\..\..\runner\svgs\icon.ico</ApplicationIcon>
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<EnableMsixTooling>false</EnableMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
|
||||
@@ -132,12 +132,20 @@ public partial class InstallPackageListItem : ListItem
|
||||
// These can be l o n g
|
||||
{ Properties.Resources.winget_release_notes, (metadata.ReleaseNotes, string.Empty) },
|
||||
};
|
||||
var docs = metadata.Documentations;
|
||||
var count = docs.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
|
||||
try
|
||||
{
|
||||
var item = docs[i];
|
||||
simpleData.Add(item.DocumentLabel, (string.Empty, item.DocumentUrl));
|
||||
var docs = metadata.Documentations;
|
||||
var count = docs.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var item = docs[i];
|
||||
simpleData.Add(item.DocumentLabel, (string.Empty, item.DocumentUrl));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to retrieve documentations from metadata: {ex.Message}");
|
||||
}
|
||||
|
||||
UriCreationOptions options = default;
|
||||
@@ -159,14 +167,21 @@ public partial class InstallPackageListItem : ListItem
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.Tags.Count > 0)
|
||||
try
|
||||
{
|
||||
DetailsElement pair = new()
|
||||
if (metadata.Tags.Count > 0)
|
||||
{
|
||||
Key = "Tags",
|
||||
Data = new DetailsTags() { Tags = metadata.Tags.Select(t => new Tag(t)).ToArray() },
|
||||
};
|
||||
detailsElements.Add(pair);
|
||||
DetailsElement pair = new()
|
||||
{
|
||||
Key = "Tags",
|
||||
Data = new DetailsTags() { Tags = metadata.Tags.Select(t => new Tag(t)).ToArray() },
|
||||
};
|
||||
detailsElements.Add(pair);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to retrieve tags from metadata: {ex.Message}");
|
||||
}
|
||||
|
||||
return detailsElements;
|
||||
|
||||
@@ -69,14 +69,14 @@ internal sealed partial class SampleMarkdownImagesPage : ContentPage
|
||||
|
||||
### Web URL
|
||||
```xml
|
||||

|
||||

|
||||
```
|
||||

|
||||

|
||||
|
||||
```xml
|
||||

|
||||

|
||||
```
|
||||

|
||||

|
||||
|
||||
### File URL (PNG):
|
||||
```xml
|
||||
|
||||
@@ -81,7 +81,7 @@ Result:
|
||||
|
||||
Result:
|
||||
|
||||

|
||||

|
||||
|
||||
### Links
|
||||
|
||||
|
||||
@@ -584,7 +584,7 @@ namespace UITests_FancyZones
|
||||
}
|
||||
|
||||
windowingElement.Find<Element>("FancyZones").Click();
|
||||
this.Find<ToggleSwitch>("Enable FancyZones").Toggle(true);
|
||||
Find<ToggleSwitch>(By.AccessibilityId("EnableFancyZonesToggleSwitch")).Toggle(true);
|
||||
if (isMax == true)
|
||||
{
|
||||
this.Find<Button>("Maximize").Click(); // maximize the window
|
||||
@@ -661,7 +661,7 @@ namespace UITests_FancyZones
|
||||
this.Find<NavigationViewItem>("Hosts File Editor").Click();
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
|
||||
this.Find<ToggleSwitch>("Hosts File Editor").Toggle(true);
|
||||
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin);
|
||||
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ namespace UITests_FancyZones
|
||||
}
|
||||
|
||||
this.Find<NavigationViewItem>("FancyZones").Click();
|
||||
this.Find<ToggleSwitch>("Enable FancyZones").Toggle(true);
|
||||
Find<ToggleSwitch>(By.AccessibilityId("EnableFancyZonesToggleSwitch")).Toggle(true);
|
||||
this.Session.SetMainWindowSize(WindowSize.Large);
|
||||
|
||||
// fixed settings
|
||||
@@ -322,7 +322,7 @@ namespace UITests_FancyZones
|
||||
this.Find<NavigationViewItem>("Hosts File Editor").Click();
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
|
||||
this.Find<ToggleSwitch>("Hosts File Editor").Toggle(true);
|
||||
this.Find<ToggleSwitch>("Open as administrator").Toggle(launchAsAdmin);
|
||||
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
using FancyZonesCLI.Utils;
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
@@ -35,13 +36,19 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
|
||||
{
|
||||
// FancyZones running guard is handled by FancyZonesBaseCommand.
|
||||
int key = context.ParseResult.GetValueForArgument(_key);
|
||||
string layout = context.ParseResult.GetValueForArgument(_layout);
|
||||
string layoutInput = context.ParseResult.GetValueForArgument(_layout);
|
||||
|
||||
if (key < 0 || key > 9)
|
||||
{
|
||||
throw new InvalidOperationException(Properties.Resources.set_hotkey_error_invalid_key);
|
||||
}
|
||||
|
||||
// Normalize GUID to Windows format with braces (supports input with or without braces)
|
||||
if (!GuidHelper.TryNormalizeGuid(layoutInput, out string layout))
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layoutInput));
|
||||
}
|
||||
|
||||
// Editor only allows assigning hotkeys to existing custom layouts.
|
||||
var customLayouts = FancyZonesDataIO.ReadCustomLayouts();
|
||||
|
||||
@@ -60,7 +67,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
|
||||
|
||||
if (!matchedLayout.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layout));
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layoutInput));
|
||||
}
|
||||
|
||||
string layoutName = matchedLayout.Value.Name;
|
||||
|
||||
@@ -140,9 +140,12 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalize GUID to Windows format with braces (supports input with or without braces)
|
||||
string normalizedLayout = GuidHelper.NormalizeGuid(layout) ?? layout;
|
||||
|
||||
foreach (var customLayout in customLayouts.CustomLayouts)
|
||||
{
|
||||
if (customLayout.Uuid.Equals(layout, StringComparison.OrdinalIgnoreCase))
|
||||
if (customLayout.Uuid.Equals(normalizedLayout, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return customLayout;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine;
|
||||
@@ -13,15 +14,15 @@ internal static class FancyZonesCliUsage
|
||||
public static void PrintUsage()
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
Console.WriteLine("FancyZones CLI - Command line interface for FancyZones");
|
||||
Console.WriteLine(Properties.Resources.usage_title);
|
||||
Console.WriteLine();
|
||||
|
||||
var cmd = FancyZonesCliCommandFactory.CreateRootCommand();
|
||||
|
||||
Console.WriteLine("Usage: FancyZonesCLI [command] [options]");
|
||||
Console.WriteLine(Properties.Resources.usage_syntax);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("Options:");
|
||||
Console.WriteLine(Properties.Resources.usage_options);
|
||||
foreach (var option in cmd.Options)
|
||||
{
|
||||
var aliases = string.Join(", ", option.Aliases);
|
||||
@@ -30,7 +31,7 @@ internal static class FancyZonesCliUsage
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Commands:");
|
||||
Console.WriteLine(Properties.Resources.usage_commands);
|
||||
foreach (var command in cmd.Subcommands)
|
||||
{
|
||||
if (command.IsHidden)
|
||||
@@ -51,7 +52,7 @@ internal static class FancyZonesCliUsage
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine(Properties.Resources.usage_examples);
|
||||
Console.WriteLine(" FancyZonesCLI --help");
|
||||
Console.WriteLine(" FancyZonesCLI --version");
|
||||
Console.WriteLine(" FancyZonesCLI get-monitors");
|
||||
@@ -59,4 +60,135 @@ internal static class FancyZonesCliUsage
|
||||
Console.WriteLine(" FancyZonesCLI set-layout <uuid> --monitor 1");
|
||||
Console.WriteLine(" FancyZonesCLI get-hotkeys");
|
||||
}
|
||||
|
||||
public static void PrintCommandUsage(string commandName)
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
var rootCmd = FancyZonesCliCommandFactory.CreateRootCommand();
|
||||
|
||||
// Find matching subcommand by name or alias
|
||||
var subcommand = rootCmd.Subcommands.FirstOrDefault(c =>
|
||||
c.Aliases.Any(a => string.Equals(a, commandName, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (subcommand == null)
|
||||
{
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.usage_unknown_command, commandName));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.usage_run_help);
|
||||
return;
|
||||
}
|
||||
|
||||
// Command name and description
|
||||
Console.WriteLine($"{Properties.Resources.usage_command} {subcommand.Name}");
|
||||
if (!string.IsNullOrEmpty(subcommand.Description))
|
||||
{
|
||||
Console.WriteLine($" {subcommand.Description}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Usage line
|
||||
string argsLabel = string.Join(" ", subcommand.Arguments.Select(a => $"<{a.Name}>"));
|
||||
string optionsLabel = subcommand.Options.Any() ? " [options]" : string.Empty;
|
||||
Console.WriteLine($"Usage: FancyZonesCLI {subcommand.Name} {argsLabel}{optionsLabel}".TrimEnd());
|
||||
Console.WriteLine();
|
||||
|
||||
// Aliases
|
||||
var aliases = subcommand.Aliases.Where(a => !string.Equals(a, subcommand.Name, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (aliases.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"{Properties.Resources.usage_aliases} {string.Join(", ", aliases)}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
// Arguments
|
||||
if (subcommand.Arguments.Any())
|
||||
{
|
||||
Console.WriteLine(Properties.Resources.usage_arguments);
|
||||
foreach (var arg in subcommand.Arguments)
|
||||
{
|
||||
var argDescription = arg.Description ?? string.Empty;
|
||||
Console.WriteLine($" <{arg.Name}>{(arg.Arity.MinimumNumberOfValues == 0 ? $" {Properties.Resources.usage_optional}" : string.Empty),-20} {argDescription}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
// Options
|
||||
if (subcommand.Options.Any())
|
||||
{
|
||||
Console.WriteLine(Properties.Resources.usage_options);
|
||||
foreach (var option in subcommand.Options)
|
||||
{
|
||||
var optAliases = string.Join(", ", option.Aliases);
|
||||
var optDescription = option.Description ?? string.Empty;
|
||||
Console.WriteLine($" {optAliases,-25} {optDescription}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
// Command-specific examples
|
||||
PrintCommandExamples(subcommand.Name);
|
||||
}
|
||||
|
||||
private static void PrintCommandExamples(string commandName)
|
||||
{
|
||||
Console.WriteLine(Properties.Resources.usage_examples);
|
||||
|
||||
switch (commandName.ToLowerInvariant())
|
||||
{
|
||||
case "get-monitors":
|
||||
Console.WriteLine(" FancyZonesCLI get-monitors");
|
||||
Console.WriteLine(" FancyZonesCLI m");
|
||||
break;
|
||||
|
||||
case "get-layouts":
|
||||
Console.WriteLine(" FancyZonesCLI get-layouts");
|
||||
Console.WriteLine(" FancyZonesCLI ls");
|
||||
break;
|
||||
|
||||
case "get-active-layout":
|
||||
Console.WriteLine(" FancyZonesCLI get-active-layout");
|
||||
Console.WriteLine(" FancyZonesCLI active");
|
||||
break;
|
||||
|
||||
case "set-layout":
|
||||
Console.WriteLine(" FancyZonesCLI set-layout focus");
|
||||
Console.WriteLine(" FancyZonesCLI set-layout columns --monitor 1");
|
||||
Console.WriteLine(" FancyZonesCLI set-layout {uuid} --all");
|
||||
Console.WriteLine(" FancyZonesCLI s rows -m 2");
|
||||
break;
|
||||
|
||||
case "open-editor":
|
||||
Console.WriteLine(" FancyZonesCLI open-editor");
|
||||
Console.WriteLine(" FancyZonesCLI e");
|
||||
break;
|
||||
|
||||
case "open-settings":
|
||||
Console.WriteLine(" FancyZonesCLI open-settings");
|
||||
Console.WriteLine(" FancyZonesCLI settings");
|
||||
break;
|
||||
|
||||
case "get-hotkeys":
|
||||
Console.WriteLine(" FancyZonesCLI get-hotkeys");
|
||||
Console.WriteLine(" FancyZonesCLI hk");
|
||||
break;
|
||||
|
||||
case "set-hotkey":
|
||||
Console.WriteLine(" FancyZonesCLI set-hotkey 1 {layout-uuid}");
|
||||
Console.WriteLine(" FancyZonesCLI shk 2 0CEBCBA9-9C32-4395-B93E-DC77485AD6D0");
|
||||
break;
|
||||
|
||||
case "remove-hotkey":
|
||||
Console.WriteLine(" FancyZonesCLI remove-hotkey 1");
|
||||
Console.WriteLine(" FancyZonesCLI rhk 2");
|
||||
break;
|
||||
|
||||
default:
|
||||
Console.WriteLine($" FancyZonesCLI {commandName}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace FancyZonesCLI;
|
||||
|
||||
internal sealed class Program
|
||||
{
|
||||
private static readonly string[] HelpFlags = ["--help", "-h", "-?"];
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger();
|
||||
@@ -21,14 +23,17 @@ internal sealed class Program
|
||||
NativeMethods.InitializeWindowMessages();
|
||||
|
||||
// Intercept help requests early and print custom usage.
|
||||
if (args.Any(a => string.Equals(a, "--help", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(a, "-h", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(a, "-?", StringComparison.OrdinalIgnoreCase)))
|
||||
if (TryHandleHelpRequest(args))
|
||||
{
|
||||
FancyZonesCliUsage.PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Detect PowerShell script block expansion (when {} is interpreted as script block)
|
||||
if (DetectPowerShellScriptBlockArgs(args))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
RootCommand rootCommand = FancyZonesCliCommandFactory.CreateRootCommand();
|
||||
int exitCode = await rootCommand.InvokeAsync(args);
|
||||
|
||||
@@ -43,4 +48,69 @@ internal sealed class Program
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles help requests for root command and subcommands.
|
||||
/// </summary>
|
||||
/// <returns>True if help was printed, false otherwise.</returns>
|
||||
private static bool TryHandleHelpRequest(string[] args)
|
||||
{
|
||||
bool hasHelpFlag = args.Any(a => HelpFlags.Any(h => string.Equals(a, h, StringComparison.OrdinalIgnoreCase)));
|
||||
if (!hasHelpFlag)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get non-help arguments to identify subcommand
|
||||
var nonHelpArgs = args.Where(a => !HelpFlags.Any(h => string.Equals(a, h, StringComparison.OrdinalIgnoreCase))).ToArray();
|
||||
|
||||
if (nonHelpArgs.Length == 0)
|
||||
{
|
||||
// Root help: fancyzones cli --help
|
||||
FancyZonesCliUsage.PrintUsage();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Subcommand help: fancyzones cli <command> --help
|
||||
string subcommandName = nonHelpArgs[0];
|
||||
FancyZonesCliUsage.PrintCommandUsage(subcommandName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects when PowerShell interprets {GUID} as a script block and converts it to encoded command args.
|
||||
/// This happens when users forget to quote GUIDs with braces in PowerShell.
|
||||
/// </summary>
|
||||
/// <returns>True if PowerShell script block args were detected, false otherwise.</returns>
|
||||
private static bool DetectPowerShellScriptBlockArgs(string[] args)
|
||||
{
|
||||
// PowerShell converts {scriptblock} to: -encodedCommand <base64> -inputFormat xml -outputFormat text
|
||||
bool hasEncodedCommand = args.Any(a => string.Equals(a, "-encodedCommand", StringComparison.OrdinalIgnoreCase));
|
||||
bool hasInputFormat = args.Any(a => string.Equals(a, "-inputFormat", StringComparison.OrdinalIgnoreCase));
|
||||
bool hasOutputFormat = args.Any(a => string.Equals(a, "-outputFormat", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (hasEncodedCommand || (hasInputFormat && hasOutputFormat))
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_title);
|
||||
Console.ResetColor();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_explanation);
|
||||
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_hint);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option1}");
|
||||
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option1_example}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option2}");
|
||||
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option2_example}");
|
||||
Console.WriteLine();
|
||||
|
||||
Logger.LogWarning("PowerShell script block expansion detected - user needs to quote GUID or omit braces");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,5 +349,113 @@ namespace FancyZonesCLI.Properties {
|
||||
return ResourceManager.GetString("editor_params_timeout", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_title {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_explanation {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_explanation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_hint {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_hint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_option1 {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_option1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_option1_example {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_option1_example", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_option2 {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_option2", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_option2_example {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_option2_example", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_title {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_syntax {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_syntax", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_options {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_options", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_commands {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_commands", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_examples {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_examples", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_arguments {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_arguments", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_aliases {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_aliases", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_command {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_command", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_optional {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_optional", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_unknown_command {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_unknown_command", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_run_help {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_run_help", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,4 +230,62 @@ Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid',
|
||||
<data name="editor_params_timeout" xml:space="preserve">
|
||||
<value>Could not get current monitor information (timed out after {0}ms waiting for '{1}').</value>
|
||||
</data>
|
||||
|
||||
<!-- PowerShell Script Block Detection -->
|
||||
<data name="error_powershell_scriptblock_title" xml:space="preserve">
|
||||
<value>Error: Invalid GUID format detected.</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_explanation" xml:space="preserve">
|
||||
<value>PowerShell interprets curly braces {} as script blocks.</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_hint" xml:space="preserve">
|
||||
<value>Please quote your GUID or use it without braces:</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_option1" xml:space="preserve">
|
||||
<value>Option 1 - Quote the GUID:</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_option1_example" xml:space="preserve">
|
||||
<value>FancyZonesCLI shk 1 '{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}'</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_option2" xml:space="preserve">
|
||||
<value>Option 2 - Omit the braces (recommended):</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_option2_example" xml:space="preserve">
|
||||
<value>FancyZonesCLI shk 1 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</value>
|
||||
</data>
|
||||
|
||||
<!-- CLI Usage -->
|
||||
<data name="usage_title" xml:space="preserve">
|
||||
<value>FancyZones CLI - Command line interface for FancyZones</value>
|
||||
</data>
|
||||
<data name="usage_syntax" xml:space="preserve">
|
||||
<value>Usage: FancyZonesCLI [command] [options]</value>
|
||||
</data>
|
||||
<data name="usage_options" xml:space="preserve">
|
||||
<value>Options:</value>
|
||||
</data>
|
||||
<data name="usage_commands" xml:space="preserve">
|
||||
<value>Commands:</value>
|
||||
</data>
|
||||
<data name="usage_examples" xml:space="preserve">
|
||||
<value>Examples:</value>
|
||||
</data>
|
||||
<data name="usage_arguments" xml:space="preserve">
|
||||
<value>Arguments:</value>
|
||||
</data>
|
||||
<data name="usage_aliases" xml:space="preserve">
|
||||
<value>Aliases:</value>
|
||||
</data>
|
||||
<data name="usage_command" xml:space="preserve">
|
||||
<value>Command:</value>
|
||||
</data>
|
||||
<data name="usage_optional" xml:space="preserve">
|
||||
<value>(optional)</value>
|
||||
</data>
|
||||
<data name="usage_unknown_command" xml:space="preserve">
|
||||
<value>Unknown command: {0}</value>
|
||||
</data>
|
||||
<data name="usage_run_help" xml:space="preserve">
|
||||
<value>Run 'FancyZonesCLI --help' to see available commands.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
52
src/modules/fancyzones/FancyZonesCLI/Utils/GuidHelper.cs
Normal file
52
src/modules/fancyzones/FancyZonesCLI/Utils/GuidHelper.cs
Normal 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.Diagnostics.CodeAnalysis;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace FancyZonesCLI.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for normalizing GUID strings to Windows format with braces.
|
||||
/// Supports input with or without braces: both "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
/// and "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}" are accepted.
|
||||
/// </summary>
|
||||
internal static class GuidHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes a GUID string to Windows format with braces.
|
||||
/// Returns null if the input is not a valid GUID.
|
||||
/// </summary>
|
||||
/// <param name="input">GUID string with or without braces.</param>
|
||||
/// <returns>GUID in "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}" format, or null if invalid.</returns>
|
||||
public static string? NormalizeGuid(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(input, out Guid guid))
|
||||
{
|
||||
// "B" format includes braces: {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||
return guid.ToString("B").ToUpperInvariant();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to normalize a GUID string to Windows format with braces.
|
||||
/// </summary>
|
||||
/// <param name="input">GUID string with or without braces.</param>
|
||||
/// <param name="normalizedGuid">The normalized GUID string, or the original input if normalization fails.</param>
|
||||
/// <returns>True if the input was successfully normalized; otherwise, false.</returns>
|
||||
public static bool TryNormalizeGuid(string? input, [NotNullWhen(true)] out string? normalizedGuid)
|
||||
{
|
||||
normalizedGuid = NormalizeGuid(input);
|
||||
return normalizedGuid != null;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user