mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-10 22:36:38 +01:00
Compare commits
3 Commits
stable
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
380669dd81 | ||
|
|
d24569d49c | ||
|
|
94cbea91a0 |
1
.github/actions/spell-check/excludes.txt
vendored
1
.github/actions/spell-check/excludes.txt
vendored
@@ -93,6 +93,7 @@
|
||||
\.xz$
|
||||
\.zip$
|
||||
^\.github/actions/spell-check/
|
||||
^\.github/skills/
|
||||
^\.github/workflows/spelling\d*\.yml$
|
||||
^\.gitmodules$
|
||||
^\Q.pipelines/ESRPSigning_core.json\E$
|
||||
|
||||
16
.github/actions/spell-check/expect.txt
vendored
16
.github/actions/spell-check/expect.txt
vendored
@@ -215,7 +215,6 @@ cim
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
CLEARTYPE
|
||||
clickable
|
||||
@@ -365,9 +364,7 @@ DEFAULTFLAGS
|
||||
DEFAULTICON
|
||||
defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTSIZE
|
||||
DEFAULTTONEAREST
|
||||
Defaulttonearest
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
@@ -832,11 +829,9 @@ ITHUMBNAIL
|
||||
IUI
|
||||
IUWP
|
||||
IWIC
|
||||
jeli
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
jjw
|
||||
JOBOBJECT
|
||||
jobject
|
||||
jpe
|
||||
jpnime
|
||||
@@ -870,7 +865,6 @@ lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
LAYOUTRTL
|
||||
lbl
|
||||
Lbuttondown
|
||||
LCh
|
||||
lcid
|
||||
LCIDTo
|
||||
@@ -993,7 +987,6 @@ maxversiontested
|
||||
mber
|
||||
MBM
|
||||
MBR
|
||||
Mbuttondown
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
@@ -1452,7 +1445,6 @@ RAWINPUTHEADER
|
||||
RAWMODE
|
||||
RAWPATH
|
||||
rbhid
|
||||
Rbuttondown
|
||||
rclsid
|
||||
RCZOOMIT
|
||||
remotedesktop
|
||||
@@ -1603,7 +1595,7 @@ sharpfuzz
|
||||
SHCNE
|
||||
SHCNF
|
||||
SHCONTF
|
||||
shcore
|
||||
Shcore
|
||||
shellapi
|
||||
SHELLDETAILS
|
||||
SHELLDLL
|
||||
@@ -1702,7 +1694,6 @@ srw
|
||||
srwlock
|
||||
sse
|
||||
ssf
|
||||
sszzz
|
||||
STACKFRAME
|
||||
stackoverflow
|
||||
STARTF
|
||||
@@ -1713,7 +1704,6 @@ STARTUPINFOW
|
||||
startupscreen
|
||||
STATFLAG
|
||||
STATICEDGE
|
||||
staticmethod
|
||||
STATSTG
|
||||
stdafx
|
||||
STDAPI
|
||||
@@ -1757,7 +1747,6 @@ svgz
|
||||
SVSI
|
||||
SWFO
|
||||
SWP
|
||||
Swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
@@ -1777,7 +1766,6 @@ syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
SYSMENU
|
||||
Sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
@@ -1880,7 +1868,6 @@ uild
|
||||
uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
Ultrawide
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
@@ -2103,7 +2090,6 @@ Wwanpp
|
||||
xap
|
||||
XAxis
|
||||
XButton
|
||||
Xbuttondown
|
||||
xclip
|
||||
xcopy
|
||||
XDeployment
|
||||
|
||||
3
.github/actions/spell-check/patterns.txt
vendored
3
.github/actions/spell-check/patterns.txt
vendored
@@ -273,6 +273,3 @@ St&yle
|
||||
# Usernames with numbers
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\bx6f677548\b
|
||||
|
||||
# Microsoft Store URLs and product IDs
|
||||
ms-windows-store://\S+
|
||||
|
||||
80
.github/copilot-instructions.md
vendored
80
.github/copilot-instructions.md
vendored
@@ -1,45 +1,59 @@
|
||||
---
|
||||
description: 'PowerToys AI contributor guidance'
|
||||
description: PowerToys AI contributor guidance.
|
||||
applyTo: pullRequests
|
||||
---
|
||||
|
||||
# PowerToys – Copilot Instructions
|
||||
# PowerToys - Copilot guide (concise)
|
||||
|
||||
Concise guidance for AI contributions. For complete details, see [AGENTS.md](../AGENTS.md).
|
||||
This is the top-level guide for AI changes. Keep edits small, follow existing patterns, and cite exact paths in PRs.
|
||||
|
||||
## Quick Reference
|
||||
# Repo map (1-line per area)
|
||||
- Core apps: `src/runner/**` (tray/loader), `src/settings-ui/**` (Settings app)
|
||||
- Shared libs: `src/common/**`
|
||||
- Modules: `src/modules/*` (one per utility; Command Palette in `src/modules/cmdpal/**`)
|
||||
- Build tools/docs: `tools/**`, `doc/devdocs/**`
|
||||
|
||||
- **Build**: `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
|
||||
# Build and test (defaults)
|
||||
- Prerequisites: Visual Studio 2022 17.4+, minimal Windows 10 1803+.
|
||||
- Build discipline:
|
||||
- One terminal per operation (build -> test). Do not switch or open new ones mid-flow.
|
||||
- After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`).
|
||||
- Use scripts to build, synchronously block and wait in foreground for completion: `tools/build/build.ps1|.cmd` (current folder), `build-essentials.*` (once per brand new build for missing nuget packages).
|
||||
- Treat build exit code 0 as success; any non-zero exit code is a failure. Read the errors log in the build folder (such as `build.*.*.errors.log`) and surface problems.
|
||||
- Do not start tests or launch Runner until the previous step succeeded.
|
||||
- Tests (fast and targeted):
|
||||
- Find the test project by product code prefix (for example FancyZones, AdvancedPaste). Look for a sibling folder or one to two levels up named like `<Product>*UnitTests` or `<Product>*UITests`.
|
||||
- Build the test project, wait for exit, then run only those tests via VS Test Explorer or `vstest.console.exe` with filters. Avoid `dotnet test` in this repo.
|
||||
- Add or adjust tests when changing behavior; if skipped, state why (for example comment-only or string rename).
|
||||
|
||||
## Key Rules
|
||||
# Pull requests (expectations)
|
||||
- Atomic: one logical change; no drive-by refactors.
|
||||
- Describe: problem, approach, risk, test evidence.
|
||||
- List: touched paths if not obvious.
|
||||
|
||||
- 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)
|
||||
# When to ask for clarification
|
||||
- Ambiguous spec after scanning relevant docs (see below).
|
||||
- Cross-module impact (shared enum or struct) not clear.
|
||||
- Security, elevation, or installer changes.
|
||||
|
||||
## Style Enforcement
|
||||
# Logging (use existing stacks)
|
||||
- C++ logging lives in `src/common/logger/**` (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`). Keep hot paths quiet (hooks, tight loops).
|
||||
- C# logging goes through `ManagedCommon.Logger` (`LogInfo`, `LogWarning`, `LogError`, `LogDebug`, `LogTrace`). Some UIs use injected `ILogger` via `LoggerInstance.Logger`.
|
||||
|
||||
- C#: `src/.editorconfig`, StyleCop.Analyzers
|
||||
- C++: `src/.clang-format`
|
||||
- XAML: XamlStyler
|
||||
# Docs to consult
|
||||
- `tools/build/BUILD-GUIDELINES.md`
|
||||
- `doc/devdocs/core/architecture.md`
|
||||
- `doc/devdocs/core/runner.md`
|
||||
- `doc/devdocs/core/settings/readme.md`
|
||||
- `doc/devdocs/modules/readme.md`
|
||||
|
||||
## When to Ask for Clarification
|
||||
# Language style rules
|
||||
- Always enforce repo analyzers: root `.editorconfig` plus any `stylecop.json`.
|
||||
- C# code follows StyleCop.Analyzers and Microsoft.CodeAnalysis.NetAnalyzers.
|
||||
- C++ code honors `.clang-format` plus `.clang-tidy` (modernize/cppcoreguidelines/readability).
|
||||
- Markdown files wrap at 80 characters and use ATX headers with fenced code blocks that include language tags.
|
||||
- YAML files indent two spaces and add comments for complex settings while keeping keys clear.
|
||||
- PowerShell scripts use Verb-Noun names and prefer single-quoted literals while documenting parameters and satisfying PSScriptAnalyzer.
|
||||
|
||||
- Ambiguous spec after scanning docs
|
||||
- Cross-module impact unclear
|
||||
- Security, elevation, or installer changes
|
||||
|
||||
## Component-Specific Instructions
|
||||
|
||||
These are auto-applied based on file location:
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
|
||||
## 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)
|
||||
# Done checklist (self review before finishing)
|
||||
- Build clean? Tests updated or passed? No unintended formatting? Any new dependency? Documented skips?
|
||||
|
||||
791
.github/instructions/agents.instructions.md
vendored
791
.github/instructions/agents.instructions.md
vendored
@@ -1,791 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for creating custom agent files for GitHub Copilot'
|
||||
applyTo: '**/*.agent.md'
|
||||
---
|
||||
|
||||
# Custom Agent File Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable custom agent files that provide specialized expertise for specific development tasks in GitHub Copilot.
|
||||
|
||||
## Project Context
|
||||
|
||||
- Target audience: Developers creating custom agents for GitHub Copilot
|
||||
- File format: Markdown with YAML frontmatter
|
||||
- File naming convention: lowercase with hyphens (e.g., `test-specialist.agent.md`)
|
||||
- Location: `.github/agents/` directory (repository-level) or `agents/` directory (organization/enterprise-level)
|
||||
- Purpose: Define specialized agents with tailored expertise, tools, and instructions for specific tasks
|
||||
- Official documentation: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents
|
||||
|
||||
## Required Frontmatter
|
||||
|
||||
Every agent file must include YAML frontmatter with the following fields:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: 'Brief description of the agent purpose and capabilities'
|
||||
name: 'Agent Display Name'
|
||||
tools: ['read', 'edit', 'search']
|
||||
model: 'Claude Sonnet 4.5'
|
||||
target: 'vscode'
|
||||
infer: true
|
||||
---
|
||||
```
|
||||
|
||||
### Core Frontmatter Properties
|
||||
|
||||
#### **description** (REQUIRED)
|
||||
- Single-quoted string, clearly stating the agent's purpose and domain expertise
|
||||
- Should be concise (50-150 characters) and actionable
|
||||
- Example: `'Focuses on test coverage, quality, and testing best practices'`
|
||||
|
||||
#### **name** (OPTIONAL)
|
||||
- Display name for the agent in the UI
|
||||
- If omitted, defaults to filename (without `.md` or `.agent.md`)
|
||||
- Use title case and be descriptive
|
||||
- Example: `'Testing Specialist'`
|
||||
|
||||
#### **tools** (OPTIONAL)
|
||||
- List of tool names or aliases the agent can use
|
||||
- Supports comma-separated string or YAML array format
|
||||
- If omitted, agent has access to all available tools
|
||||
- See "Tool Configuration" section below for details
|
||||
|
||||
#### **model** (STRONGLY RECOMMENDED)
|
||||
- Specifies which AI model the agent should use
|
||||
- Supported in VS Code, JetBrains IDEs, Eclipse, and Xcode
|
||||
- Example: `'Claude Sonnet 4.5'`, `'gpt-4'`, `'gpt-4o'`
|
||||
- Choose based on agent complexity and required capabilities
|
||||
|
||||
#### **target** (OPTIONAL)
|
||||
- Specifies target environment: `'vscode'` or `'github-copilot'`
|
||||
- If omitted, agent is available in both environments
|
||||
- Use when agent has environment-specific features
|
||||
|
||||
#### **infer** (OPTIONAL)
|
||||
- Boolean controlling whether Copilot can automatically use this agent based on context
|
||||
- Default: `true` if omitted
|
||||
- Set to `false` to require manual agent selection
|
||||
|
||||
#### **metadata** (OPTIONAL, GitHub.com only)
|
||||
- Object with name-value pairs for agent annotation
|
||||
- Example: `metadata: { category: 'testing', version: '1.0' }`
|
||||
- Not supported in VS Code
|
||||
|
||||
#### **mcp-servers** (OPTIONAL, Organization/Enterprise only)
|
||||
- Configure MCP servers available only to this agent
|
||||
- Only supported for organization/enterprise level agents
|
||||
- See "MCP Server Configuration" section below
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
### Tool Specification Strategies
|
||||
|
||||
**Enable all tools** (default):
|
||||
```yaml
|
||||
# Omit tools property entirely, or use:
|
||||
tools: ['*']
|
||||
```
|
||||
|
||||
**Enable specific tools**:
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'search', 'execute']
|
||||
```
|
||||
|
||||
**Enable MCP server tools**:
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'github/*', 'playwright/navigate']
|
||||
```
|
||||
|
||||
**Disable all tools**:
|
||||
```yaml
|
||||
tools: []
|
||||
```
|
||||
|
||||
### Standard Tool Aliases
|
||||
|
||||
All aliases are case-insensitive:
|
||||
|
||||
| Alias | Alternative Names | Category | Description |
|
||||
|-------|------------------|----------|-------------|
|
||||
| `execute` | shell, Bash, powershell | Shell execution | Execute commands in appropriate shell |
|
||||
| `read` | Read, NotebookRead, view | File reading | Read file contents |
|
||||
| `edit` | Edit, MultiEdit, Write, NotebookEdit | File editing | Edit and modify files |
|
||||
| `search` | Grep, Glob, search | Code search | Search for files or text in files |
|
||||
| `agent` | custom-agent, Task | Agent invocation | Invoke other custom agents |
|
||||
| `web` | WebSearch, WebFetch | Web access | Fetch web content and search |
|
||||
| `todo` | TodoWrite | Task management | Create and manage task lists (VS Code only) |
|
||||
|
||||
### Built-in MCP Server Tools
|
||||
|
||||
**GitHub MCP Server**:
|
||||
```yaml
|
||||
tools: ['github/*'] # All GitHub tools
|
||||
tools: ['github/get_file_contents', 'github/search_repositories'] # Specific tools
|
||||
```
|
||||
- All read-only tools available by default
|
||||
- Token scoped to source repository
|
||||
|
||||
**Playwright MCP Server**:
|
||||
```yaml
|
||||
tools: ['playwright/*'] # All Playwright tools
|
||||
tools: ['playwright/navigate', 'playwright/screenshot'] # Specific tools
|
||||
```
|
||||
- Configured to access localhost only
|
||||
- Useful for browser automation and testing
|
||||
|
||||
### Tool Selection Best Practices
|
||||
|
||||
- **Principle of Least Privilege**: Only enable tools necessary for the agent's purpose
|
||||
- **Security**: Limit `execute` access unless explicitly required
|
||||
- **Focus**: Fewer tools = clearer agent purpose and better performance
|
||||
- **Documentation**: Comment why specific tools are required for complex configurations
|
||||
|
||||
## Sub-Agent Invocation (Agent Orchestration)
|
||||
|
||||
Agents can invoke other agents using `runSubagent` to orchestrate multi-step workflows.
|
||||
|
||||
### How It Works
|
||||
|
||||
Include `agent` in tools list to enable sub-agent invocation:
|
||||
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'search', 'agent']
|
||||
```
|
||||
|
||||
Then invoke other agents with `runSubagent`:
|
||||
|
||||
```javascript
|
||||
const result = await runSubagent({
|
||||
description: 'What this step does',
|
||||
prompt: `You are the [Specialist] specialist.
|
||||
|
||||
Context:
|
||||
- Parameter: ${parameterValue}
|
||||
- Input: ${inputPath}
|
||||
- Output: ${outputPath}
|
||||
|
||||
Task:
|
||||
1. Do the specific work
|
||||
2. Write results to output location
|
||||
3. Return summary of completion`
|
||||
});
|
||||
```
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
Structure each sub-agent call with:
|
||||
|
||||
1. **description**: Clear one-line purpose of the sub-agent invocation
|
||||
2. **prompt**: Detailed instructions with substituted variables
|
||||
|
||||
The prompt should include:
|
||||
- Who the sub-agent is (specialist role)
|
||||
- What context it needs (parameters, paths)
|
||||
- What to do (concrete tasks)
|
||||
- Where to write output
|
||||
- What to return (summary)
|
||||
|
||||
### Example: Multi-Step Processing
|
||||
|
||||
```javascript
|
||||
// Step 1: Process data
|
||||
const processing = await runSubagent({
|
||||
description: 'Transform raw input data',
|
||||
prompt: `You are the Data Processor specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${basePath}/raw/
|
||||
Output: ${basePath}/processed/
|
||||
|
||||
Task:
|
||||
1. Read all files from input directory
|
||||
2. Apply transformations
|
||||
3. Write processed files to output
|
||||
4. Create summary: ${basePath}/processed/summary.md
|
||||
|
||||
Return: Number of files processed and any issues found`
|
||||
});
|
||||
|
||||
// Step 2: Analyze (depends on Step 1)
|
||||
const analysis = await runSubagent({
|
||||
description: 'Analyze processed data',
|
||||
prompt: `You are the Data Analyst specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${basePath}/processed/
|
||||
Output: ${basePath}/analysis/
|
||||
|
||||
Task:
|
||||
1. Read processed files from input
|
||||
2. Generate analysis report
|
||||
3. Write to: ${basePath}/analysis/report.md
|
||||
|
||||
Return: Key findings and identified patterns`
|
||||
});
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
- **Pass variables in prompts**: Use `${variableName}` for all dynamic values
|
||||
- **Keep prompts focused**: Clear, specific tasks for each sub-agent
|
||||
- **Return summaries**: Each sub-agent should report what it accomplished
|
||||
- **Sequential execution**: Use `await` to maintain order when steps depend on each other
|
||||
- **Error handling**: Check results before proceeding to dependent steps
|
||||
|
||||
### ⚠️ Tool Availability Requirement
|
||||
|
||||
**Critical**: If a sub-agent requires specific tools (e.g., `edit`, `execute`, `search`), the orchestrator must include those tools in its own `tools` list. Sub-agents cannot access tools that aren't available to their parent orchestrator.
|
||||
|
||||
**Example**:
|
||||
```yaml
|
||||
# If your sub-agents need to edit files, execute commands, or search code
|
||||
tools: ['read', 'edit', 'search', 'execute', 'agent']
|
||||
```
|
||||
|
||||
The orchestrator's tool permissions act as a ceiling for all invoked sub-agents. Plan your tool list carefully to ensure all sub-agents have the tools they need.
|
||||
|
||||
### ⚠️ Important Limitation
|
||||
|
||||
**Sub-agent orchestration is NOT suitable for large-scale data processing.** Avoid using `runSubagent` when:
|
||||
- Processing hundreds or thousands of files
|
||||
- Handling large datasets
|
||||
- Performing bulk transformations on big codebases
|
||||
- Orchestrating more than 5-10 sequential steps
|
||||
|
||||
Each sub-agent call adds latency and context overhead. For high-volume processing, implement logic directly in a single agent instead. Use orchestration only for coordinating specialized tasks on focused, manageable datasets.
|
||||
|
||||
## Agent Prompt Structure
|
||||
|
||||
The markdown content below the frontmatter defines the agent's behavior, expertise, and instructions. Well-structured prompts typically include:
|
||||
|
||||
1. **Agent Identity and Role**: Who the agent is and its primary role
|
||||
2. **Core Responsibilities**: What specific tasks the agent performs
|
||||
3. **Approach and Methodology**: How the agent works to accomplish tasks
|
||||
4. **Guidelines and Constraints**: What to do/avoid and quality standards
|
||||
5. **Output Expectations**: Expected output format and quality
|
||||
|
||||
### Prompt Writing Best Practices
|
||||
|
||||
- **Be Specific and Direct**: Use imperative mood ("Analyze", "Generate"); avoid vague terms
|
||||
- **Define Boundaries**: Clearly state scope limits and constraints
|
||||
- **Include Context**: Explain domain expertise and reference relevant frameworks
|
||||
- **Focus on Behavior**: Describe how the agent should think and work
|
||||
- **Use Structured Format**: Headers, bullets, and lists make prompts scannable
|
||||
|
||||
## Variable Definition and Extraction
|
||||
|
||||
Agents can define dynamic parameters to extract values from user input and use them throughout the agent's behavior and sub-agent communications. This enables flexible, context-aware agents that adapt to user-provided data.
|
||||
|
||||
### When to Use Variables
|
||||
|
||||
**Use variables when**:
|
||||
- Agent behavior depends on user input
|
||||
- Need to pass dynamic values to sub-agents
|
||||
- Want to make agents reusable across different contexts
|
||||
- Require parameterized workflows
|
||||
- Need to track or reference user-provided context
|
||||
|
||||
**Examples**:
|
||||
- Extract project name from user prompt
|
||||
- Capture certification name for pipeline processing
|
||||
- Identify file paths or directories
|
||||
- Extract configuration options
|
||||
- Parse feature names or module identifiers
|
||||
|
||||
### Variable Declaration Pattern
|
||||
|
||||
Define variables section early in the agent prompt to document expected parameters:
|
||||
|
||||
```markdown
|
||||
# Agent Name
|
||||
|
||||
## Dynamic Parameters
|
||||
|
||||
- **Parameter Name**: Description and usage
|
||||
- **Another Parameter**: How it's extracted and used
|
||||
|
||||
## Your Mission
|
||||
|
||||
Process [PARAMETER_NAME] to accomplish [task].
|
||||
```
|
||||
|
||||
### Variable Extraction Methods
|
||||
|
||||
#### 1. **Explicit User Input**
|
||||
Ask the user to provide the variable if not detected in the prompt:
|
||||
|
||||
```markdown
|
||||
## Your Mission
|
||||
|
||||
Process the project by analyzing your codebase.
|
||||
|
||||
### Step 1: Identify Project
|
||||
If no project name is provided, **ASK THE USER** for:
|
||||
- Project name or identifier
|
||||
- Base path or directory location
|
||||
- Configuration type (if applicable)
|
||||
|
||||
Use this information to contextualize all subsequent tasks.
|
||||
```
|
||||
|
||||
#### 2. **Implicit Extraction from Prompt**
|
||||
Automatically extract variables from the user's natural language input:
|
||||
|
||||
```javascript
|
||||
// Example: Extract certification name from user input
|
||||
const userInput = "Process My Certification";
|
||||
|
||||
// Extract key information
|
||||
const certificationName = extractCertificationName(userInput);
|
||||
// Result: "My Certification"
|
||||
|
||||
const basePath = `certifications/${certificationName}`;
|
||||
// Result: "certifications/My Certification"
|
||||
```
|
||||
|
||||
#### 3. **Contextual Variable Resolution**
|
||||
Use file context or workspace information to derive variables:
|
||||
|
||||
```markdown
|
||||
## Variable Resolution Strategy
|
||||
|
||||
1. **From User Prompt**: First, look for explicit mentions in user input
|
||||
2. **From File Context**: Check current file name or path
|
||||
3. **From Workspace**: Use workspace folder or active project
|
||||
4. **From Settings**: Reference configuration files
|
||||
5. **Ask User**: If all else fails, request missing information
|
||||
```
|
||||
|
||||
### Using Variables in Agent Prompts
|
||||
|
||||
#### Variable Substitution in Instructions
|
||||
|
||||
Use template variables in agent prompts to make them dynamic:
|
||||
|
||||
```markdown
|
||||
# Agent Name
|
||||
|
||||
## Dynamic Parameters
|
||||
- **Project Name**: ${projectName}
|
||||
- **Base Path**: ${basePath}
|
||||
- **Output Directory**: ${outputDir}
|
||||
|
||||
## Your Mission
|
||||
|
||||
Process the **${projectName}** project located at `${basePath}`.
|
||||
|
||||
## Process Steps
|
||||
|
||||
1. Read input from: `${basePath}/input/`
|
||||
2. Process files according to project configuration
|
||||
3. Write results to: `${outputDir}/`
|
||||
4. Generate summary report
|
||||
|
||||
## Quality Standards
|
||||
|
||||
- Maintain project-specific coding standards for **${projectName}**
|
||||
- Follow directory structure: `${basePath}/[structure]`
|
||||
```
|
||||
|
||||
#### Passing Variables to Sub-Agents
|
||||
|
||||
When invoking a sub-agent, pass all context through template variables in the prompt:
|
||||
|
||||
```javascript
|
||||
// Extract and prepare variables
|
||||
const basePath = `projects/${projectName}`;
|
||||
const inputPath = `${basePath}/src/`;
|
||||
const outputPath = `${basePath}/docs/`;
|
||||
|
||||
// Pass to sub-agent with all variables substituted
|
||||
const result = await runSubagent({
|
||||
description: 'Generate project documentation',
|
||||
prompt: `You are the Documentation specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${inputPath}
|
||||
Output: ${outputPath}
|
||||
|
||||
Task:
|
||||
1. Read source files from ${inputPath}
|
||||
2. Generate comprehensive documentation
|
||||
3. Write to ${outputPath}/index.md
|
||||
4. Include code examples and usage guides
|
||||
|
||||
Return: Summary of documentation generated (file count, word count)`
|
||||
});
|
||||
```
|
||||
|
||||
The sub-agent receives all necessary context embedded in the prompt. Variables are resolved before sending the prompt, so the sub-agent works with concrete paths and values, not variable placeholders.
|
||||
|
||||
### Real-World Example: Code Review Orchestrator
|
||||
|
||||
Example of a simple orchestrator that validates code through multiple specialized agents:
|
||||
|
||||
```javascript
|
||||
async function reviewCodePipeline(repositoryName, prNumber) {
|
||||
const basePath = `projects/${repositoryName}/pr-${prNumber}`;
|
||||
|
||||
// Step 1: Security Review
|
||||
const security = await runSubagent({
|
||||
description: 'Scan for security vulnerabilities',
|
||||
prompt: `You are the Security Reviewer specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
PR: ${prNumber}
|
||||
Code: ${basePath}/changes/
|
||||
|
||||
Task:
|
||||
1. Scan code for OWASP Top 10 vulnerabilities
|
||||
2. Check for injection attacks, auth flaws
|
||||
3. Write findings to ${basePath}/security-review.md
|
||||
|
||||
Return: List of critical, high, and medium issues found`
|
||||
});
|
||||
|
||||
// Step 2: Test Coverage Check
|
||||
const coverage = await runSubagent({
|
||||
description: 'Verify test coverage for changes',
|
||||
prompt: `You are the Test Coverage specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
PR: ${prNumber}
|
||||
Changes: ${basePath}/changes/
|
||||
|
||||
Task:
|
||||
1. Analyze code coverage for modified files
|
||||
2. Identify untested critical paths
|
||||
3. Write report to ${basePath}/coverage-report.md
|
||||
|
||||
Return: Current coverage percentage and gaps`
|
||||
});
|
||||
|
||||
// Step 3: Aggregate Results
|
||||
const finalReport = await runSubagent({
|
||||
description: 'Compile all review findings',
|
||||
prompt: `You are the Review Aggregator specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
Reports: ${basePath}/*.md
|
||||
|
||||
Task:
|
||||
1. Read all review reports from ${basePath}/
|
||||
2. Synthesize findings into single report
|
||||
3. Determine overall verdict (APPROVE/NEEDS_FIXES/BLOCK)
|
||||
4. Write to ${basePath}/final-review.md
|
||||
|
||||
Return: Final verdict and executive summary`
|
||||
});
|
||||
|
||||
return finalReport;
|
||||
}
|
||||
```
|
||||
|
||||
This pattern applies to any orchestration scenario: extract variables, call sub-agents with clear context, await results.
|
||||
|
||||
|
||||
### Variable Best Practices
|
||||
|
||||
#### 1. **Clear Documentation**
|
||||
Always document what variables are expected:
|
||||
|
||||
```markdown
|
||||
## Required Variables
|
||||
- **projectName**: The name of the project (string, required)
|
||||
- **basePath**: Root directory for project files (path, required)
|
||||
|
||||
## Optional Variables
|
||||
- **mode**: Processing mode - quick/standard/detailed (enum, default: standard)
|
||||
- **outputFormat**: Output format - markdown/json/html (enum, default: markdown)
|
||||
|
||||
## Derived Variables
|
||||
- **outputDir**: Automatically set to ${basePath}/output
|
||||
- **logFile**: Automatically set to ${basePath}/.log.md
|
||||
```
|
||||
|
||||
#### 2. **Consistent Naming**
|
||||
Use consistent variable naming conventions:
|
||||
|
||||
```javascript
|
||||
// Good: Clear, descriptive naming
|
||||
const variables = {
|
||||
projectName, // What project to work on
|
||||
basePath, // Where project files are located
|
||||
outputDirectory, // Where to save results
|
||||
processingMode, // How to process (detail level)
|
||||
configurationPath // Where config files are
|
||||
};
|
||||
|
||||
// Avoid: Ambiguous or inconsistent
|
||||
const bad_variables = {
|
||||
name, // Too generic
|
||||
path, // Unclear which path
|
||||
mode, // Too short
|
||||
config // Too vague
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. **Validation and Constraints**
|
||||
Document valid values and constraints:
|
||||
|
||||
```markdown
|
||||
## Variable Constraints
|
||||
|
||||
**projectName**:
|
||||
- Type: string (alphanumeric, hyphens, underscores allowed)
|
||||
- Length: 1-100 characters
|
||||
- Required: yes
|
||||
- Pattern: `/^[a-zA-Z0-9_-]+$/`
|
||||
|
||||
**processingMode**:
|
||||
- Type: enum
|
||||
- Valid values: "quick" (< 5min), "standard" (5-15min), "detailed" (15+ min)
|
||||
- Default: "standard"
|
||||
- Required: no
|
||||
```
|
||||
|
||||
## MCP Server Configuration (Organization/Enterprise Only)
|
||||
|
||||
MCP servers extend agent capabilities with additional tools. Only supported for organization and enterprise-level agents.
|
||||
|
||||
### Configuration Format
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-custom-agent
|
||||
description: 'Agent with MCP integration'
|
||||
tools: ['read', 'edit', 'custom-mcp/tool-1']
|
||||
mcp-servers:
|
||||
custom-mcp:
|
||||
type: 'local'
|
||||
command: 'some-command'
|
||||
args: ['--arg1', '--arg2']
|
||||
tools: ["*"]
|
||||
env:
|
||||
ENV_VAR_NAME: ${{ secrets.API_KEY }}
|
||||
---
|
||||
```
|
||||
|
||||
### MCP Server Properties
|
||||
|
||||
- **type**: Server type (`'local'` or `'stdio'`)
|
||||
- **command**: Command to start the MCP server
|
||||
- **args**: Array of command arguments
|
||||
- **tools**: Tools to enable from this server (`["*"]` for all)
|
||||
- **env**: Environment variables (supports secrets)
|
||||
|
||||
### Environment Variables and Secrets
|
||||
|
||||
Secrets must be configured in repository settings under "copilot" environment.
|
||||
|
||||
**Supported syntax**:
|
||||
```yaml
|
||||
env:
|
||||
# Environment variable only
|
||||
VAR_NAME: COPILOT_MCP_ENV_VAR_VALUE
|
||||
|
||||
# Variable with header
|
||||
VAR_NAME: $COPILOT_MCP_ENV_VAR_VALUE
|
||||
VAR_NAME: ${COPILOT_MCP_ENV_VAR_VALUE}
|
||||
|
||||
# GitHub Actions-style (YAML only)
|
||||
VAR_NAME: ${{ secrets.COPILOT_MCP_ENV_VAR_VALUE }}
|
||||
VAR_NAME: ${{ var.COPILOT_MCP_ENV_VAR_VALUE }}
|
||||
```
|
||||
|
||||
## File Organization and Naming
|
||||
|
||||
### Repository-Level Agents
|
||||
- Location: `.github/agents/`
|
||||
- Scope: Available only in the specific repository
|
||||
- Access: Uses repository-configured MCP servers
|
||||
|
||||
### Organization/Enterprise-Level Agents
|
||||
- Location: `.github-private/agents/` (then move to `agents/` root)
|
||||
- Scope: Available across all repositories in org/enterprise
|
||||
- Access: Can configure dedicated MCP servers
|
||||
|
||||
### Naming Conventions
|
||||
- Use lowercase with hyphens: `test-specialist.agent.md`
|
||||
- Name should reflect agent purpose
|
||||
- Filename becomes default agent name (if `name` not specified)
|
||||
- Allowed characters: `.`, `-`, `_`, `a-z`, `A-Z`, `0-9`
|
||||
|
||||
## Agent Processing and Behavior
|
||||
|
||||
### Versioning
|
||||
- Based on Git commit SHAs for the agent file
|
||||
- Create branches/tags for different agent versions
|
||||
- Instantiated using latest version for repository/branch
|
||||
- PR interactions use same agent version for consistency
|
||||
|
||||
### Name Conflicts
|
||||
Priority (highest to lowest):
|
||||
1. Repository-level agent
|
||||
2. Organization-level agent
|
||||
3. Enterprise-level agent
|
||||
|
||||
Lower-level configurations override higher-level ones with the same name.
|
||||
|
||||
### Tool Processing
|
||||
- `tools` list filters available tools (built-in and MCP)
|
||||
- No tools specified = all tools enabled
|
||||
- Empty list (`[]`) = all tools disabled
|
||||
- Specific list = only those tools enabled
|
||||
- Unrecognized tool names are ignored (allows environment-specific tools)
|
||||
|
||||
### MCP Server Processing Order
|
||||
1. Out-of-the-box MCP servers (e.g., GitHub MCP)
|
||||
2. Custom agent MCP configuration (org/enterprise only)
|
||||
3. Repository-level MCP configurations
|
||||
|
||||
Each level can override settings from previous levels.
|
||||
|
||||
## Agent Creation Checklist
|
||||
|
||||
### Frontmatter
|
||||
- [ ] `description` field present and descriptive (50-150 chars)
|
||||
- [ ] `description` wrapped in single quotes
|
||||
- [ ] `name` specified (optional but recommended)
|
||||
- [ ] `tools` configured appropriately (or intentionally omitted)
|
||||
- [ ] `model` specified for optimal performance
|
||||
- [ ] `target` set if environment-specific
|
||||
- [ ] `infer` set to `false` if manual selection required
|
||||
|
||||
### Prompt Content
|
||||
- [ ] Clear agent identity and role defined
|
||||
- [ ] Core responsibilities listed explicitly
|
||||
- [ ] Approach and methodology explained
|
||||
- [ ] Guidelines and constraints specified
|
||||
- [ ] Output expectations documented
|
||||
- [ ] Examples provided where helpful
|
||||
- [ ] Instructions are specific and actionable
|
||||
- [ ] Scope and boundaries clearly defined
|
||||
- [ ] Total content under 30,000 characters
|
||||
|
||||
### File Structure
|
||||
- [ ] Filename follows lowercase-with-hyphens convention
|
||||
- [ ] File placed in correct directory (`.github/agents/` or `agents/`)
|
||||
- [ ] Filename uses only allowed characters
|
||||
- [ ] File extension is `.agent.md`
|
||||
|
||||
### Quality Assurance
|
||||
- [ ] Agent purpose is unique and not duplicative
|
||||
- [ ] Tools are minimal and necessary
|
||||
- [ ] Instructions are clear and unambiguous
|
||||
- [ ] Agent has been tested with representative tasks
|
||||
- [ ] Documentation references are current
|
||||
- [ ] Security considerations addressed (if applicable)
|
||||
|
||||
## Common Agent Patterns
|
||||
|
||||
### Testing Specialist
|
||||
**Purpose**: Focus on test coverage and quality
|
||||
**Tools**: All tools (for comprehensive test creation)
|
||||
**Approach**: Analyze, identify gaps, write tests, avoid production code changes
|
||||
|
||||
### Implementation Planner
|
||||
**Purpose**: Create detailed technical plans and specifications
|
||||
**Tools**: Limited to `['read', 'search', 'edit']`
|
||||
**Approach**: Analyze requirements, create documentation, avoid implementation
|
||||
|
||||
### Code Reviewer
|
||||
**Purpose**: Review code quality and provide feedback
|
||||
**Tools**: `['read', 'search']` only
|
||||
**Approach**: Analyze, suggest improvements, no direct modifications
|
||||
|
||||
### Refactoring Specialist
|
||||
**Purpose**: Improve code structure and maintainability
|
||||
**Tools**: `['read', 'search', 'edit']`
|
||||
**Approach**: Analyze patterns, propose refactorings, implement safely
|
||||
|
||||
### Security Auditor
|
||||
**Purpose**: Identify security issues and vulnerabilities
|
||||
**Tools**: `['read', 'search', 'web']`
|
||||
**Approach**: Scan code, check against OWASP, report findings
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### Frontmatter Errors
|
||||
- ❌ Missing `description` field
|
||||
- ❌ Description not wrapped in quotes
|
||||
- ❌ Invalid tool names without checking documentation
|
||||
- ❌ Incorrect YAML syntax (indentation, quotes)
|
||||
|
||||
### Tool Configuration Issues
|
||||
- ❌ Granting excessive tool access unnecessarily
|
||||
- ❌ Missing required tools for agent's purpose
|
||||
- ❌ Not using tool aliases consistently
|
||||
- ❌ Forgetting MCP server namespace (`server-name/tool`)
|
||||
|
||||
### Prompt Content Problems
|
||||
- ❌ Vague, ambiguous instructions
|
||||
- ❌ Conflicting or contradictory guidelines
|
||||
- ❌ Lack of clear scope definition
|
||||
- ❌ Missing output expectations
|
||||
- ❌ Overly verbose instructions (exceeding character limits)
|
||||
- ❌ No examples or context for complex tasks
|
||||
|
||||
### Organizational Issues
|
||||
- ❌ Filename doesn't reflect agent purpose
|
||||
- ❌ Wrong directory (confusing repo vs org level)
|
||||
- ❌ Using spaces or special characters in filename
|
||||
- ❌ Duplicate agent names causing conflicts
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### Manual Testing
|
||||
1. Create the agent file with proper frontmatter
|
||||
2. Reload VS Code or refresh GitHub.com
|
||||
3. Select the agent from the dropdown in Copilot Chat
|
||||
4. Test with representative user queries
|
||||
5. Verify tool access works as expected
|
||||
6. Confirm output meets expectations
|
||||
|
||||
### Integration Testing
|
||||
- Test agent with different file types in scope
|
||||
- Verify MCP server connectivity (if configured)
|
||||
- Check agent behavior with missing context
|
||||
- Test error handling and edge cases
|
||||
- Validate agent switching and handoffs
|
||||
|
||||
### Quality Checks
|
||||
- Run through agent creation checklist
|
||||
- Review against common mistakes list
|
||||
- Compare with example agents in repository
|
||||
- Get peer review for complex agents
|
||||
- Document any special configuration needs
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Official Documentation
|
||||
- [Creating Custom Agents](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents)
|
||||
- [Custom Agents Configuration](https://docs.github.com/en/copilot/reference/custom-agents-configuration)
|
||||
- [Custom Agents in VS Code](https://code.visualstudio.com/docs/copilot/customization/custom-agents)
|
||||
- [MCP Integration](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/extend-coding-agent-with-mcp)
|
||||
|
||||
### Community Resources
|
||||
- [Awesome Copilot Agents Collection](https://github.com/github/awesome-copilot/tree/main/agents)
|
||||
- [Customization Library Examples](https://docs.github.com/en/copilot/tutorials/customization-library/custom-agents)
|
||||
- [Your First Custom Agent Tutorial](https://docs.github.com/en/copilot/tutorials/customization-library/custom-agents/your-first-custom-agent)
|
||||
|
||||
### Related Files
|
||||
- [Prompt Files Guidelines](./prompt.instructions.md) - For creating prompt files
|
||||
- [Instructions Guidelines](./instructions.instructions.md) - For creating instruction files
|
||||
|
||||
## Version Compatibility Notes
|
||||
|
||||
### GitHub.com (Coding Agent)
|
||||
- ✅ Fully supports all standard frontmatter properties
|
||||
- ✅ Repository and org/enterprise level agents
|
||||
- ✅ MCP server configuration (org/enterprise)
|
||||
- ❌ Does not support `model`, `argument-hint`, `handoffs` properties
|
||||
|
||||
### VS Code / JetBrains / Eclipse / Xcode
|
||||
- ✅ Supports `model` property for AI model selection
|
||||
- ✅ Supports `argument-hint` and `handoffs` properties
|
||||
- ✅ User profile and workspace-level agents
|
||||
- ❌ Cannot configure MCP servers at repository level
|
||||
- ⚠️ Some properties may behave differently
|
||||
|
||||
When creating agents for multiple environments, focus on common properties and test in all target environments. Use `target` property to create environment-specific agents when necessary.
|
||||
@@ -1,187 +0,0 @@
|
||||
---
|
||||
description: 'Best practices for Azure DevOps Pipeline YAML files'
|
||||
applyTo: '**/azure-pipelines.yml, **/azure-pipelines*.yml, **/*.pipeline.yml'
|
||||
---
|
||||
|
||||
# Azure DevOps Pipeline YAML Best Practices
|
||||
|
||||
Guidelines for creating maintainable, secure, and efficient Azure DevOps pipelines in PowerToys.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
- Use YAML syntax consistently with proper indentation (2 spaces)
|
||||
- Always include meaningful names and display names for pipelines, stages, jobs, and steps
|
||||
- Implement proper error handling and conditional execution
|
||||
- Use variables and parameters to make pipelines reusable and maintainable
|
||||
- Follow the principle of least privilege for service connections and permissions
|
||||
- Include comprehensive logging and diagnostics for troubleshooting
|
||||
|
||||
## Pipeline Structure
|
||||
|
||||
- Organize complex pipelines using stages for better visualization and control
|
||||
- Use jobs to group related steps and enable parallel execution when possible
|
||||
- Implement proper dependencies between stages and jobs
|
||||
- Use templates for reusable pipeline components
|
||||
- Keep pipeline files focused and modular - split large pipelines into multiple files
|
||||
|
||||
## Build Best Practices
|
||||
|
||||
- Use specific agent pool versions and VM images for consistency
|
||||
- Cache dependencies (npm, NuGet, Maven, etc.) to improve build performance
|
||||
- Implement proper artifact management with meaningful names and retention policies
|
||||
- Use build variables for version numbers and build metadata
|
||||
- Include code quality gates (lint checks, testing, security scans)
|
||||
- Ensure builds are reproducible and environment-independent
|
||||
|
||||
## Testing Integration
|
||||
|
||||
- Run unit tests as part of the build process
|
||||
- Publish test results in standard formats (JUnit, VSTest, etc.)
|
||||
- Include code coverage reporting and quality gates
|
||||
- Implement integration and end-to-end tests in appropriate stages
|
||||
- Use test impact analysis when available to optimize test execution
|
||||
- Fail fast on test failures to provide quick feedback
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Use Azure Key Vault for sensitive configuration and secrets
|
||||
- Implement proper secret management with variable groups
|
||||
- Use service connections with minimal required permissions
|
||||
- Enable security scans (dependency vulnerabilities, static analysis)
|
||||
- Implement approval gates for production deployments
|
||||
- Use managed identities when possible instead of service principals
|
||||
|
||||
## Deployment Strategies
|
||||
|
||||
- Implement proper environment promotion (dev → staging → production)
|
||||
- Use deployment jobs with proper environment targeting
|
||||
- Implement blue-green or canary deployment strategies when appropriate
|
||||
- Include rollback mechanisms and health checks
|
||||
- Use infrastructure as code (ARM, Bicep, Terraform) for consistent deployments
|
||||
- Implement proper configuration management per environment
|
||||
|
||||
## Variable and Parameter Management
|
||||
|
||||
- Use variable groups for shared configuration across pipelines
|
||||
- Implement runtime parameters for flexible pipeline execution
|
||||
- Use conditional variables based on branches or environments
|
||||
- Secure sensitive variables and mark them as secrets
|
||||
- Document variable purposes and expected values
|
||||
- Use variable templates for complex variable logic
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
- Use parallel jobs and matrix strategies when appropriate
|
||||
- Implement proper caching strategies for dependencies and build outputs
|
||||
- Use shallow clone for Git operations when full history isn't needed
|
||||
- Optimize Docker image builds with multi-stage builds and layer caching
|
||||
- Monitor pipeline performance and optimize bottlenecks
|
||||
- Use pipeline resource triggers efficiently
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
- Include comprehensive logging throughout the pipeline
|
||||
- Use Azure Monitor and Application Insights for deployment tracking
|
||||
- Implement proper notification strategies for failures and successes
|
||||
- Include deployment health checks and automated rollback triggers
|
||||
- Use pipeline analytics to identify improvement opportunities
|
||||
- Document pipeline behavior and troubleshooting steps
|
||||
|
||||
## Template and Reusability
|
||||
|
||||
- Create pipeline templates for common patterns
|
||||
- Use extends templates for complete pipeline inheritance
|
||||
- Implement step templates for reusable task sequences
|
||||
- Use variable templates for complex variable logic
|
||||
- Version templates appropriately for stability
|
||||
- Document template parameters and usage examples
|
||||
|
||||
## Branch and Trigger Strategy
|
||||
|
||||
- Implement appropriate triggers for different branch types
|
||||
- Use path filters to trigger builds only when relevant files change
|
||||
- Configure proper CI/CD triggers for main/master branches
|
||||
- Use pull request triggers for code validation
|
||||
- Implement scheduled triggers for maintenance tasks
|
||||
- Consider resource triggers for multi-repository scenarios
|
||||
|
||||
## Example Structure
|
||||
|
||||
```yaml
|
||||
# azure-pipelines.yml
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- docs/*
|
||||
- README.md
|
||||
|
||||
variables:
|
||||
- group: shared-variables
|
||||
- name: buildConfiguration
|
||||
value: 'Release'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: 'Build and Test'
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: 'Build Application'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET SDK'
|
||||
inputs:
|
||||
version: '8.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Restore dependencies'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
projects: '**/*.csproj'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build application'
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: '**/*.csproj'
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore'
|
||||
|
||||
- stage: Deploy
|
||||
displayName: 'Deploy to Staging'
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
jobs:
|
||||
- deployment: DeployToStaging
|
||||
displayName: 'Deploy to Staging Environment'
|
||||
environment: 'staging'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- download: current
|
||||
displayName: 'Download drop artifact'
|
||||
artifact: drop
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Deploy to Azure Web App'
|
||||
inputs:
|
||||
azureSubscription: 'staging-service-connection'
|
||||
appType: 'webApp'
|
||||
appName: 'myapp-staging'
|
||||
package: '$(Pipeline.Workspace)/drop/**/*.zip'
|
||||
```
|
||||
|
||||
## Common Anti-Patterns to Avoid
|
||||
|
||||
- Hardcoding sensitive values directly in YAML files
|
||||
- Using overly broad triggers that cause unnecessary builds
|
||||
- Mixing build and deployment logic in a single stage
|
||||
- Not implementing proper error handling and cleanup
|
||||
- Using deprecated task versions without upgrade plans
|
||||
- Creating monolithic pipelines that are difficult to maintain
|
||||
- Not using proper naming conventions for clarity
|
||||
- Ignoring pipeline security best practices
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for shared libraries including logging, IPC, settings, DPI, telemetry, and utilities consumed by multiple modules'
|
||||
applyTo: 'src/common/**'
|
||||
---
|
||||
|
||||
# Common Libraries – Shared Code Guidance
|
||||
|
||||
Guidelines for modifying shared code in `src/common/`. Changes here can have wide-reaching impact across the entire PowerToys codebase.
|
||||
|
||||
## Scope
|
||||
|
||||
- Logging infrastructure (`src/common/logger/`)
|
||||
- IPC primitives and named pipe utilities
|
||||
- Settings serialization and management
|
||||
- DPI awareness and scaling utilities
|
||||
- Telemetry helpers
|
||||
- General utilities (JSON parsing, string helpers, etc.)
|
||||
|
||||
## Guidelines
|
||||
|
||||
### API Stability
|
||||
|
||||
- Avoid breaking public headers/APIs; if changed, search & update all callers
|
||||
- Coordinate ABI-impacting struct/class layout changes; keep binary compatibility
|
||||
- When modifying public interfaces, grep the entire codebase for usages
|
||||
|
||||
### Performance
|
||||
|
||||
- Watch perf in hot paths (hooks, timers, serialization)
|
||||
- Avoid avoidable allocations in frequently called code
|
||||
- Profile changes that touch performance-sensitive areas
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Ask before adding third-party deps or changing serialization formats
|
||||
- New dependencies must be MIT-licensed or approved by PM team
|
||||
- Add any new external packages to `NOTICE.md`
|
||||
|
||||
### Logging
|
||||
|
||||
- C++ logging uses spdlog (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`)
|
||||
- Initialize with `init_logger()` early in startup
|
||||
- Keep hot paths quiet – no logging in tight loops or hooks
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- No unintended ABI breaks
|
||||
- No noisy logs in hot paths
|
||||
- New non-obvious symbols briefly commented
|
||||
- All callers updated when interfaces change
|
||||
|
||||
## Code Style
|
||||
|
||||
- **C++**: Follow `.clang-format` in `src/`; use Modern C++ patterns per C++ Core Guidelines
|
||||
- **C#**: Follow `src/.editorconfig`; enforce StyleCop.Analyzers
|
||||
|
||||
## Validation
|
||||
|
||||
- Build: `tools\build\build.cmd` from `src/common/` folder
|
||||
- Verify no ABI breaks: grep for changed function/struct names across codebase
|
||||
- Check logs: ensure no new logging in performance-critical paths
|
||||
256
.github/instructions/instructions.instructions.md
vendored
256
.github/instructions/instructions.instructions.md
vendored
@@ -1,256 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for creating high-quality custom instruction files for GitHub Copilot'
|
||||
applyTo: '**/*.instructions.md'
|
||||
---
|
||||
|
||||
# Custom Instructions File Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable custom instruction files that guide GitHub Copilot in generating domain-specific code and following project conventions.
|
||||
|
||||
## Project Context
|
||||
|
||||
- Target audience: Developers and GitHub Copilot working with domain-specific code
|
||||
- File format: Markdown with YAML frontmatter
|
||||
- File naming convention: lowercase with hyphens (e.g., `react-best-practices.instructions.md`)
|
||||
- Location: `.github/instructions/` directory
|
||||
- Purpose: Provide context-aware guidance for code generation, review, and documentation
|
||||
|
||||
## Required Frontmatter
|
||||
|
||||
Every instruction file must include YAML frontmatter with the following fields:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: 'Brief description of the instruction purpose and scope'
|
||||
applyTo: 'glob pattern for target files (e.g., **/*.ts, **/*.py)'
|
||||
---
|
||||
```
|
||||
|
||||
### Frontmatter Guidelines
|
||||
|
||||
- **description**: Single-quoted string, 1-500 characters, clearly stating the purpose
|
||||
- **applyTo**: Glob pattern(s) specifying which files these instructions apply to
|
||||
- Single pattern: `'**/*.ts'`
|
||||
- Multiple patterns: `'**/*.ts, **/*.tsx, **/*.js'`
|
||||
- Specific files: `'src/**/*.py'`
|
||||
- All files: `'**'`
|
||||
|
||||
## File Structure
|
||||
|
||||
A well-structured instruction file should include the following sections:
|
||||
|
||||
### 1. Title and Overview
|
||||
|
||||
- Clear, descriptive title using `#` heading
|
||||
- Brief introduction explaining the purpose and scope
|
||||
- Optional: Project context section with key technologies and versions
|
||||
|
||||
### 2. Core Sections
|
||||
|
||||
Organize content into logical sections based on the domain:
|
||||
|
||||
- **General Instructions**: High-level guidelines and principles
|
||||
- **Best Practices**: Recommended patterns and approaches
|
||||
- **Code Standards**: Naming conventions, formatting, style rules
|
||||
- **Architecture/Structure**: Project organization and design patterns
|
||||
- **Common Patterns**: Frequently used implementations
|
||||
- **Security**: Security considerations (if applicable)
|
||||
- **Performance**: Optimization guidelines (if applicable)
|
||||
- **Testing**: Testing standards and approaches (if applicable)
|
||||
|
||||
### 3. Examples and Code Snippets
|
||||
|
||||
Provide concrete examples with clear labels:
|
||||
|
||||
```markdown
|
||||
### Good Example
|
||||
\`\`\`language
|
||||
// Recommended approach
|
||||
code example here
|
||||
\`\`\`
|
||||
|
||||
### Bad Example
|
||||
\`\`\`language
|
||||
// Avoid this pattern
|
||||
code example here
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### 4. Validation and Verification (Optional but Recommended)
|
||||
|
||||
- Build commands to verify code
|
||||
- Lint checks and formatting tools
|
||||
- Testing requirements
|
||||
- Verification steps
|
||||
|
||||
## Content Guidelines
|
||||
|
||||
### Writing Style
|
||||
|
||||
- Use clear, concise language
|
||||
- Write in imperative mood ("Use", "Implement", "Avoid")
|
||||
- Be specific and actionable
|
||||
- Avoid ambiguous terms like "should", "might", "possibly"
|
||||
- Use bullet points and lists for readability
|
||||
- Keep sections focused and scannable
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **Be Specific**: Provide concrete examples rather than abstract concepts
|
||||
- **Show Why**: Explain the reasoning behind recommendations when it adds value
|
||||
- **Use Tables**: For comparing options, listing rules, or showing patterns
|
||||
- **Include Examples**: Real code snippets are more effective than descriptions
|
||||
- **Stay Current**: Reference current versions and best practices
|
||||
- **Link Resources**: Include official documentation and authoritative sources
|
||||
|
||||
### Common Patterns to Include
|
||||
|
||||
1. **Naming Conventions**: How to name variables, functions, classes, files
|
||||
2. **Code Organization**: File structure, module organization, import order
|
||||
3. **Error Handling**: Preferred error handling patterns
|
||||
4. **Dependencies**: How to manage and document dependencies
|
||||
5. **Comments and Documentation**: When and how to document code
|
||||
6. **Version Information**: Target language/framework versions
|
||||
|
||||
## Patterns to Follow
|
||||
|
||||
### Bullet Points and Lists
|
||||
|
||||
```markdown
|
||||
## Security Best Practices
|
||||
|
||||
- Always validate user input before processing
|
||||
- Use parameterized queries to prevent SQL injection
|
||||
- Store secrets in environment variables, never in code
|
||||
- Implement proper authentication and authorization
|
||||
- Enable HTTPS for all production endpoints
|
||||
```
|
||||
|
||||
### Tables for Structured Information
|
||||
|
||||
```markdown
|
||||
## Common Issues
|
||||
|
||||
| Issue | Solution | Example |
|
||||
| ---------------- | ------------------- | ----------------------------- |
|
||||
| Magic numbers | Use named constants | `const MAX_RETRIES = 3` |
|
||||
| Deep nesting | Extract functions | Refactor nested if statements |
|
||||
| Hardcoded values | Use configuration | Store API URLs in config |
|
||||
```
|
||||
|
||||
### Code Comparison
|
||||
|
||||
```markdown
|
||||
### Good Example - Using TypeScript interfaces
|
||||
\`\`\`typescript
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
function getUser(id: string): User {
|
||||
// Implementation
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Bad Example - Using any type
|
||||
\`\`\`typescript
|
||||
function getUser(id: any): any {
|
||||
// Loses type safety
|
||||
}
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### Conditional Guidance
|
||||
|
||||
```markdown
|
||||
## Framework Selection
|
||||
|
||||
- **For small projects**: Use Minimal API approach
|
||||
- **For large projects**: Use controller-based architecture with clear separation
|
||||
- **For microservices**: Consider domain-driven design patterns
|
||||
```
|
||||
|
||||
## Patterns to Avoid
|
||||
|
||||
- **Overly verbose explanations**: Keep it concise and scannable
|
||||
- **Outdated information**: Always reference current versions and practices
|
||||
- **Ambiguous guidelines**: Be specific about what to do or avoid
|
||||
- **Missing examples**: Abstract rules without concrete code examples
|
||||
- **Contradictory advice**: Ensure consistency throughout the file
|
||||
- **Copy-paste from documentation**: Add value by distilling and providing context
|
||||
|
||||
## Testing Your Instructions
|
||||
|
||||
Before finalizing instruction files:
|
||||
|
||||
1. **Test with Copilot**: Try the instructions with actual prompts in VS Code
|
||||
2. **Verify Examples**: Ensure code examples are correct and run without errors
|
||||
3. **Check Glob Patterns**: Confirm `applyTo` patterns match intended files
|
||||
|
||||
## Example Structure
|
||||
|
||||
Here's a minimal example structure for a new instruction file:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: 'Brief description of purpose'
|
||||
applyTo: '**/*.ext'
|
||||
---
|
||||
|
||||
# Technology Name Development
|
||||
|
||||
Brief introduction and context.
|
||||
|
||||
## General Instructions
|
||||
|
||||
- High-level guideline 1
|
||||
- High-level guideline 2
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Specific practice 1
|
||||
- Specific practice 2
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Naming Conventions
|
||||
- Rule 1
|
||||
- Rule 2
|
||||
|
||||
### File Organization
|
||||
- Structure 1
|
||||
- Structure 2
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1
|
||||
Description and example
|
||||
|
||||
\`\`\`language
|
||||
code example
|
||||
\`\`\`
|
||||
|
||||
### Pattern 2
|
||||
Description and example
|
||||
|
||||
## Validation
|
||||
|
||||
- Build command: `command to verify`
|
||||
- Lint checks: `command to lint`
|
||||
- Testing: `command to test`
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
- Review instructions when dependencies or frameworks are updated
|
||||
- Update examples to reflect current best practices
|
||||
- Remove outdated patterns or deprecated features
|
||||
- Add new patterns as they emerge in the community
|
||||
- Keep glob patterns accurate as project structure evolves
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Custom Instructions Documentation](https://code.visualstudio.com/docs/copilot/customization/custom-instructions)
|
||||
- [Awesome Copilot Instructions](https://github.com/github/awesome-copilot/tree/main/instructions)
|
||||
88
.github/instructions/prompt.instructions.md
vendored
88
.github/instructions/prompt.instructions.md
vendored
@@ -1,88 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for creating high-quality prompt files for GitHub Copilot'
|
||||
applyTo: '**/*.prompt.md'
|
||||
---
|
||||
|
||||
# Copilot Prompt Files Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable prompt files that guide GitHub Copilot in delivering consistent, high-quality outcomes across any repository.
|
||||
|
||||
## Scope and Principles
|
||||
- Target audience: maintainers and contributors authoring reusable prompts for Copilot Chat.
|
||||
- Goals: predictable behaviour, clear expectations, minimal permissions, and portability across repositories.
|
||||
- Primary references: VS Code documentation on prompt files and organization-specific conventions.
|
||||
|
||||
## Frontmatter Requirements
|
||||
|
||||
Every prompt file should include YAML frontmatter with the following fields:
|
||||
|
||||
### Required/Recommended Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `description` | Recommended | A short description of the prompt (single sentence, actionable outcome) |
|
||||
| `name` | Optional | The name shown after typing `/` in chat. Defaults to filename if not specified |
|
||||
| `agent` | Recommended | The agent to use: `ask`, `edit`, `agent`, or a custom agent name. Defaults to current agent |
|
||||
| `model` | Optional | The language model to use. Defaults to currently selected model |
|
||||
| `tools` | Optional | List of tool/tool set names available for this prompt |
|
||||
| `argument-hint` | Optional | Hint text shown in chat input to guide user interaction |
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Use consistent quoting (single quotes recommended) and keep one field per line for readability and version control clarity
|
||||
- If `tools` are specified and current agent is `ask` or `edit`, the default agent becomes `agent`
|
||||
- Preserve any additional metadata (`language`, `tags`, `visibility`, etc.) required by your organization
|
||||
|
||||
## File Naming and Placement
|
||||
- Use kebab-case filenames ending with `.prompt.md` and store them under `.github/prompts/` unless your workspace standard specifies another directory.
|
||||
- Provide a short filename that communicates the action (for example, `generate-readme.prompt.md` rather than `prompt1.prompt.md`).
|
||||
|
||||
## Body Structure
|
||||
- Start with an `#` level heading that matches the prompt intent so it surfaces well in Quick Pick search.
|
||||
- Organize content with predictable sections. Recommended baseline: `Mission` or `Primary Directive`, `Scope & Preconditions`, `Inputs`, `Workflow` (step-by-step), `Output Expectations`, and `Quality Assurance`.
|
||||
- Adjust section names to fit the domain, but retain the logical flow: why → context → inputs → actions → outputs → validation.
|
||||
- Reference related prompts or instruction files using relative links to aid discoverability.
|
||||
|
||||
## Input and Context Handling
|
||||
- Use `${input:variableName[:placeholder]}` for required values and explain when the user must supply them. Provide defaults or alternatives where possible.
|
||||
- Call out contextual variables such as `${selection}`, `${file}`, `${workspaceFolder}` only when they are essential, and describe how Copilot should interpret them.
|
||||
- Document how to proceed when mandatory context is missing (for example, “Request the file path and stop if it remains undefined”).
|
||||
|
||||
## Tool and Permission Guidance
|
||||
- Limit `tools` to the smallest set that enables the task. List them in the preferred execution order when the sequence matters.
|
||||
- If the prompt inherits tools from a chat mode, mention that relationship and state any critical tool behaviours or side effects.
|
||||
- Warn about destructive operations (file creation, edits, terminal commands) and include guard rails or confirmation steps in the workflow.
|
||||
|
||||
## Instruction Tone and Style
|
||||
- Write in direct, imperative sentences targeted at Copilot (for example, “Analyze”, “Generate”, “Summarize”).
|
||||
- Keep sentences short and unambiguous, following Google Developer Documentation translation best practices to support localization.
|
||||
- Avoid idioms, humor, or culturally specific references; favor neutral, inclusive language.
|
||||
|
||||
## Output Definition
|
||||
- Specify the format, structure, and location of expected results (for example, “Create an architecture decision record file using the template below, such as `docs/architecture-decisions/record-XXXX.md`).
|
||||
- Include success criteria and failure triggers so Copilot knows when to halt or retry.
|
||||
- Provide validation steps—manual checks, automated commands, or acceptance criteria lists—that reviewers can execute after running the prompt.
|
||||
|
||||
## Examples and Reusable Assets
|
||||
- Embed Good/Bad examples or scaffolds (Markdown templates, JSON stubs) that the prompt should produce or follow.
|
||||
- Maintain reference tables (capabilities, status codes, role descriptions) inline to keep the prompt self-contained. Update these tables when upstream resources change.
|
||||
- Link to authoritative documentation instead of duplicating lengthy guidance.
|
||||
|
||||
## Quality Assurance Checklist
|
||||
- [ ] Frontmatter fields are complete, accurate, and least-privilege.
|
||||
- [ ] Inputs include placeholders, default behaviours, and fallbacks.
|
||||
- [ ] Workflow covers preparation, execution, and post-processing without gaps.
|
||||
- [ ] Output expectations include formatting and storage details.
|
||||
- [ ] Validation steps are actionable (commands, diff checks, review prompts).
|
||||
- [ ] Security, compliance, and privacy policies referenced by the prompt are current.
|
||||
- [ ] Prompt executes successfully in VS Code (`Chat: Run Prompt`) using representative scenarios.
|
||||
|
||||
## Maintenance Guidance
|
||||
- Version-control prompts alongside the code they affect; update them when dependencies, tooling, or review processes change.
|
||||
- Review prompts periodically to ensure tool lists, model requirements, and linked documents remain valid.
|
||||
- Coordinate with other repositories: when a prompt proves broadly useful, extract common guidance into instruction files or shared prompt packs.
|
||||
|
||||
## Additional Resources
|
||||
- [Prompt Files Documentation](https://code.visualstudio.com/docs/copilot/customization/prompt-files#_prompt-file-format)
|
||||
- [Awesome Copilot Prompt Files](https://github.com/github/awesome-copilot/tree/main/prompts)
|
||||
- [Tool Configuration](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode#_agent-mode-tools)
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for Runner and Settings UI components that communicate via named pipes and manage module lifecycle'
|
||||
applyTo: 'src/runner/**,src/settings-ui/**'
|
||||
---
|
||||
|
||||
# Runner & Settings UI – Core Components Guidance
|
||||
|
||||
Guidelines for modifying the Runner (tray/module loader) and Settings UI (configuration app). These components communicate via Windows Named Pipes using JSON messages.
|
||||
|
||||
## Runner (`src/runner/`)
|
||||
|
||||
### Scope
|
||||
|
||||
- Module bootstrap, hotkey management, settings bridge, update/elevation handling
|
||||
|
||||
### Guidelines
|
||||
|
||||
- If IPC/JSON contracts change, mirror updates in `src/settings-ui/**`
|
||||
- Keep module discovery in `src/runner/main.cpp` in sync when adding/removing modules
|
||||
- Keep startup lean: avoid blocking/network calls in early init path
|
||||
- Preserve GPO & elevation behaviors; confirm no regression in policy handling
|
||||
- Ask before modifying update workflow or elevation logic
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Stable startup, consistent contracts, no unnecessary logging noise
|
||||
|
||||
## Settings UI (`src/settings-ui/`)
|
||||
|
||||
### Scope
|
||||
|
||||
- WinUI/WPF UI, communicates with Runner over named pipes; manages persisted settings schema
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Don't break settings schema silently; add migration when shape changes
|
||||
- If IPC/JSON contracts change, align with `src/runner/**` implementation
|
||||
- Keep UI responsive: marshal to UI thread for UI-bound operations
|
||||
- Reuse existing styles/resources; avoid duplicate theme keys
|
||||
- Add/adjust migration or serialization tests when changing persisted settings
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Schema integrity preserved, responsive UI, consistent contracts, no style duplication
|
||||
|
||||
## Shared Concerns
|
||||
|
||||
### IPC Contract Changes
|
||||
|
||||
When modifying the JSON message format between Runner and Settings UI:
|
||||
|
||||
1. Update both `src/runner/` and `src/settings-ui/` in the same PR
|
||||
2. Preserve backward compatibility where possible
|
||||
3. Add migration logic for settings schema changes
|
||||
4. Test both directions of communication
|
||||
|
||||
### Code Style
|
||||
|
||||
- **C++ (Runner)**: Follow `.clang-format` in `src/`
|
||||
- **C# (Settings UI)**: Follow `src/.editorconfig`, use StyleCop.Analyzers
|
||||
- **XAML**: Use XamlStyler or run `.\.pipelines\applyXamlStyling.ps1 -Main`
|
||||
|
||||
## Validation
|
||||
|
||||
- Build Runner: `tools\build\build.cmd` from `src/runner/`
|
||||
- Build Settings UI: `tools\build\build.cmd` from `src/settings-ui/`
|
||||
- Test IPC: Launch both Runner and Settings UI, verify communication works
|
||||
- Schema changes: Run serialization tests if settings shape changed
|
||||
58
.github/prompts/create-commit-title.prompt.md
vendored
58
.github/prompts/create-commit-title.prompt.md
vendored
@@ -1,50 +1,16 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate an 80-character git commit title for the local diff'
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: 'Generate an 80-character git commit title for the local diff.'
|
||||
---
|
||||
|
||||
# Generate Commit Title
|
||||
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
|
||||
|
||||
## Purpose
|
||||
Provide a single-line, ready-to-paste git commit title (<= 80 characters) that reflects the most important local changes since `HEAD`.
|
||||
|
||||
## Input to collect
|
||||
- Run exactly one command to view the local diff:
|
||||
```@terminal
|
||||
git diff HEAD
|
||||
```
|
||||
|
||||
## How to decide the title
|
||||
1. From the diff, find the dominant area (e.g., `src/modules/*`, `doc/devdocs/**`) and the change type (bug fix, docs update, config tweak).
|
||||
2. Draft an imperative, plain-ASCII title that:
|
||||
- Mentions the primary component when obvious (e.g., `FancyZones:` or `Docs:`)
|
||||
- Stays within 80 characters and has no trailing punctuation
|
||||
|
||||
## Final output
|
||||
- Reply with only the commit title on a single line—no extra text.
|
||||
|
||||
## PR title convention (when asked)
|
||||
Use Conventional Commits style:
|
||||
|
||||
`<type>(<scope>): <summary>`
|
||||
|
||||
**Allowed types**
|
||||
- feat, fix, docs, refactor, perf, test, build, ci, chore
|
||||
|
||||
**Scope rules**
|
||||
- Use a short, PowerToys-focused scope (one word preferred). Common scopes:
|
||||
- Core: `runner`, `settings-ui`, `common`, `docs`, `build`, `ci`, `installer`, `gpo`, `dsc`
|
||||
- Modules: `fancyzones`, `powerrename`, `awake`, `colorpicker`, `imageresizer`, `keyboardmanager`, `mouseutils`, `peek`, `hosts`, `file-locksmith`, `screen-ruler`, `text-extractor`, `cropandlock`, `paste`, `powerlauncher`
|
||||
- If unclear, pick the closest module or subsystem; omit only if unavoidable
|
||||
|
||||
**Summary rules**
|
||||
- Imperative, present tense (“add”, “update”, “remove”, “fix”)
|
||||
- Keep it <= 72 characters when possible; be specific, avoid “misc changes”
|
||||
|
||||
**Examples**
|
||||
- `feat(fancyzones): add canvas template duplication`
|
||||
- `fix(mouseutils): guard crosshair toggle when dpi info missing`
|
||||
- `docs(runner): document tray icon states`
|
||||
- `build(installer): align wix v5 suffix flag`
|
||||
- `ci(ci): cache pipeline artifacts for x64`
|
||||
**Workflow:**
|
||||
1. Run a single command to view the local diff since the last commit:
|
||||
```@terminal
|
||||
git diff HEAD
|
||||
```
|
||||
2. From that diff, identify the dominant area (reference key paths like `src/modules/*`, `doc/devdocs/**`, etc.), the type of change (bug fix, docs update, config tweak), and any notable impact.
|
||||
3. Draft a concise, imperative commit title summarizing the dominant change. Keep it plain ASCII, <= 80 characters, and avoid trailing punctuation. Mention the primary component when obvious (for example `FancyZones:` or `Docs:`).
|
||||
4. Respond with only the final commit title on a single line so it can be pasted directly into `git commit`.
|
||||
|
||||
9
.github/prompts/create-pr-summary.prompt.md
vendored
9
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -1,11 +1,9 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff'
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff.'
|
||||
---
|
||||
|
||||
# Generate PR Summary
|
||||
|
||||
**Goal:** Produce a ready-to-paste PR title and description that follows PowerToys conventions by comparing the current branch against a user-selected target branch.
|
||||
|
||||
**Repo guardrails:**
|
||||
@@ -22,4 +20,3 @@ description: 'Generate a PowerToys-ready pull request description from the local
|
||||
5. Confirm validation: list tests executed with results or state why tests were skipped in line with repo guidance.
|
||||
6. Load `.github/pull_request_template.md`, mirror its section order, and populate it with the gathered facts. Include only relevant checklist entries, marking them `[x]/[ ]` and noting any intentional omissions as "N/A".
|
||||
7. Present the filled template inside a fenced ```markdown code block with no extra commentary so it is ready to paste into a PR, clearly flagging any placeholders that still need user input.
|
||||
8. Prepend the PR title above the filled template, applying the Conventional Commit type/scope rules from `.github/prompts/create-commit-title.prompt.md`; pick the dominant component from the diff and keep the title concise and imperative.
|
||||
|
||||
10
.github/prompts/fix-issue.prompt.md
vendored
10
.github/prompts/fix-issue.prompt.md
vendored
@@ -1,12 +1,10 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Execute the fix for a GitHub issue using the previously generated implementation plan'
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: " Execute the fix for a GitHub issue using the previously generated implementation plan. Apply code & tests directly in the repo. Output only a PR description (and optional manual steps)."
|
||||
---
|
||||
|
||||
# Fix GitHub Issue
|
||||
|
||||
## Dependencies
|
||||
# DEPENDENCY
|
||||
Source review prompt (for generating the implementation plan if missing):
|
||||
- .github/prompts/review-issue.prompt.md
|
||||
|
||||
|
||||
17
.github/prompts/fix-spelling.prompt.md
vendored
17
.github/prompts/fix-spelling.prompt.md
vendored
@@ -1,17 +1,15 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR.'
|
||||
---
|
||||
|
||||
# Fix Spelling Comments
|
||||
|
||||
**Goal:** Clear every outstanding GitHub pull request comment created by the `Code scanning / check-spelling` workflow by explicitly allowing intentional terms.
|
||||
|
||||
**Guardrails:**
|
||||
- Update only discussion threads authored by `github-actions` or `github-actions[bot]` that mention `Code scanning results / check-spelling`.
|
||||
- Prefer improving the wording in the originally flagged file when it clarifies intent without changing meaning; if the wording is already clear/standard for the context, handle it via `.github/actions/spell-check/expect.txt` and reuse existing entries.
|
||||
- Limit edits to the flagged text and `.github/actions/spell-check/expect.txt`; leave all other files and topics untouched.
|
||||
- Resolve findings solely by editing `.github/actions/spell-check/expect.txt`; reuse existing entries.
|
||||
- Leave all other files and topics untouched.
|
||||
|
||||
**Prerequisites:**
|
||||
- Install GitHub CLI if it is not present: `winget install GitHub.cli`.
|
||||
@@ -20,6 +18,5 @@ description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
**Workflow:**
|
||||
1. Determine the active pull request with a single `gh pr view --json number` call (default to the current branch).
|
||||
2. Fetch all PR discussion data once via `gh pr view --json comments,reviews` and filter to check-spelling comments authored by `github-actions` or `github-actions[bot]` that are not minimized; when several remain, process only the most recent comment body.
|
||||
3. For each flagged token, first consider tightening or rephrasing the original text to avoid the false positive while keeping the meaning intact; if the existing wording is already normal and professional for the context, proceed to allowlisting instead of changing it.
|
||||
4. When allowlisting, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
|
||||
5. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.
|
||||
3. For each flagged token, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
|
||||
4. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.
|
||||
16
.github/prompts/review-issue.prompt.md
vendored
16
.github/prompts/review-issue.prompt.md
vendored
@@ -1,22 +1,20 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Review a GitHub issue, score it (0-100), and generate an implementation plan'
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: "You are github issue review and planning expertise, Score (0–100) and write one Implementation Plan. Outputs: overview.md, implementation-plan.md."
|
||||
---
|
||||
|
||||
# Review GitHub Issue
|
||||
|
||||
## Goal
|
||||
# GOAL
|
||||
For **#{{issue_number}}** produce:
|
||||
1) `Generated Files/issueReview/{{issue_number}}/overview.md`
|
||||
2) `Generated Files/issueReview/{{issue_number}}/implementation-plan.md`
|
||||
|
||||
## Inputs
|
||||
Figure out required inputs {{issue_number}} from the invocation context; if anything is missing, ask for the value or note it as a gap.
|
||||
figure out from the prompt on the
|
||||
|
||||
# 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`, and download the image for understand the context of the issue more.
|
||||
Locate source code in current workspace, but also free feel to use via `rg`/`git grep`. Link related issues/PRs.
|
||||
|
||||
# OVERVIEW.MD
|
||||
## Summary
|
||||
|
||||
8
.github/prompts/review-pr.prompt.md
vendored
8
.github/prompts/review-pr.prompt.md
vendored
@@ -1,10 +1,10 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Perform a comprehensive PR review with per-step Markdown and machine-readable outputs'
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: "gh-driven PR review; per-step Markdown + machine-readable outputs"
|
||||
---
|
||||
|
||||
# Review Pull Request
|
||||
# PR Review — gh + stepwise
|
||||
|
||||
**Goal**: Given `{{pr_number}}`, run a *one-topic-per-step* review. Write files to `Generated Files/prReview/{{pr_number}}/` (replace `{{pr_number}}` with the integer). Emit machine‑readable blocks for a GitHub MCP to post review comments.
|
||||
|
||||
|
||||
152
.github/skills/changelog-generator/SKILL.md
vendored
Normal file
152
.github/skills/changelog-generator/SKILL.md
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
name: changelog-generator
|
||||
description: Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
|
||||
---
|
||||
|
||||
# Changelog Generator
|
||||
|
||||
This skill transforms technical git commits into polished, user-friendly changelogs that your customers and users will actually understand and appreciate.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Preparing release notes for a new version
|
||||
- Generating changelog between two tags/commits
|
||||
- Creating draft release notes from recent commits
|
||||
|
||||
## Quick Start
|
||||
|
||||
```
|
||||
Create a changelog from commits since v0.96.1
|
||||
```
|
||||
|
||||
```
|
||||
Create release notes for version 0.97.0 starting from tag v0.96.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 1. Fetch │───▶│ 2. Filter │───▶│ 3. Categorize│───▶│ 4. Generate │
|
||||
│ Commits │ │ & Dedupe │ │ by Module │ │ Descriptions │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
📄 github-api 📄 commit- 📄 module- 📄 user-facing-
|
||||
reference.md filtering.md mapping.md description.md
|
||||
```
|
||||
|
||||
## Sub-Skills Reference
|
||||
|
||||
This skill is composed of the following sub-skills.
|
||||
|
||||
**⚠️ IMPORTANT: Do NOT read all sub-skills at once!**
|
||||
- Only read a sub-skill when you reach that step in the workflow
|
||||
- This saves context window and improves accuracy
|
||||
- Use `read_file` to load a sub-skill only when needed
|
||||
|
||||
| Step | Sub-Skill | When to Read |
|
||||
|------|-----------|--------------|
|
||||
| Fetch data | [github-api-reference.md](sub-skills/github-api-reference.md) | When fetching commits/PRs |
|
||||
| Filter commits | [commit-filtering.md](sub-skills/commit-filtering.md) | When checking if commit should be skipped |
|
||||
| Categorize | [module-mapping.md](sub-skills/module-mapping.md) | When determining which module a PR belongs to |
|
||||
| Generate text | [user-facing-description.md](sub-skills/user-facing-description.md) | When writing the changelog entry text |
|
||||
| Attribution | [contributor-attribution.md](sub-skills/contributor-attribution.md) | When checking if author needs thanks |
|
||||
| Large releases | [progress-tracking.md](sub-skills/progress-tracking.md) | Only if processing 50+ commits |
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Summary
|
||||
|
||||
### Step 1: Get Commit Range
|
||||
```powershell
|
||||
# Count commits between tags
|
||||
gh api repos/microsoft/PowerToys/compare/v0.96.0...v0.96.1 --jq '.commits | length'
|
||||
```
|
||||
👉 Details: [github-api-reference.md](sub-skills/github-api-reference.md)
|
||||
|
||||
### Step 2: Filter Commits
|
||||
- Skip commits already in start tag
|
||||
- Skip cherry-picks and backports
|
||||
- Deduplicate by PR number
|
||||
|
||||
👉 Details: [commit-filtering.md](sub-skills/commit-filtering.md)
|
||||
|
||||
### Step 3: For Each PR, Generate Entry
|
||||
1. Get PR title, body, files, labels
|
||||
2. Determine module from file paths or labels
|
||||
3. Check if user-facing (skip internal changes)
|
||||
4. Transform to user-friendly description
|
||||
5. Add contributor attribution if needed
|
||||
|
||||
👉 Details: [user-facing-description.md](sub-skills/user-facing-description.md), [module-mapping.md](sub-skills/module-mapping.md), [contributor-attribution.md](sub-skills/contributor-attribution.md)
|
||||
|
||||
### Step 4: Checkpoint (if 50+ commits)
|
||||
- Save progress after every 15-20 commits
|
||||
- Track processed PRs for deduplication
|
||||
- Enable resume from interruption
|
||||
|
||||
👉 Details: [progress-tracking.md](sub-skills/progress-tracking.md)
|
||||
|
||||
### Step 5: Format Output
|
||||
```markdown
|
||||
## ✨ What's new
|
||||
**Version X.XX (Month Year)**
|
||||
|
||||
**✨ Highlights**
|
||||
- [Most impactful change 1]
|
||||
- [Most impactful change 2]
|
||||
|
||||
### [Module Name - Alphabetical]
|
||||
- [Description]. Thanks [@contributor](https://github.com/contributor)!
|
||||
|
||||
### Development
|
||||
- [Internal changes]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Output
|
||||
|
||||
```markdown
|
||||
## ✨ What's new
|
||||
**Version 0.96.1 (December 2025)**
|
||||
|
||||
**✨ Highlights**
|
||||
- Advanced Paste now supports multiple AI providers.
|
||||
- PowerRename can extract photo metadata for renaming.
|
||||
|
||||
### Advanced Paste
|
||||
- Added support for Azure OpenAI, Google Gemini, Mistral, and more.
|
||||
|
||||
### Awake
|
||||
- The countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Development
|
||||
- Resolved build warnings in Command Palette projects.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Propose highlights** after all entries are generated
|
||||
2. **Check PR body** when title is unclear
|
||||
3. **Thank external contributors** - see [contributor-attribution.md](sub-skills/contributor-attribution.md)
|
||||
4. **Use progress tracking** for large releases - see [progress-tracking.md](sub-skills/progress-tracking.md)
|
||||
5. **Save output** to `release-change-note-draft.md`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Lost progress mid-generation | Read `release-notes-progress.md` to resume |
|
||||
| Duplicate entries | Check processed PR list, dedupe by PR number |
|
||||
| Commit already released | Use `git merge-base --is-ancestor` to verify |
|
||||
| API rate limited | Check `gh api rate_limit`, wait or use token |
|
||||
|
||||
👉 See sub-skills for detailed troubleshooting.
|
||||
107
.github/skills/changelog-generator/sub-skills/commit-filtering.md
vendored
Normal file
107
.github/skills/changelog-generator/sub-skills/commit-filtering.md
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
# Commit Filtering Rules
|
||||
|
||||
This sub-skill defines rules for filtering commits to include in the changelog.
|
||||
|
||||
## Understanding the Branch Model
|
||||
|
||||
PowerToys uses a release branch model where fixes are cherry-picked from main:
|
||||
|
||||
```
|
||||
main: A---B---C---D---E---F---G---H (HEAD)
|
||||
\
|
||||
release: X---Y---Z (v0.96.1 tag)
|
||||
↑
|
||||
(X, Y are cherry-picks of C, E from main)
|
||||
```
|
||||
|
||||
**Key insight:** When comparing `v0.96.1...main`:
|
||||
- The release tag (v0.96.1) is on a **different branch** than main
|
||||
- GitHub compare finds the merge-base and returns commits on main after that point
|
||||
- Commits C and E appear in the results even though they were cherry-picked to release as X and Y
|
||||
- **The SHAs are different**, so SHA-based filtering won't work!
|
||||
|
||||
## ⚠️ CRITICAL: Filter by PR Number, Not SHA
|
||||
|
||||
Since cherry-picks have different SHAs, you **MUST** check by PR number:
|
||||
|
||||
```powershell
|
||||
# Extract PR number from commit message and check if it exists in the release tag
|
||||
$prNumber = "43785"
|
||||
$startTag = "v0.96.1"
|
||||
|
||||
# Search the release branch for this PR number in commit messages
|
||||
$cherryPicked = git log $startTag --oneline --grep="#$prNumber"
|
||||
if ($cherryPicked) {
|
||||
Write-Host "SKIP: PR #$prNumber was cherry-picked to $startTag"
|
||||
} else {
|
||||
Write-Host "INCLUDE: PR #$prNumber is new since $startTag"
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Filtering Workflow
|
||||
|
||||
```powershell
|
||||
$startTag = "v0.96.1" # Release tag (on release branch)
|
||||
$endRef = "main" # Target (main branch)
|
||||
|
||||
# Step 1: Get all commits from main since the merge-base with release tag
|
||||
$commits = gh api "repos/microsoft/PowerToys/compare/$startTag...$endRef" `
|
||||
--jq '.commits[] | {sha: .sha, message: .commit.message}' | ConvertFrom-Json
|
||||
|
||||
# Step 2: Build list of PR numbers already in the release tag
|
||||
$releasePRs = git log $startTag --oneline | Select-String -Pattern '#(\d+)' -AllMatches |
|
||||
ForEach-Object { $_.Matches.Groups[1].Value } | Sort-Object -Unique
|
||||
|
||||
Write-Host "PRs already in $startTag : $($releasePRs.Count)"
|
||||
|
||||
# Step 3: Filter commits - skip if PR was cherry-picked to release
|
||||
$newCommits = @()
|
||||
foreach ($commit in $commits) {
|
||||
if ($commit.message -match '#(\d+)') {
|
||||
$prNumber = $matches[1]
|
||||
if ($releasePRs -contains $prNumber) {
|
||||
Write-Host "SKIP: PR #$prNumber already in $startTag (cherry-picked)"
|
||||
continue
|
||||
}
|
||||
}
|
||||
$newCommits += $commit
|
||||
}
|
||||
|
||||
Write-Host "New commits to process: $($newCommits.Count)"
|
||||
```
|
||||
|
||||
## Why SHA-Based Methods Don't Work Here
|
||||
|
||||
| Method | Works for same branch? | Works for cross-branch (cherry-picks)? |
|
||||
|--------|------------------------|----------------------------------------|
|
||||
| `git merge-base --is-ancestor` | ✅ Yes | ❌ No - different SHAs |
|
||||
| `git tag --contains` | ✅ Yes | ❌ No - tag is on different branch |
|
||||
| GitHub Compare API | ✅ Yes | ❌ No - returns commits by SHA |
|
||||
| **PR number matching** | ✅ Yes | ✅ **Yes** |
|
||||
|
||||
## Skip Rules Summary
|
||||
|
||||
| Priority | Condition | Action |
|
||||
|----------|-----------|--------|
|
||||
| 1 | PR number found in `git log $startTag --grep="#$prNumber"` | **SKIP** - cherry-picked |
|
||||
| 2 | Same PR number already processed in this run | **SKIP** - duplicate |
|
||||
| 3 | Bot author (dependabot, etc.) | **SKIP** - unless user-visible |
|
||||
| 4 | Internal-only change (CI, tests, refactor) | Move to **Development** section |
|
||||
|
||||
## User-Facing vs Non-User-Facing
|
||||
|
||||
**Include in changelog:**
|
||||
- New features and capabilities
|
||||
- Bug fixes that affect users
|
||||
- UI/UX improvements
|
||||
- Performance improvements users would notice
|
||||
- Breaking changes or behavior modifications
|
||||
- Security fixes
|
||||
|
||||
**Exclude from changelog (put in Development section):**
|
||||
- Internal refactoring
|
||||
- CI/CD changes
|
||||
- Code style fixes
|
||||
- Test additions/modifications
|
||||
- Documentation-only changes
|
||||
- Dependency updates (unless user-visible impact)
|
||||
110
.github/skills/changelog-generator/sub-skills/contributor-attribution.md
vendored
Normal file
110
.github/skills/changelog-generator/sub-skills/contributor-attribution.md
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
# Contributor Attribution Rules
|
||||
|
||||
This sub-skill defines when and how to credit contributors in changelog entries.
|
||||
|
||||
## Attribution Rules
|
||||
|
||||
- Check [COMMUNITY.md](../../../COMMUNITY.md) for the "PowerToys core team" section
|
||||
- If author is **NOT** in core team → Add `Thanks [@username](https://github.com/username)!`
|
||||
- If author **IS** in core team → No attribution needed
|
||||
- Microsoft employees working on PowerToys as their job → No attribution needed
|
||||
|
||||
## Core Team Members
|
||||
|
||||
Current core team members (from COMMUNITY.md) - do NOT thank these:
|
||||
|
||||
```
|
||||
@craigloewen-msft, @niels9001, @dhowett, @yeelam-gordon, @jamrobot,
|
||||
@lei9444, @shuaiyuanxx, @moooyo, @haoliuu, @chenmy77, @chemwolf6922,
|
||||
@yaqingmi, @zhaoqpcn, @urnotdfs, @zhaopy536, @wang563681252, @vanzue,
|
||||
@zadjii-msft, @khmyznikov, @chatasweetie, @MichaelJolley, @Jaylyn-Barbee,
|
||||
@zateutsch, @crutkas
|
||||
```
|
||||
|
||||
## Check Author Script
|
||||
|
||||
```powershell
|
||||
$coreTeam = @(
|
||||
'craigloewen-msft', 'niels9001', 'dhowett', 'yeelam-gordon', 'jamrobot',
|
||||
'lei9444', 'shuaiyuanxx', 'moooyo', 'haoliuu', 'chenmy77', 'chemwolf6922',
|
||||
'yaqingmi', 'zhaoqpcn', 'urnotdfs', 'zhaopy536', 'wang563681252', 'vanzue',
|
||||
'zadjii-msft', 'khmyznikov', 'chatasweetie', 'MichaelJolley', 'Jaylyn-Barbee',
|
||||
'zateutsch', 'crutkas'
|
||||
)
|
||||
|
||||
function Get-Attribution {
|
||||
param([string]$author)
|
||||
|
||||
if ($coreTeam -contains $author) {
|
||||
return $null # No attribution needed
|
||||
}
|
||||
return "Thanks [@$author](https://github.com/$author)!"
|
||||
}
|
||||
|
||||
# Usage
|
||||
$author = gh pr view 12345 --repo microsoft/PowerToys --json author --jq '.author.login'
|
||||
$attribution = Get-Attribution $author
|
||||
if ($attribution) {
|
||||
Write-Host "Add: $attribution"
|
||||
} else {
|
||||
Write-Host "No attribution needed (core team member)"
|
||||
}
|
||||
```
|
||||
|
||||
## Attribution Format
|
||||
|
||||
**With attribution:**
|
||||
```markdown
|
||||
- The Awake countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
```
|
||||
|
||||
**Without attribution (core team):**
|
||||
```markdown
|
||||
- Added new feature to Command Palette for opening settings.
|
||||
```
|
||||
|
||||
## Special Cases
|
||||
|
||||
1. **Co-authored commits**: Credit the primary author (first in list)
|
||||
2. **Bot accounts** (dependabot, etc.): No attribution
|
||||
3. **Former core team members**: Check if they were core team at time of PR
|
||||
4. **Multiple PRs by same external contributor**: Thank them on each entry
|
||||
|
||||
## High-Impact Community Members
|
||||
|
||||
These contributors have made significant ongoing contributions and are recognized in COMMUNITY.md.
|
||||
**ALWAYS thank these contributors** - they are NOT core team and deserve recognition:
|
||||
|
||||
```
|
||||
@davidegiacometti, @htcfreek, @daverayment, @jiripolasek
|
||||
```
|
||||
|
||||
Check COMMUNITY.md for the full up-to-date list under "High impact community members" section.
|
||||
|
||||
## Updated Check Author Script
|
||||
|
||||
```powershell
|
||||
$coreTeam = @(
|
||||
'craigloewen-msft', 'niels9001', 'dhowett', 'yeelam-gordon', 'jamrobot',
|
||||
'lei9444', 'shuaiyuanxx', 'moooyo', 'haoliuu', 'chenmy77', 'chemwolf6922',
|
||||
'yaqingmi', 'zhaoqpcn', 'urnotdfs', 'zhaopy536', 'wang563681252', 'vanzue',
|
||||
'zadjii-msft', 'khmyznikov', 'chatasweetie', 'MichaelJolley', 'Jaylyn-Barbee',
|
||||
'zateutsch', 'crutkas'
|
||||
)
|
||||
|
||||
# High-impact community members - ALWAYS thank these!
|
||||
$highImpactCommunity = @(
|
||||
'davidegiacometti', 'htcfreek', 'daverayment', 'jiripolasek'
|
||||
)
|
||||
|
||||
function Get-Attribution {
|
||||
param([string]$author)
|
||||
|
||||
# Core team and bots don't need thanks
|
||||
if ($coreTeam -contains $author -or $author -match '\[bot\]$') {
|
||||
return $null
|
||||
}
|
||||
# Everyone else (including high-impact community) gets thanked
|
||||
return "Thanks [@$author](https://github.com/$author)!"
|
||||
}
|
||||
```
|
||||
101
.github/skills/changelog-generator/sub-skills/github-api-reference.md
vendored
Normal file
101
.github/skills/changelog-generator/sub-skills/github-api-reference.md
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# GitHub API Reference for Changelog Generation
|
||||
|
||||
This sub-skill provides GitHub API commands and scripts for fetching commit and PR data.
|
||||
|
||||
## Authentication
|
||||
|
||||
```powershell
|
||||
# Ensure gh CLI is authenticated
|
||||
gh auth status
|
||||
|
||||
# Or use personal access token
|
||||
$headers = @{ Authorization = "token $env:GITHUB_TOKEN" }
|
||||
```
|
||||
|
||||
## Useful API Endpoints
|
||||
|
||||
```powershell
|
||||
# Compare two refs (tags, branches, commits)
|
||||
gh api repos/microsoft/PowerToys/compare/v0.96.0...v0.96.1
|
||||
|
||||
# List commits with pagination
|
||||
gh api "repos/microsoft/PowerToys/commits?per_page=100&page=1"
|
||||
|
||||
# Get PR associated with a commit
|
||||
gh api repos/microsoft/PowerToys/commits/{sha}/pulls
|
||||
|
||||
# Get PR details
|
||||
gh api repos/microsoft/PowerToys/pulls/{number}
|
||||
|
||||
# Get files changed in a PR
|
||||
gh api repos/microsoft/PowerToys/pulls/{number}/files
|
||||
|
||||
# Search PRs merged in date range
|
||||
gh api "search/issues?q=repo:microsoft/PowerToys+is:pr+is:merged+merged:2025-01-01..2025-01-31"
|
||||
```
|
||||
|
||||
## Fetch Commits in Range
|
||||
|
||||
```powershell
|
||||
$owner = "microsoft"
|
||||
$repo = "PowerToys"
|
||||
$startTag = "v0.96.0"
|
||||
$endTag = "v0.96.1"
|
||||
|
||||
# Get all commits between two tags
|
||||
gh api repos/$owner/$repo/compare/$startTag...$endTag --jq '.commits[] | {sha: .sha, message: .commit.message, author: .author.login, date: .commit.author.date}'
|
||||
```
|
||||
|
||||
## Get PR Details for a Commit
|
||||
|
||||
```powershell
|
||||
# Get PR associated with a commit SHA
|
||||
gh api "repos/$owner/$repo/commits/{sha}/pulls" --jq '.[0] | {number: .number, title: .title, body: .body, user: .user.login, labels: [.labels[].name]}'
|
||||
|
||||
# Get full PR details
|
||||
gh pr view 1234 --repo microsoft/PowerToys --json title,body,author,files,labels,mergedAt
|
||||
```
|
||||
|
||||
## Batch Processing Script
|
||||
|
||||
```powershell
|
||||
$startTag = "v0.96.0"
|
||||
$endTag = "v0.96.1"
|
||||
|
||||
$commits = gh api repos/microsoft/PowerToys/compare/$startTag...$endTag --jq '.commits[].sha' | ForEach-Object {
|
||||
$sha = $_
|
||||
$prInfo = gh api "repos/microsoft/PowerToys/commits/$sha/pulls" 2>$null | ConvertFrom-Json
|
||||
if ($prInfo) {
|
||||
[PSCustomObject]@{
|
||||
SHA = $sha
|
||||
PRNumber = $prInfo[0].number
|
||||
Title = $prInfo[0].title
|
||||
Author = $prInfo[0].user.login
|
||||
Labels = $prInfo[0].labels.name -join ", "
|
||||
}
|
||||
}
|
||||
}
|
||||
$commits | Format-Table
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
```powershell
|
||||
# Check remaining rate limit
|
||||
gh api rate_limit --jq '.rate'
|
||||
# Authenticated: 5000 requests/hour
|
||||
# Unauthenticated: 60 requests/hour
|
||||
```
|
||||
|
||||
## Pagination for Large Results
|
||||
|
||||
```powershell
|
||||
# GitHub API returns max 100 items per page
|
||||
$page = 1
|
||||
$allCommits = @()
|
||||
do {
|
||||
$commits = gh api "repos/$owner/$repo/commits?per_page=100&page=$page" | ConvertFrom-Json
|
||||
$allCommits += $commits
|
||||
$page++
|
||||
} while ($commits.Count -eq 100)
|
||||
```
|
||||
137
.github/skills/changelog-generator/sub-skills/module-mapping.md
vendored
Normal file
137
.github/skills/changelog-generator/sub-skills/module-mapping.md
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
# PowerToys Module Path Mapping
|
||||
|
||||
This sub-skill maps file paths to PowerToys module names for categorization.
|
||||
|
||||
## Module Path Mapping Table
|
||||
|
||||
| Module | Path Pattern |
|
||||
|--------|--------------|
|
||||
| Advanced Paste | `src/modules/AdvancedPaste/**` |
|
||||
| Always On Top | `src/modules/alwaysontop/**` |
|
||||
| Awake | `src/modules/Awake/**` |
|
||||
| Color Picker | `src/modules/colorPicker/**` |
|
||||
| Command Palette | `src/modules/cmdpal/**` |
|
||||
| Crop And Lock | `src/modules/CropAndLock/**` |
|
||||
| Environment Variables | `src/modules/EnvironmentVariables/**` |
|
||||
| FancyZones | `src/modules/fancyzones/**` |
|
||||
| File Explorer Add-ons | `src/modules/previewpane/**`, `src/modules/FileExplorerPreview/**` |
|
||||
| File Locksmith | `src/modules/FileLocksmith/**` |
|
||||
| Find My Mouse | `src/modules/MouseUtils/FindMyMouse/**` |
|
||||
| Hosts File Editor | `src/modules/Hosts/**` |
|
||||
| Image Resizer | `src/modules/imageresizer/**` |
|
||||
| Keyboard Manager | `src/modules/keyboardmanager/**` |
|
||||
| Light Switch | `src/modules/LightSwitch/**` |
|
||||
| Mouse Highlighter | `src/modules/MouseUtils/MouseHighlighter/**` |
|
||||
| Mouse Jump | `src/modules/MouseUtils/MouseJump/**` |
|
||||
| Mouse Pointer Crosshairs | `src/modules/MouseUtils/MousePointerCrosshairs/**` |
|
||||
| Mouse Without Borders | `src/modules/MouseWithoutBorders/**` |
|
||||
| New+ | `src/modules/NewPlus/**` |
|
||||
| Paste As Plain Text | `src/modules/PastePlain/**` |
|
||||
| Peek | `src/modules/Peek/**` |
|
||||
| PowerRename | `src/modules/powerrename/**` |
|
||||
| PowerToys Run | `src/modules/launcher/**` |
|
||||
| Quick Accent | `src/modules/QuickAccent/**` |
|
||||
| Registry Preview | `src/modules/RegistryPreview/**` |
|
||||
| Screen Ruler | `src/modules/MeasureTool/**` |
|
||||
| Shortcut Guide | `src/modules/ShortcutGuide/**` |
|
||||
| Text Extractor | `src/modules/TextExtractor/**` |
|
||||
| Video Conference Mute | `src/modules/videoconference/**` |
|
||||
| Workspaces | `src/modules/Workspaces/**` |
|
||||
| ZoomIt | `src/modules/ZoomIt/**` |
|
||||
| Settings | `src/settings-ui/**` |
|
||||
| Runner | `src/runner/**` |
|
||||
| Installer | `installer/**` |
|
||||
| General / Infrastructure | `src/common/**`, `.github/**`, `tools/**` |
|
||||
|
||||
## Categorization by PR Labels
|
||||
|
||||
Common PowerToys PR labels for modules:
|
||||
- `Product-FancyZones`
|
||||
- `Product-PowerToys Run`
|
||||
- `Product-Awake`
|
||||
- `Product-ColorPicker`
|
||||
- `Product-Keyboard Manager`
|
||||
- etc.
|
||||
|
||||
## Auto-Categorization Script
|
||||
|
||||
```powershell
|
||||
function Get-ModuleFromPath {
|
||||
param([string]$filePath)
|
||||
|
||||
$moduleMap = @{
|
||||
'src/modules/AdvancedPaste/' = 'Advanced Paste'
|
||||
'src/modules/alwaysontop/' = 'Always On Top'
|
||||
'src/modules/Awake/' = 'Awake'
|
||||
'src/modules/colorPicker/' = 'Color Picker'
|
||||
'src/modules/cmdpal/' = 'Command Palette'
|
||||
'src/modules/CropAndLock/' = 'Crop And Lock'
|
||||
'src/modules/EnvironmentVariables/' = 'Environment Variables'
|
||||
'src/modules/fancyzones/' = 'FancyZones'
|
||||
'src/modules/previewpane/' = 'File Explorer Add-ons'
|
||||
'src/modules/FileExplorerPreview/' = 'File Explorer Add-ons'
|
||||
'src/modules/FileLocksmith/' = 'File Locksmith'
|
||||
'src/modules/MouseUtils/FindMyMouse/' = 'Find My Mouse'
|
||||
'src/modules/Hosts/' = 'Hosts File Editor'
|
||||
'src/modules/imageresizer/' = 'Image Resizer'
|
||||
'src/modules/keyboardmanager/' = 'Keyboard Manager'
|
||||
'src/modules/LightSwitch/' = 'Light Switch'
|
||||
'src/modules/MouseUtils/MouseHighlighter/' = 'Mouse Highlighter'
|
||||
'src/modules/MouseUtils/MouseJump/' = 'Mouse Jump'
|
||||
'src/modules/MouseUtils/MousePointerCrosshairs/' = 'Mouse Pointer Crosshairs'
|
||||
'src/modules/MouseWithoutBorders/' = 'Mouse Without Borders'
|
||||
'src/modules/NewPlus/' = 'New+'
|
||||
'src/modules/PastePlain/' = 'Paste As Plain Text'
|
||||
'src/modules/Peek/' = 'Peek'
|
||||
'src/modules/powerrename/' = 'PowerRename'
|
||||
'src/modules/launcher/' = 'PowerToys Run'
|
||||
'src/modules/QuickAccent/' = 'Quick Accent'
|
||||
'src/modules/RegistryPreview/' = 'Registry Preview'
|
||||
'src/modules/MeasureTool/' = 'Screen Ruler'
|
||||
'src/modules/ShortcutGuide/' = 'Shortcut Guide'
|
||||
'src/modules/TextExtractor/' = 'Text Extractor'
|
||||
'src/modules/videoconference/' = 'Video Conference Mute'
|
||||
'src/modules/Workspaces/' = 'Workspaces'
|
||||
'src/modules/ZoomIt/' = 'ZoomIt'
|
||||
'src/settings-ui/' = 'Settings'
|
||||
'src/runner/' = 'Runner'
|
||||
'installer/' = 'Installer'
|
||||
'src/common/' = 'General'
|
||||
'.github/' = 'Development'
|
||||
'tools/' = 'Development'
|
||||
}
|
||||
|
||||
foreach ($pattern in $moduleMap.Keys) {
|
||||
if ($filePath -like "*$pattern*") {
|
||||
return $moduleMap[$pattern]
|
||||
}
|
||||
}
|
||||
return 'General'
|
||||
}
|
||||
|
||||
# Usage: categorize a PR by its changed files
|
||||
$files = gh pr view 12345 --repo microsoft/PowerToys --json files --jq '.files[].path'
|
||||
$modules = $files | ForEach-Object { Get-ModuleFromPath $_ } | Sort-Object -Unique
|
||||
Write-Host "PR affects modules: $($modules -join ', ')"
|
||||
```
|
||||
|
||||
## Output Organization
|
||||
|
||||
Modules should be listed in **alphabetical order** in the changelog:
|
||||
|
||||
```markdown
|
||||
### Advanced Paste
|
||||
- ...
|
||||
|
||||
### Awake
|
||||
- ...
|
||||
|
||||
### Command Palette
|
||||
- ...
|
||||
|
||||
### General
|
||||
- ...
|
||||
|
||||
### Development
|
||||
- ...
|
||||
```
|
||||
121
.github/skills/changelog-generator/sub-skills/progress-tracking.md
vendored
Normal file
121
.github/skills/changelog-generator/sub-skills/progress-tracking.md
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
# Progress Tracking for Large Changelogs
|
||||
|
||||
This sub-skill provides the checkpoint mechanism for processing many commits without losing progress.
|
||||
|
||||
## When to Use Progress Tracking
|
||||
|
||||
- Processing 50+ commits
|
||||
- Long-running changelog generation
|
||||
- Risk of context overflow in AI conversations
|
||||
|
||||
## Checkpoint Mechanism
|
||||
|
||||
1. **Before starting**: Create a progress tracking file `release-notes-progress.md`
|
||||
2. **After each batch**: Append processed results to `release-change-note-draft.md`
|
||||
3. **Track position**: Record the last processed commit SHA in `release-notes-progress.md`
|
||||
|
||||
## Progress File Template
|
||||
|
||||
Create `release-notes-progress.md`:
|
||||
|
||||
```markdown
|
||||
# Release Notes Generation Progress
|
||||
|
||||
## Configuration
|
||||
- Start Tag: v0.96.0
|
||||
- End Tag: v0.96.1
|
||||
- Total Commits: 127
|
||||
- Batch Size: 20
|
||||
|
||||
## Progress Tracker
|
||||
| Batch | Status | Last SHA | PRs Processed |
|
||||
|-------|--------|----------|---------------|
|
||||
| 1 (1-20) | ✅ Done | abc1234 | #1001, #1002, #1003... |
|
||||
| 2 (21-40) | ✅ Done | def5678 | #1004, #1005... |
|
||||
| 3 (41-60) | 🔄 In Progress | ghi9012 | #1006... |
|
||||
| 4 (61-80) | ⏳ Pending | - | - |
|
||||
| 5 (81-100) | ⏳ Pending | - | - |
|
||||
| 6 (101-120) | ⏳ Pending | - | - |
|
||||
| 7 (121-127) | ⏳ Pending | - | - |
|
||||
|
||||
## Processed PRs (deduplication list)
|
||||
#1001, #1002, #1003, #1004, #1005, #1006
|
||||
|
||||
## Last Checkpoint
|
||||
- Timestamp: 2025-01-07 10:30:00
|
||||
- Last processed commit: ghi9012
|
||||
- Next commit to process: jkl3456
|
||||
```
|
||||
|
||||
## Batch Processing Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. Get total commit count and create progress file │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2. Filter: Skip commits already in start tag │
|
||||
│ - Check if commit is ancestor of start tag │
|
||||
│ - Skip cherry-picks or backports already released │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3. Process batch of 15-20 commits │
|
||||
│ - Fetch commit details │
|
||||
│ - Get associated PRs │
|
||||
│ - Generate changelog entries │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 4. CHECKPOINT: Save progress │
|
||||
│ - Append entries to release-change-note-draft.md │
|
||||
│ - Update release-notes-progress.md with last SHA │
|
||||
│ - Record processed PR numbers │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 5. Check: More commits remaining? │
|
||||
│ YES → Go to step 3 with next batch │
|
||||
│ NO → Go to step 6 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 6. Final merge and formatting │
|
||||
│ - Combine all batches │
|
||||
│ - Deduplicate by PR number │
|
||||
│ - Sort by module alphabetically │
|
||||
│ - Add highlights section │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Resuming from Checkpoint
|
||||
|
||||
If interrupted, read `release-notes-progress.md` to find:
|
||||
1. Which batch was last completed
|
||||
2. The SHA of the last processed commit
|
||||
3. Which PRs have already been processed (for deduplication)
|
||||
|
||||
Then continue from the next unprocessed commit:
|
||||
|
||||
```powershell
|
||||
# Find where you left off
|
||||
$lastSha = "abc1234" # from progress file
|
||||
$remainingShas = gh api repos/microsoft/PowerToys/compare/$lastSha...main --jq '.commits[].sha'
|
||||
```
|
||||
|
||||
## Batch Size Recommendations
|
||||
|
||||
| Total Commits | Recommended Batch Size |
|
||||
|---------------|------------------------|
|
||||
| < 30 | Process all at once |
|
||||
| 30-100 | 15-20 per batch |
|
||||
| 100-300 | 20 per batch |
|
||||
| 300+ | 25 per batch + parallel processing |
|
||||
|
||||
## Deduplication
|
||||
|
||||
Track processed PR numbers to avoid duplicates:
|
||||
- Same PR can appear multiple times (multiple commits)
|
||||
- Cherry-picks may reference same PR
|
||||
- Always check `Processed PRs` list before generating entry
|
||||
119
.github/skills/changelog-generator/sub-skills/user-facing-description.md
vendored
Normal file
119
.github/skills/changelog-generator/sub-skills/user-facing-description.md
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
# Generating User-Facing Descriptions
|
||||
|
||||
This sub-skill explains how to transform technical PR/commit info into user-friendly changelog entries.
|
||||
|
||||
## Information Sources (Priority Order)
|
||||
|
||||
1. **PR Title** - Often the best summary, already user-facing
|
||||
2. **PR Description/Body** - Look for "What does this PR do?" or summary sections
|
||||
3. **Commit Message** - First line is usually descriptive
|
||||
4. **Changed Files** - Infer the impact from what was modified
|
||||
5. **PR Labels** - Indicate type (bug, feature, enhancement)
|
||||
|
||||
## Data Collection Command
|
||||
|
||||
```powershell
|
||||
# Get all relevant info for a PR
|
||||
$prNumber = 12345
|
||||
$prData = gh pr view $prNumber --repo microsoft/PowerToys --json title,body,author,files,labels,mergedAt
|
||||
|
||||
# Parse the data
|
||||
$pr = $prData | ConvertFrom-Json
|
||||
Write-Host "Title: $($pr.title)"
|
||||
Write-Host "Author: $($pr.author.login)"
|
||||
Write-Host "Labels: $($pr.labels.name -join ', ')"
|
||||
Write-Host "Files changed: $($pr.files.Count)"
|
||||
Write-Host "Body preview: $($pr.body.Substring(0, [Math]::Min(500, $pr.body.Length)))"
|
||||
```
|
||||
|
||||
## Transformation Rules
|
||||
|
||||
| Source Info | Transformation | Example Output |
|
||||
|-------------|----------------|----------------|
|
||||
| PR Title: "Fix null reference in FancyZones editor" | Describe the fix from user perspective | "Fixed a crash that could occur when editing zone layouts." |
|
||||
| PR Title: "Add support for XYZ format" | State the new capability | "Added support for XYZ format in File Explorer preview." |
|
||||
| PR Title: "[FancyZones] Refactor grid logic" | Check if user-visible; if not → Development section | (Development) "Refactored FancyZones grid logic for improved maintainability." |
|
||||
| PR with label "bug" | Frame as a fix | "Fixed an issue where..." |
|
||||
| PR with label "enhancement" | Frame as improvement | "Improved..." or "Enhanced..." |
|
||||
|
||||
## Description Generation Process
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ INPUT: PR #12345 │
|
||||
│ Title: "Fix Awake timer drift after system sleep" │
|
||||
│ Body: "The timer was resetting incorrectly when the system │
|
||||
│ resumed from sleep, causing the countdown to be wrong." │
|
||||
│ Author: @daverayment │
|
||||
│ Labels: [bug, Product-Awake] │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ ANALYSIS: │
|
||||
│ 1. Is it user-facing? YES (timer behavior affects users) │
|
||||
│ 2. Category: Bug fix │
|
||||
│ 3. Module: Awake (from label + file paths) │
|
||||
│ 4. Author in core team? NO → needs attribution │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ OUTPUT: │
|
||||
│ "The Awake countdown timer now stays accurate over long │
|
||||
│ periods. Thanks [@daverayment](https://github.com/daverayment)!" │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Writing Style Guidelines
|
||||
|
||||
**DO:**
|
||||
- Start with what changed or what users can now do
|
||||
- Use active voice: "Added...", "Fixed...", "Improved..."
|
||||
- Be specific about the benefit to users
|
||||
- Keep it concise (1-2 sentences max)
|
||||
- Include link to documentation if it's a new feature
|
||||
|
||||
**DON'T:**
|
||||
- Use technical jargon users won't understand
|
||||
- Reference internal code names, file names, or class names
|
||||
- Say "Fixed bug" without explaining what was wrong
|
||||
- Include PR numbers in the description (they're tracked separately)
|
||||
|
||||
## Example Transformations
|
||||
|
||||
| Technical PR Title | User-Facing Description |
|
||||
|--------------------|------------------------|
|
||||
| "Fix NRE in FZEditor when zones is null" | "Fixed a crash that could occur when opening the FancyZones editor with no saved layouts." |
|
||||
| "Add ExifTool integration for PowerRename" | "PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`." |
|
||||
| "Perf: Reduce memory allocation in PT Run" | "Improved PowerToys Run startup performance and reduced memory usage." |
|
||||
| "Update Newtonsoft.Json to 13.0.3" | (Development) "Updated Newtonsoft.Json dependency." OR skip if no user impact |
|
||||
| "Add unit tests for color picker" | SKIP - not user-facing |
|
||||
| "Fix typo in settings UI" | "Fixed a typo in the Settings interface." (only if visible to users) |
|
||||
|
||||
## Context-Aware Description
|
||||
|
||||
Sometimes you need to read the PR body or changed files to understand the impact:
|
||||
|
||||
```powershell
|
||||
# If PR title is unclear, check the body for context
|
||||
$prBody = gh pr view 12345 --repo microsoft/PowerToys --json body --jq '.body'
|
||||
|
||||
# Look for common patterns in PR descriptions:
|
||||
# - "## Summary" or "## Description"
|
||||
# - "This PR fixes/adds/improves..."
|
||||
# - "Before/After" comparisons
|
||||
# - Screenshots (indicate UI changes)
|
||||
|
||||
# Check what files changed to understand scope
|
||||
$files = gh pr view 12345 --repo microsoft/PowerToys --json files --jq '.files[].path'
|
||||
# If mostly .xaml files → UI change
|
||||
# If mostly .cs/.cpp in one module → module-specific change
|
||||
# If in src/common/ → potentially affects multiple modules
|
||||
```
|
||||
|
||||
## Handling Ambiguous PRs
|
||||
|
||||
If a PR's impact is unclear:
|
||||
1. **Check the linked issue** - often has user-reported symptoms
|
||||
2. **Look at file changes** - understand what was modified
|
||||
3. **Check PR comments** - may have discussion about user impact
|
||||
4. **When in doubt** - put in Development section or ask for clarification
|
||||
@@ -125,10 +125,6 @@
|
||||
"WinUI3Apps\\Powertoys.Peek.UI.exe",
|
||||
"WinUI3Apps\\Powertoys.Peek.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.QuickAccess.dll",
|
||||
"WinUI3Apps\\PowerToys.QuickAccess.exe",
|
||||
"WinUI3Apps\\PowerToys.Settings.UI.Controls.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariablesModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariablesUILib.dll",
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
|
||||
|
||||
@@ -68,13 +68,14 @@ jobs:
|
||||
|
||||
- template: .\steps-restore-nuget.yml
|
||||
|
||||
- task: MSBuild@1
|
||||
- task: NuGetCommand@2
|
||||
displayName: Restore solution-level NuGet packages
|
||||
inputs:
|
||||
solution: PowerToys.slnx
|
||||
msbuildArguments: '/t:restore /p:RestorePackagesConfig=true'
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
command: restore
|
||||
feedsToUse: config
|
||||
configPath: nuget.config
|
||||
restoreSolution: PowerToys.slnx
|
||||
restoreDirectory: '$(Build.SourcesDirectory)\packages'
|
||||
|
||||
# Build all UI test projects if no specific modules are specified
|
||||
- ${{ if eq(length(parameters.uiTestModules), 0) }}:
|
||||
|
||||
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"github.copilot.chat.reviewSelection.instructions": [
|
||||
{
|
||||
"file": ".github/prompts/review-pr.prompt.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.commitMessageGeneration.instructions": [
|
||||
{
|
||||
"file": ".github/prompts/create-commit-title.prompt.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.pullRequestDescriptionGeneration.instructions": [
|
||||
{
|
||||
"file": ".github/prompts/create-pr-summary.prompt.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
106
.vscode/tasks.json
vendored
106
.vscode/tasks.json
vendored
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": ["/d", "/c"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"inputs": [
|
||||
{
|
||||
"id": "config",
|
||||
"type": "pickString",
|
||||
"description": "Configuration",
|
||||
"options": ["Debug", "Release"],
|
||||
"default": "Debug"
|
||||
},
|
||||
{
|
||||
"id": "platform",
|
||||
"type": "pickString",
|
||||
"description": "Platform (leave empty to auto-detect host platform)",
|
||||
"options": ["", "X64", "ARM64"],
|
||||
"default": "X64"
|
||||
},
|
||||
{
|
||||
"id": "msbuildExtra",
|
||||
"type": "promptString",
|
||||
"description": "Extra MSBuild args (optional). Example: /p:CIBuild=true /m",
|
||||
"default": ""
|
||||
}
|
||||
],
|
||||
|
||||
"tasks": [
|
||||
{
|
||||
"label": "PT: Build (quick)",
|
||||
"type": "shell",
|
||||
"command": "\"${workspaceFolder}\\tools\\build\\build.cmd\"",
|
||||
"args": [
|
||||
"-Path",
|
||||
"${fileDirname}"
|
||||
],
|
||||
"group": { "kind": "build", "isDefault": true },
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "PT: Build (with options)",
|
||||
"type": "shell",
|
||||
"command": "\"${workspaceFolder}\\tools\\build\\build.cmd\"",
|
||||
"args": [
|
||||
"-Path",
|
||||
"${fileDirname}",
|
||||
"-Platform",
|
||||
"${input:platform}",
|
||||
"-Configuration",
|
||||
"${input:config}",
|
||||
"${input:msbuildExtra}"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "PT: Build Essentials (quick)",
|
||||
"type": "shell",
|
||||
"command": "\"${workspaceFolder}\\tools\\build\\build-essentials.cmd\"",
|
||||
"args": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "PT: Build Essentials (with options)",
|
||||
"type": "shell",
|
||||
"command": "\"${workspaceFolder}\\tools\\build\\build-essentials.cmd\"",
|
||||
"args": [
|
||||
"-Platform",
|
||||
"${input:platform}",
|
||||
"-Configuration",
|
||||
"${input:config}",
|
||||
"${input:msbuildExtra}"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
165
AGENTS.md
165
AGENTS.md
@@ -1,165 +0,0 @@
|
||||
---
|
||||
description: 'Top-level AI contributor guidance for developing PowerToys - a collection of Windows productivity utilities'
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
# PowerToys – AI Contributor Guide
|
||||
|
||||
This is the top-level guidance for AI contributions to PowerToys. Keep changes atomic, follow existing patterns, and cite exact paths in PRs.
|
||||
|
||||
## Overview
|
||||
|
||||
PowerToys is a set of utilities for power users to tune and streamline their Windows experience.
|
||||
|
||||
| Area | Location | Description |
|
||||
|------|----------|-------------|
|
||||
| Runner | `src/runner/` | Main executable, tray icon, module loader, hotkey management |
|
||||
| Settings UI | `src/settings-ui/` | WinUI/WPF configuration app communicating via named pipes |
|
||||
| Modules | `src/modules/` | Individual PowerToys utilities (each in its own subfolder) |
|
||||
| Common Libraries | `src/common/` | Shared code: logging, IPC, settings, DPI, telemetry, utilities |
|
||||
| Build Tools | `tools/build/` | Build scripts and automation |
|
||||
| Documentation | `doc/devdocs/` | Developer documentation |
|
||||
| Installer | `installer/` | WiX-based installer projects |
|
||||
|
||||
For architecture details and module types, see [Architecture Overview](doc/devdocs/core/architecture.md).
|
||||
|
||||
## Conventions
|
||||
|
||||
For detailed coding conventions, see:
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md) – Dependencies, testing, PR management
|
||||
- [Coding Style](doc/devdocs/development/style.md) – Formatting, C++/C#/XAML style rules
|
||||
- [Logging](doc/devdocs/development/logging.md) – C++ spdlog and C# Logger usage
|
||||
|
||||
### Component-Specific Instructions
|
||||
|
||||
These instruction files are automatically applied when working in their respective areas:
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md) – IPC contracts, schema migrations
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md) – ABI stability, shared code guidelines
|
||||
|
||||
## Build
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Visual Studio 2022 17.4+
|
||||
- Windows 10 1803+ (April 2018 Update or newer)
|
||||
- Initialize submodules once: `git submodule update --init --recursive`
|
||||
|
||||
### Build Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| First build / NuGet restore | `tools\build\build-essentials.cmd` |
|
||||
| Build current folder | `tools\build\build.cmd` |
|
||||
| Build with options | `build.ps1 -Platform x64 -Configuration Release` |
|
||||
|
||||
### Build Discipline
|
||||
|
||||
1. One terminal per operation (build → test). Do not switch or open new ones mid-flow
|
||||
2. After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`)
|
||||
3. Use scripts to build: `tools/build/build.ps1` or `tools/build/build.cmd`
|
||||
4. For first build or missing NuGet packages, run `build-essentials.cmd` first
|
||||
5. **Exit code 0 = success; non-zero = failure** – treat this as absolute
|
||||
6. On failure, read the errors log: `build.<config>.<platform>.errors.log`
|
||||
7. Do not start tests or launch Runner until the build succeeds
|
||||
|
||||
### Build Logs
|
||||
|
||||
Located next to the solution/project being built:
|
||||
- `build.<configuration>.<platform>.errors.log` – errors only (check this first)
|
||||
- `build.<configuration>.<platform>.all.log` – full log
|
||||
- `build.<configuration>.<platform>.trace.binlog` – for MSBuild Structured Log Viewer
|
||||
|
||||
For complete details, see [Build Guidelines](tools/build/BUILD-GUIDELINES.md).
|
||||
|
||||
## Tests
|
||||
|
||||
### Test Discovery
|
||||
|
||||
- Find test projects by product code prefix (e.g., `FancyZones`, `AdvancedPaste`)
|
||||
- Look for sibling folders or 1-2 levels up named `<Product>*UnitTests` or `<Product>*UITests`
|
||||
|
||||
### Running Tests
|
||||
|
||||
1. **Build the test project first**, wait for exit code 0
|
||||
2. Run via VS Test Explorer (`Ctrl+E, T`) or `vstest.console.exe` with filters
|
||||
3. **Avoid `dotnet test`** in this repo – use VS Test Explorer or vstest.console.exe
|
||||
|
||||
### Test Types
|
||||
|
||||
| Type | Requirements | Setup |
|
||||
|------|--------------|-------|
|
||||
| Unit Tests | Standard dev environment | None |
|
||||
| UI Tests | WinAppDriver v1.2.1, Developer Mode | Install from [WinAppDriver releases](https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1) |
|
||||
| Fuzz Tests | OneFuzz, .NET 8 | See [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md) |
|
||||
|
||||
### Test Discipline
|
||||
|
||||
1. Add or adjust tests when changing behavior
|
||||
2. If tests skipped, state why (e.g., comment-only change, string rename)
|
||||
3. New modules handling file I/O or user input **must** implement fuzzing tests
|
||||
|
||||
### Special Requirements
|
||||
|
||||
- **Mouse Without Borders**: Requires 2+ physical computers (not VMs)
|
||||
- **Multi-monitor utilities**: Test with 2+ monitors, different DPI settings
|
||||
|
||||
For UI test setup details, see [UI Tests](doc/devdocs/development/ui-tests.md).
|
||||
|
||||
## Boundaries
|
||||
|
||||
### Ask for Clarification When
|
||||
|
||||
- Ambiguous spec after scanning relevant docs
|
||||
- Cross-module impact (shared enum/struct) is unclear
|
||||
- Security, elevation, or installer changes involved
|
||||
- GPO or policy handling modifications needed
|
||||
|
||||
### Areas Requiring Extra Care
|
||||
|
||||
| Area | Concern | Reference |
|
||||
|------|---------|-----------|
|
||||
| `src/common/` | ABI breaks | [Common Libraries Instructions](.github/instructions/common-libraries.instructions.md) |
|
||||
| `src/runner/`, `src/settings-ui/` | IPC contracts, schema | [Runner & Settings UI Instructions](.github/instructions/runner-settings-ui.instructions.md) |
|
||||
| Installer files | Release impact | Careful review required |
|
||||
| Elevation/GPO logic | Security | Confirm no regression in policy handling |
|
||||
|
||||
### What NOT to Do
|
||||
|
||||
- Don't merge incomplete features into main (use feature branches)
|
||||
- Don't break IPC/JSON contracts without updating both runner and settings-ui
|
||||
- Don't add noisy logs in hot paths
|
||||
- Don't introduce third-party deps without PM approval and `NOTICE.md` update
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before finishing, verify:
|
||||
|
||||
- [ ] Build clean with exit code 0
|
||||
- [ ] Tests updated and passing locally
|
||||
- [ ] No unintended ABI breaks or schema changes
|
||||
- [ ] IPC contracts consistent between runner and settings-ui
|
||||
- [ ] New dependencies added to `NOTICE.md`
|
||||
- [ ] PR is atomic (one logical change), with issue linked
|
||||
|
||||
## Documentation Index
|
||||
|
||||
### Core Architecture
|
||||
- [Architecture Overview](doc/devdocs/core/architecture.md)
|
||||
- [Runner](doc/devdocs/core/runner.md)
|
||||
- [Settings System](doc/devdocs/core/settings/readme.md)
|
||||
- [Module Interface](doc/devdocs/modules/interface.md)
|
||||
|
||||
### Development
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md)
|
||||
- [Coding Style](doc/devdocs/development/style.md)
|
||||
- [Logging](doc/devdocs/development/logging.md)
|
||||
- [UI Tests](doc/devdocs/development/ui-tests.md)
|
||||
- [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md)
|
||||
|
||||
### Build & Tools
|
||||
- [Build Guidelines](tools/build/BUILD-GUIDELINES.md)
|
||||
- [Tools Overview](doc/devdocs/tools/readme.md)
|
||||
|
||||
### Instructions (Auto-Applied)
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
36
COMMUNITY.md
36
COMMUNITY.md
@@ -6,6 +6,9 @@ Names are in alphabetical order based on first name.
|
||||
|
||||
## High impact community members
|
||||
|
||||
### [@Noraa-Junker](https://github.com/Noraa-Junker) - [Noraa Junker](https://noraajunker.ch)
|
||||
Noraa has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes. Noraa was the primary person for helping build the File Explorer preview pane handler for developer files.
|
||||
|
||||
### [@cgaarden](https://github.com/cgaarden) - [Christian Gaarden Gaardmark](https://www.onegreatworld.com)
|
||||
Christian contributed New+ utility
|
||||
|
||||
@@ -39,12 +42,6 @@ Jay has helped triaging, discussing, creating a substantial number of issues and
|
||||
### [@jefflord](https://github.com/Jjefflord) - Jeff Lord
|
||||
Jeff added in multiple new features into Keyboard manager, such as key chord support and launching apps. He also contributed multiple features/fixes to PowerToys.
|
||||
|
||||
### [@snickler](https://github.com/snickler) - [Jeremy Sinclair](http://sinclairinat0r.com)
|
||||
Jeremy has helped drive large sums of the ARM64 support inside PowerToys
|
||||
|
||||
### [@jiripolasek](https://github.com/jiripolasek) - [Jiří Polášek](https://github.com/jiripolasek)
|
||||
Jiří has contributed a massive number of features and improvements to Command Palette, including drag & drop support, custom themes, Web Search enhancements, Remote Desktop extension fixes, and many UX improvements.
|
||||
|
||||
### [@TheJoeFin](https://github.com/TheJoeFin) - [Joe Finney](https://joefinapps.com)
|
||||
Joe has helped triaging, discussing, issues as well as fixing bugs and building features for Text Extractor.
|
||||
|
||||
@@ -60,9 +57,6 @@ Color Picker is from Martin.
|
||||
### [@mikeclayton](https://github.com/mikeclayton) - [Michael Clayton](https://michael-clayton.com)
|
||||
Michael contributed the [initial version](https://github.com/microsoft/PowerToys/issues/23216) of the Mouse Jump tool and [a number of updates](https://github.com/microsoft/PowerToys/pulls?q=is%3Apr+author%3Amikeclayton) based on his FancyMouse utility.
|
||||
|
||||
### [@Noraa-Junker](https://github.com/Noraa-Junker) - [Noraa Junker](https://noraajunker.ch)
|
||||
Noraa has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes. Noraa was the primary person for helping build the File Explorer preview pane handler for developer files.
|
||||
|
||||
### [@pedrolamas](https://github.com/pedrolamas/) - Pedro Lamas
|
||||
Pedro helped create the thumbnail and File Explorer previewers for 3D files like STL and GCode. If you like 3D printing, these are very helpful.
|
||||
|
||||
@@ -75,12 +69,15 @@ Rafael has helped do the [upgrade from CppWinRT 1.x to 2.0](https://github.com/m
|
||||
### [@royvou](https://github.com/royvou)
|
||||
Roy has helped out contributing multiple features to PowerToys Run
|
||||
|
||||
### [@ThiefZero](https://github.com/ThiefZero)
|
||||
ThiefZero has helped out contributing a features to PowerToys Run such as the unit converter plugin
|
||||
### [@snickler](https://github.com/snickler) - [Jeremy Sinclair](http://sinclairinat0r.com)
|
||||
Jeremy has helped drive large sums of the ARM64 support inside PowerToys
|
||||
|
||||
### [@TobiasSekan](https://github.com/TobiasSekan) - Tobias Sekan
|
||||
Tobias Sekan has helped out contributing features to PowerToys Run such as Settings plugin, Registry plugin
|
||||
|
||||
### [@ThiefZero](https://github.com/ThiefZero)
|
||||
ThiefZero has helped out contributing a features to PowerToys Run such as the unit converter plugin
|
||||
|
||||
## Open source projects
|
||||
|
||||
As PowerToys creates new utilities, some will be based off existing technology. We'll continue to do our best to contribute back to these projects but their efforts were the base of some of our projects. We want to be sure their work is directly recognized.
|
||||
@@ -190,10 +187,18 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
- [@niels9001](https://github.com/niels9001/) - Niels Laute - Product Manager
|
||||
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev Lead
|
||||
- [@yeelam-gordon](https://github.com/yeelam-gordon) - Gordon Lam - Dev Lead
|
||||
- [@jamrobot](https://github.com/jamrobot) - Jerry Xu - Dev Lead
|
||||
- [@lei9444](https://github.com/lei9444) - Leilei Zhang - Dev
|
||||
- [@shuaiyuanxx](https://github.com/shuaiyuanxx) - Shawn Yuan - Dev
|
||||
- [@moooyo](https://github.com/moooyo) - Yu Leng - Dev
|
||||
- [@haoliuu](https://github.com/haoliuu) - Hao Liu - Dev
|
||||
- [@chenmy77](https://github.com/chenmy77) - Mengyuan Chen - Dev
|
||||
- [@chemwolf6922](https://github.com/chemwolf6922) - Feng Wang - Dev
|
||||
- [@yaqingmi](https://github.com/yaqingmi) - Yaqing Mi - Dev
|
||||
- [@zhaoqpcn](https://github.com/zhaoqpcn) - Qingpeng Zhao - Dev
|
||||
- [@urnotdfs](https://github.com/urnotdfs) - Xiaofeng Wang - Dev
|
||||
- [@zhaopy536](https://github.com/zhaopy536) - Peiyao Zhao - Dev
|
||||
- [@wang563681252](https://github.com/wang563681252) - Zhaopeng Wang - Dev
|
||||
- [@vanzue](https://github.com/vanzue) - Kai Tao - Dev
|
||||
- [@zadjii-msft](https://github.com/zadjii-msft) - Mike Griese - Dev
|
||||
- [@khmyznikov](https://github.com/khmyznikov) - Gleb Khmyznikov - Dev
|
||||
@@ -224,12 +229,3 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
- [@SeraphimaZykova](https://github.com/SeraphimaZykova) - Seraphima Zykova - Dev
|
||||
- [@stefansjfw](https://github.com/stefansjfw) - Stefan Markovic - Dev
|
||||
- [@jaimecbernardo](https://github.com/jaimecbernardo) - Jaime Bernardo - Dev Lead
|
||||
- [@haoliuu](https://github.com/haoliuu) - Hao Liu - Dev
|
||||
- [@chenmy77](https://github.com/chenmy77) - Mengyuan Chen - Dev
|
||||
- [@chemwolf6922](https://github.com/chemwolf6922) - Feng Wang - Dev
|
||||
- [@yaqingmi](https://github.com/yaqingmi) - Yaqing Mi - Dev
|
||||
- [@zhaoqpcn](https://github.com/zhaoqpcn) - Qingpeng Zhao - Dev
|
||||
- [@urnotdfs](https://github.com/urnotdfs) - Xiaofeng Wang - Dev
|
||||
- [@zhaopy536](https://github.com/zhaopy536) - Peiyao Zhao - Dev
|
||||
- [@wang563681252](https://github.com/wang563681252) - Zhaopeng Wang - Dev
|
||||
- [@jamrobot](https://github.com/jamrobot) - Jerry Xu - Dev Lead
|
||||
|
||||
@@ -1005,14 +1005,6 @@
|
||||
<Project Path="src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettingsInterop.vcxproj" Id="ca7d8106-30b9-4aec-9d05-b69b31b8c461" />
|
||||
</Folder>
|
||||
<Folder Name="/settings-ui/">
|
||||
<Project Path="src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/settings-ui/Settings.UI.Controls/Settings.UI.Controls.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
||||
var softlineBreak = new Markdig.Extensions.Hardlines.SoftlineBreakAsHardlineExtension();
|
||||
|
||||
MarkdownPipelineBuilder pipelineBuilder;
|
||||
pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics();
|
||||
pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics().DisableHtml();
|
||||
pipelineBuilder.Extensions.Add(extension);
|
||||
pipelineBuilder.Extensions.Add(softlineBreak);
|
||||
|
||||
|
||||
@@ -36,6 +36,5 @@ namespace ManagedCommon
|
||||
PowerOCR,
|
||||
Workspaces,
|
||||
ZoomIt,
|
||||
GeneralSettings,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,16 +119,6 @@ namespace PowerToysSettings
|
||||
class HotkeyObject
|
||||
{
|
||||
public:
|
||||
HotkeyObject() :
|
||||
m_json(json::JsonObject())
|
||||
{
|
||||
m_json.SetNamedValue(L"win", json::value(false));
|
||||
m_json.SetNamedValue(L"ctrl", json::value(false));
|
||||
m_json.SetNamedValue(L"alt", json::value(false));
|
||||
m_json.SetNamedValue(L"shift", json::value(false));
|
||||
m_json.SetNamedValue(L"code", json::value(0));
|
||||
m_json.SetNamedValue(L"key", json::value(L""));
|
||||
}
|
||||
static HotkeyObject from_json(json::JsonObject json)
|
||||
{
|
||||
return HotkeyObject(std::move(json));
|
||||
|
||||
16
src/common/common.instructions.md
Normal file
16
src/common/common.instructions.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
applyTo: "**/*.cs,**/*.cpp,**/*.c,**/*.h,**/*.hpp"
|
||||
---
|
||||
# Common – shared libraries guidance (concise)
|
||||
|
||||
Scope
|
||||
- Logging, IPC, settings, DPI, telemetry, utilities consumed by multiple modules.
|
||||
|
||||
Guidelines
|
||||
- Avoid breaking public headers/APIs; if changed, search & update all callers.
|
||||
- Coordinate ABI-impacting struct/class layout changes; keep binary compatibility.
|
||||
- Watch perf in hot paths (hooks, timers, serialization); avoid avoidable allocations.
|
||||
- Ask before adding third‑party deps or changing serialization formats.
|
||||
|
||||
Acceptance
|
||||
- No unintended ABI breaks, no noisy logs, new non-obvious symbols briefly commented.
|
||||
@@ -1,48 +1,5 @@
|
||||
#include "resource.h"
|
||||
#include <windows.h>
|
||||
#include "../../../common/version/version.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
#include "winres.h"
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS 0x40004L
|
||||
FILETYPE 0x1L
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", "File Locksmith CLI"
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", "FileLocksmithCLI.exe"
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", "FileLocksmithCLI.exe"
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
# Validating/Testing Cursor Wrap.
|
||||
|
||||
If a user determines that CursorWrap isn't working on their PC there are some steps you can take to determine why CursorWrap functionality might not be working as expected.
|
||||
|
||||
Note that for a single monitor cursor wrap should always work since all monitor edges are not touching/overlapping with other monitors - the cursor should always wrap to the opposite edge of the same monitor.
|
||||
|
||||
Multi-monitor is supported through building a polygon shape for the outer edges of all monitors, inner monitor edges are ignored, movement of the cursor from one monitor to an adjacent monitor is handled by Windows - CursorWrap doesn't get involved in monitor-to-monitor movement, only outer-edges.
|
||||
|
||||
We have seen a couple of computer setups that have multi-monitors where CursorWrap doesn't work as expected, this appears to be due to a monitor not being 'snapped' to the edge of an adjacent monitor - If you use Display Settings in Windows you can move monitors around, these appear to 'snap' to an edge of an existing monitor.
|
||||
|
||||
What to do if Cursor Wrapping isn't working as expected ?
|
||||
|
||||
1. in the CursorWrapTests folder there's a PowerShell script called `Capture-MonitorLayout.ps1` - this will generate a .json file in the form `"$($env:USERNAME)_monitor_layout.json` - the .json file contains an array of monitors, their position, size, dpi, and scaling.
|
||||
2. Use `CursorWrapTests/monitor_layout_tests.py` to validate the monitor layout/wrapping behavior (uses the json file from point 1 above).
|
||||
3. Use `analyze_test_results.py` to analyze the monitor layout test output and provide information about why wrapping might not be working
|
||||
|
||||
To run `monitor_layout_tests.py` you will need Python installed on your PC.
|
||||
|
||||
Run `python monitor_layout_tests.py --layout-file <path to json file>` you can also add an optional `--verbose` to view verbose output.
|
||||
|
||||
monitor_layout_tests.py will produce an output file called `test_report.json` - the contents of the file will look like this (this is from a single monitor test).
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"total_configs": 1,
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"total_issues": 0,
|
||||
"pass_rate": "100.00%"
|
||||
},
|
||||
"failures": [],
|
||||
"recommendations": [
|
||||
"All tests passed - edge detection logic is working correctly!"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If there are failures (the failures array is not empty) you can run the second python application called `analyze_test_results.py`
|
||||
|
||||
Supported options include:
|
||||
```text
|
||||
-h, --help show this help message and exit
|
||||
--report REPORT Path to test report JSON file
|
||||
--detailed Show detailed failure listing
|
||||
--copilot Generate GitHub Copilot-friendly fix prompt
|
||||
```
|
||||
|
||||
Running the analyze_test_results.py script against our single monitor test results produces the following:
|
||||
|
||||
```text
|
||||
python .\analyze_test_results.py --detailed
|
||||
================================================================================
|
||||
CURSORWRAP TEST RESULTS ANALYSIS
|
||||
================================================================================
|
||||
|
||||
Total Configurations Tested: 1
|
||||
Passed: 1 (100.00%)
|
||||
Failed: 0
|
||||
Total Issues: 0
|
||||
|
||||
✓ ALL TESTS PASSED! Edge detection logic is working correctly.
|
||||
|
||||
✓ No failures to analyze!
|
||||
```
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Captures the current monitor layout configuration for CursorWrap testing.
|
||||
|
||||
.DESCRIPTION
|
||||
Queries Windows for all connected monitors and saves their configuration
|
||||
(position, size, DPI, primary status) to a JSON file that can be used
|
||||
for testing the CursorWrap module.
|
||||
|
||||
.PARAMETER OutputPath
|
||||
Path where the JSON file will be saved. Default: monitor_layout.json
|
||||
|
||||
.EXAMPLE
|
||||
.\Capture-MonitorLayout.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Capture-MonitorLayout.ps1 -OutputPath "my_setup.json"
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$OutputPath = "$($env:USERNAME)_monitor_layout.json"
|
||||
)
|
||||
|
||||
# Add Windows Forms for screen enumeration
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
function Get-MonitorDPI {
|
||||
param([System.Windows.Forms.Screen]$Screen)
|
||||
|
||||
# Try to get DPI using P/Invoke with multiple methods
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class DisplayConfig {
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags);
|
||||
|
||||
[DllImport("shcore.dll")]
|
||||
public static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT {
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||
public struct MONITORINFOEX {
|
||||
public int cbSize;
|
||||
public RECT rcMonitor;
|
||||
public RECT rcWork;
|
||||
public uint dwFlags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string szDevice;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT {
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
public const uint MONITOR_DEFAULTTOPRIMARY = 1;
|
||||
public const int MDT_EFFECTIVE_DPI = 0;
|
||||
public const int MDT_ANGULAR_DPI = 1;
|
||||
public const int MDT_RAW_DPI = 2;
|
||||
public const int LOGPIXELSX = 88;
|
||||
public const int LOGPIXELSY = 90;
|
||||
}
|
||||
"@ -ErrorAction SilentlyContinue
|
||||
|
||||
try {
|
||||
$point = New-Object DisplayConfig+POINT
|
||||
$point.X = $Screen.Bounds.Left + ($Screen.Bounds.Width / 2)
|
||||
$point.Y = $Screen.Bounds.Top + ($Screen.Bounds.Height / 2)
|
||||
|
||||
$hMonitor = [DisplayConfig]::MonitorFromPoint($point, 1)
|
||||
|
||||
# Method 1: Try GetDpiForMonitor (Windows 8.1+)
|
||||
[uint]$dpiX = 0
|
||||
[uint]$dpiY = 0
|
||||
$result = [DisplayConfig]::GetDpiForMonitor($hMonitor, 0, [ref]$dpiX, [ref]$dpiY)
|
||||
|
||||
if ($result -eq 0 -and $dpiX -gt 0) {
|
||||
Write-Verbose "DPI detected via GetDpiForMonitor: $dpiX"
|
||||
return $dpiX
|
||||
}
|
||||
|
||||
# Method 2: Try RAW DPI
|
||||
$result = [DisplayConfig]::GetDpiForMonitor($hMonitor, 2, [ref]$dpiX, [ref]$dpiY)
|
||||
if ($result -eq 0 -and $dpiX -gt 0) {
|
||||
Write-Verbose "DPI detected via RAW DPI: $dpiX"
|
||||
return $dpiX
|
||||
}
|
||||
|
||||
# Method 3: Try getting device context DPI (legacy method)
|
||||
$hdc = [DisplayConfig]::GetDC([IntPtr]::Zero)
|
||||
if ($hdc -ne [IntPtr]::Zero) {
|
||||
$dpiValue = [DisplayConfig]::GetDeviceCaps($hdc, 88) # LOGPIXELSX
|
||||
[DisplayConfig]::ReleaseDC([IntPtr]::Zero, $hdc)
|
||||
if ($dpiValue -gt 0) {
|
||||
Write-Verbose "DPI detected via GetDeviceCaps: $dpiValue"
|
||||
return $dpiValue
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "DPI detection error: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Write-Warning "Could not detect DPI for $($Screen.DeviceName), using default 96 DPI"
|
||||
return 96 # Standard 96 DPI (100% scaling)
|
||||
}
|
||||
|
||||
function Capture-MonitorLayout {
|
||||
Write-Host "Capturing monitor layout..." -ForegroundColor Cyan
|
||||
Write-Host "=" * 80
|
||||
|
||||
$screens = [System.Windows.Forms.Screen]::AllScreens
|
||||
$monitors = @()
|
||||
|
||||
foreach ($screen in $screens) {
|
||||
$isPrimary = $screen.Primary
|
||||
$bounds = $screen.Bounds
|
||||
$dpi = Get-MonitorDPI -Screen $screen
|
||||
|
||||
$monitor = [ordered]@{
|
||||
left = $bounds.Left
|
||||
top = $bounds.Top
|
||||
right = $bounds.Right
|
||||
bottom = $bounds.Bottom
|
||||
width = $bounds.Width
|
||||
height = $bounds.Height
|
||||
dpi = $dpi
|
||||
scaling_percent = [math]::Round(($dpi / 96.0) * 100, 0)
|
||||
primary = $isPrimary
|
||||
device_name = $screen.DeviceName
|
||||
}
|
||||
|
||||
$monitors += $monitor
|
||||
|
||||
# Display info
|
||||
$primaryTag = if ($isPrimary) { " [PRIMARY]" } else { "" }
|
||||
$scaling = [math]::Round(($dpi / 96.0) * 100, 0)
|
||||
|
||||
Write-Host "`nMonitor $($monitors.Count)$primaryTag" -ForegroundColor Green
|
||||
Write-Host " Device: $($screen.DeviceName)"
|
||||
Write-Host " Position: ($($bounds.Left), $($bounds.Top))"
|
||||
Write-Host " Size: $($bounds.Width)x$($bounds.Height)"
|
||||
Write-Host " DPI: $dpi ($scaling% scaling)"
|
||||
Write-Host " Bounds: [$($bounds.Left), $($bounds.Top), $($bounds.Right), $($bounds.Bottom)]"
|
||||
}
|
||||
|
||||
# Create output object
|
||||
$output = [ordered]@{
|
||||
captured_at = (Get-Date -Format "yyyy-MM-ddTHH:mm:sszzz")
|
||||
computer_name = $env:COMPUTERNAME
|
||||
user_name = $env:USERNAME
|
||||
monitor_count = $monitors.Count
|
||||
monitors = $monitors
|
||||
}
|
||||
|
||||
# Save to JSON
|
||||
$json = $output | ConvertTo-Json -Depth 10
|
||||
Set-Content -Path $OutputPath -Value $json -Encoding UTF8
|
||||
|
||||
Write-Host "`n" + ("=" * 80)
|
||||
Write-Host "Monitor layout saved to: $OutputPath" -ForegroundColor Green
|
||||
Write-Host "Total monitors captured: $($monitors.Count)" -ForegroundColor Cyan
|
||||
Write-Host "`nYou can now use this file with the test script:" -ForegroundColor Yellow
|
||||
Write-Host " python monitor_layout_tests.py --layout-file $OutputPath" -ForegroundColor White
|
||||
|
||||
return $output
|
||||
}
|
||||
|
||||
# Main execution
|
||||
try {
|
||||
$layout = Capture-MonitorLayout
|
||||
|
||||
# Display summary
|
||||
Write-Host "`n" + ("=" * 80)
|
||||
Write-Host "SUMMARY" -ForegroundColor Cyan
|
||||
Write-Host ("=" * 80)
|
||||
Write-Host "Configuration Name: $($layout.computer_name)"
|
||||
Write-Host "Captured: $($layout.captured_at)"
|
||||
Write-Host "Monitors: $($layout.monitor_count)"
|
||||
|
||||
# Calculate desktop dimensions
|
||||
$widths = @($layout.monitors | ForEach-Object { $_.width })
|
||||
$heights = @($layout.monitors | ForEach-Object { $_.height })
|
||||
|
||||
$totalWidth = ($widths | Measure-Object -Sum).Sum
|
||||
$maxHeight = ($heights | Measure-Object -Maximum).Maximum
|
||||
|
||||
Write-Host "Total Desktop Width: $totalWidth pixels"
|
||||
Write-Host "Max Desktop Height: $maxHeight pixels"
|
||||
|
||||
# Analyze potential coordinate issues
|
||||
Write-Host "`n" + ("=" * 80)
|
||||
Write-Host "COORDINATE ANALYSIS" -ForegroundColor Cyan
|
||||
Write-Host ("=" * 80)
|
||||
|
||||
# Check for gaps between monitors
|
||||
if ($layout.monitor_count -gt 1) {
|
||||
$hasGaps = $false
|
||||
for ($i = 0; $i -lt $layout.monitor_count - 1; $i++) {
|
||||
$m1 = $layout.monitors[$i]
|
||||
for ($j = $i + 1; $j -lt $layout.monitor_count; $j++) {
|
||||
$m2 = $layout.monitors[$j]
|
||||
|
||||
# Check horizontal gap
|
||||
$hGap = [Math]::Min([Math]::Abs($m1.right - $m2.left), [Math]::Abs($m2.right - $m1.left))
|
||||
# Check vertical overlap
|
||||
$vOverlapStart = [Math]::Max($m1.top, $m2.top)
|
||||
$vOverlapEnd = [Math]::Min($m1.bottom, $m2.bottom)
|
||||
$vOverlap = $vOverlapEnd - $vOverlapStart
|
||||
|
||||
if ($hGap -gt 50 -and $vOverlap -gt 0) {
|
||||
Write-Host "⚠ Gap detected between Monitor $($i+1) and Monitor $($j+1): ${hGap}px horizontal gap" -ForegroundColor Yellow
|
||||
Write-Host " Vertical overlap: ${vOverlap}px" -ForegroundColor Yellow
|
||||
Write-Host " This may indicate a Windows coordinate bug if monitors appear snapped in Display Settings" -ForegroundColor Yellow
|
||||
$hasGaps = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $hasGaps) {
|
||||
Write-Host "✓ No unexpected gaps detected" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# DPI/Scaling notes
|
||||
Write-Host "`nDPI/Scaling Impact on Coordinates:" -ForegroundColor Cyan
|
||||
Write-Host "• Coordinate values (left, top, right, bottom) are in LOGICAL PIXELS"
|
||||
Write-Host "• These are DPI-independent virtual coordinates"
|
||||
Write-Host "• Physical pixels = Logical pixels × (DPI / 96)"
|
||||
Write-Host "• Example: 1920 logical pixels at 150% scaling = 1920 × 1.5 = 2880 physical pixels"
|
||||
Write-Host "• Windows snaps monitors using logical pixel coordinates"
|
||||
Write-Host "• If monitors appear snapped but coordinates show gaps, this is a Windows bug"
|
||||
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Host "`nError capturing monitor layout:" -ForegroundColor Red
|
||||
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray
|
||||
exit 1
|
||||
}
|
||||
@@ -1,430 +0,0 @@
|
||||
"""
|
||||
Test Results Analyzer for CursorWrap Monitor Layout Tests
|
||||
|
||||
Analyzes test_report.json and provides detailed explanations of failures,
|
||||
patterns, and recommendations.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Any
|
||||
|
||||
|
||||
class TestResultAnalyzer:
|
||||
"""Analyzes test results and provides insights"""
|
||||
|
||||
def __init__(self, report_path: str = "test_report.json"):
|
||||
with open(report_path, 'r') as f:
|
||||
self.report = json.load(f)
|
||||
|
||||
self.failures = self.report.get('failures', [])
|
||||
self.summary = self.report.get('summary', {})
|
||||
self.recommendations = self.report.get('recommendations', [])
|
||||
|
||||
def print_overview(self):
|
||||
"""Print test overview"""
|
||||
print("=" * 80)
|
||||
print("CURSORWRAP TEST RESULTS ANALYSIS")
|
||||
print("=" * 80)
|
||||
print(f"\nTotal Configurations Tested: {self.summary.get('total_configs', 0)}")
|
||||
print(f"Passed: {self.summary.get('passed', 0)} ({self.summary.get('pass_rate', 'N/A')})")
|
||||
print(f"Failed: {self.summary.get('failed', 0)}")
|
||||
print(f"Total Issues: {self.summary.get('total_issues', 0)}")
|
||||
|
||||
if self.summary.get('passed', 0) == self.summary.get('total_configs', 0):
|
||||
print("\n✓ ALL TESTS PASSED! Edge detection logic is working correctly.")
|
||||
return
|
||||
|
||||
print(f"\n⚠ {self.summary.get('total_issues', 0)} issues detected\n")
|
||||
|
||||
def analyze_failure_patterns(self):
|
||||
"""Analyze and categorize failure patterns"""
|
||||
print("=" * 80)
|
||||
print("FAILURE PATTERN ANALYSIS")
|
||||
print("=" * 80)
|
||||
|
||||
# Group by test type
|
||||
by_test_type = defaultdict(list)
|
||||
for failure in self.failures:
|
||||
by_test_type[failure['test_name']].append(failure)
|
||||
|
||||
# Group by configuration
|
||||
by_config = defaultdict(list)
|
||||
for failure in self.failures:
|
||||
by_config[failure['monitor_config']].append(failure)
|
||||
|
||||
print(f"\n1. Failures by Test Type:")
|
||||
for test_type, failures in sorted(by_test_type.items(), key=lambda x: len(x[1]), reverse=True):
|
||||
print(f" • {test_type}: {len(failures)} failures")
|
||||
|
||||
print(f"\n2. Configurations with Failures:")
|
||||
for config, failures in sorted(by_config.items(), key=lambda x: len(x[1]), reverse=True):
|
||||
print(f" • {config}")
|
||||
print(f" {len(failures)} issues")
|
||||
|
||||
return by_test_type, by_config
|
||||
|
||||
def analyze_wrap_calculation_failures(self, failures: List[Dict[str, Any]]):
|
||||
"""Detailed analysis of wrap calculation failures"""
|
||||
print("\n" + "=" * 80)
|
||||
print("WRAP CALCULATION FAILURE ANALYSIS")
|
||||
print("=" * 80)
|
||||
|
||||
# Analyze cursor positions
|
||||
positions = []
|
||||
configs = set()
|
||||
|
||||
for failure in failures:
|
||||
configs.add(failure['monitor_config'])
|
||||
# Extract position from expected message
|
||||
if 'test_point' in failure.get('details', {}):
|
||||
pos = failure['details']['test_point']
|
||||
positions.append(pos)
|
||||
|
||||
print(f"\nAffected Configurations: {len(configs)}")
|
||||
for config in sorted(configs):
|
||||
print(f" • {config}")
|
||||
|
||||
if positions:
|
||||
print(f"\nFailed Test Points: {len(positions)}")
|
||||
# Analyze if failures are at edges
|
||||
edge_positions = defaultdict(int)
|
||||
for x, y in positions:
|
||||
# Simplified edge detection
|
||||
if x <= 10:
|
||||
edge_positions['left edge'] += 1
|
||||
elif y <= 10:
|
||||
edge_positions['top edge'] += 1
|
||||
else:
|
||||
edge_positions['other'] += 1
|
||||
|
||||
if edge_positions:
|
||||
print("\nPosition Distribution:")
|
||||
for pos_type, count in edge_positions.items():
|
||||
print(f" • {pos_type}: {count}")
|
||||
|
||||
def explain_common_issues(self):
|
||||
"""Explain common issues found in results"""
|
||||
print("\n" + "=" * 80)
|
||||
print("COMMON ISSUE EXPLANATIONS")
|
||||
print("=" * 80)
|
||||
|
||||
has_wrap_failures = any(f['test_name'] == 'wrap_calculation' for f in self.failures)
|
||||
has_edge_failures = any(f['test_name'] == 'single_monitor_edges' for f in self.failures)
|
||||
has_touching_failures = any(f['test_name'] == 'touching_monitors' for f in self.failures)
|
||||
|
||||
if has_wrap_failures:
|
||||
print("\n⚠ WRAP CALCULATION FAILURES")
|
||||
print("-" * 80)
|
||||
print("Issue: Cursor is on an outer edge but wrapping is not occurring.")
|
||||
print("\nLikely Causes:")
|
||||
print(" 1. Partial Overlap Problem:")
|
||||
print(" • When monitors have different sizes (e.g., 4K + 1080p)")
|
||||
print(" • Only part of an edge is actually adjacent to another monitor")
|
||||
print(" • Current code marks the ENTIRE edge as non-outer if ANY part is adjacent")
|
||||
print(" • This prevents wrapping even in regions where it should occur")
|
||||
print("\n 2. Edge Detection Logic:")
|
||||
print(" • Check IdentifyOuterEdges() in MonitorTopology.cpp")
|
||||
print(" • Consider segmenting edges based on actual overlap regions")
|
||||
print("\n 3. Test Point Selection:")
|
||||
print(" • Failures may be at corners or quarter points")
|
||||
print(" • Indicates edge behavior varies along its length")
|
||||
|
||||
if has_edge_failures:
|
||||
print("\n⚠ SINGLE MONITOR EDGE FAILURES")
|
||||
print("-" * 80)
|
||||
print("Issue: Single monitor should have exactly 4 outer edges.")
|
||||
print("\nThis indicates a fundamental problem in edge detection baseline.")
|
||||
|
||||
if has_touching_failures:
|
||||
print("\n⚠ TOUCHING MONITORS FAILURES")
|
||||
print("-" * 80)
|
||||
print("Issue: Adjacent monitors not detected correctly.")
|
||||
print("\nCheck EdgesAreAdjacent() logic and 50px tolerance settings.")
|
||||
|
||||
def print_recommendations(self):
|
||||
"""Print recommendations from the report"""
|
||||
if not self.recommendations:
|
||||
return
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("RECOMMENDATIONS")
|
||||
print("=" * 80)
|
||||
|
||||
for i, rec in enumerate(self.recommendations, 1):
|
||||
print(f"\n{i}. {rec}")
|
||||
|
||||
def detailed_failure_dump(self):
|
||||
"""Print all failure details"""
|
||||
print("\n" + "=" * 80)
|
||||
print("DETAILED FAILURE LISTING")
|
||||
print("=" * 80)
|
||||
|
||||
for i, failure in enumerate(self.failures, 1):
|
||||
print(f"\n[{i}] {failure['test_name']}")
|
||||
print(f"Configuration: {failure['monitor_config']}")
|
||||
print(f"Expected: {failure['expected']}")
|
||||
print(f"Actual: {failure['actual']}")
|
||||
|
||||
if 'details' in failure:
|
||||
details = failure['details']
|
||||
if 'edge' in details:
|
||||
edge = details['edge']
|
||||
print(f"Edge: {edge.get('edge_type', 'N/A')} at position {edge.get('position', 'N/A')}, "
|
||||
f"range [{edge.get('range_start', 'N/A')}, {edge.get('range_end', 'N/A')}]")
|
||||
if 'test_point' in details:
|
||||
print(f"Test Point: {details['test_point']}")
|
||||
print("-" * 80)
|
||||
|
||||
def generate_github_copilot_prompt(self):
|
||||
"""Generate a prompt suitable for GitHub Copilot to fix the issues"""
|
||||
print("\n" + "=" * 80)
|
||||
print("GITHUB COPILOT FIX PROMPT")
|
||||
print("=" * 80)
|
||||
print("\n```markdown")
|
||||
print("# CursorWrap Edge Detection Bug Report")
|
||||
print()
|
||||
print("## Test Results Summary")
|
||||
print(f"- Total Configurations Tested: {self.summary.get('total_configs', 0)}")
|
||||
print(f"- Pass Rate: {self.summary.get('pass_rate', 'N/A')}")
|
||||
print(f"- Failed Tests: {self.summary.get('failed', 0)}")
|
||||
print(f"- Total Issues: {self.summary.get('total_issues', 0)}")
|
||||
print()
|
||||
|
||||
# Group failures
|
||||
by_test_type = defaultdict(list)
|
||||
for failure in self.failures:
|
||||
by_test_type[failure['test_name']].append(failure)
|
||||
|
||||
print("## Critical Issues Found")
|
||||
print()
|
||||
|
||||
# Analyze wrap calculation failures
|
||||
if 'wrap_calculation' in by_test_type:
|
||||
failures = by_test_type['wrap_calculation']
|
||||
configs = set(f['monitor_config'] for f in failures)
|
||||
|
||||
print("### 1. Wrap Calculation Failures (PARTIAL OVERLAP BUG)")
|
||||
print()
|
||||
print(f"**Count**: {len(failures)} failures across {len(configs)} configuration(s)")
|
||||
print()
|
||||
print("**Affected Configurations**:")
|
||||
for config in sorted(configs):
|
||||
print(f"- {config}")
|
||||
print()
|
||||
|
||||
print("**Root Cause Analysis**:")
|
||||
print()
|
||||
print("The current implementation in `MonitorTopology::IdentifyOuterEdges()` marks an")
|
||||
print("ENTIRE edge as non-outer if ANY portion of that edge is adjacent to another monitor.")
|
||||
print()
|
||||
print("**Problem Scenario**: 1080p monitor + 4K monitor at bottom-right")
|
||||
print("```")
|
||||
print("4K Monitor (3840x2160 at 0,0)")
|
||||
print("┌────────────────────────────────────────┐")
|
||||
print("│ │ <- Y: 0-1080 NO adjacent monitor")
|
||||
print("│ │ RIGHT EDGE SHOULD BE OUTER")
|
||||
print("│ │")
|
||||
print("│ │┌──────────┐")
|
||||
print("│ ││ 1080p │ <- Y: 1080-2160 HAS adjacent")
|
||||
print("└────────────────────────────────────────┘│ at │ RIGHT EDGE NOT OUTER")
|
||||
print(" │ (3840, │")
|
||||
print(" │ 1080) │")
|
||||
print(" └──────────┘")
|
||||
print("```")
|
||||
print()
|
||||
print("**Current Behavior**: Right edge of 4K monitor is marked as NON-OUTER for entire")
|
||||
print("range (Y: 0-2160) because it detects adjacency in the bottom portion (Y: 1080-2160).")
|
||||
print()
|
||||
print("**Expected Behavior**: Right edge should be:")
|
||||
print("- OUTER from Y: 0 to Y: 1080 (no adjacent monitor)")
|
||||
print("- NON-OUTER from Y: 1080 to Y: 2160 (adjacent to 1080p monitor)")
|
||||
print()
|
||||
|
||||
print("**Failed Test Examples**:")
|
||||
print()
|
||||
for i, failure in enumerate(failures[:3], 1): # Show first 3
|
||||
details = failure.get('details', {})
|
||||
test_point = details.get('test_point', 'N/A')
|
||||
edge = details.get('edge', {})
|
||||
edge_type = edge.get('edge_type', 'N/A')
|
||||
position = edge.get('position', 'N/A')
|
||||
range_start = edge.get('range_start', 'N/A')
|
||||
range_end = edge.get('range_end', 'N/A')
|
||||
|
||||
print(f"{i}. **Configuration**: {failure['monitor_config']}")
|
||||
print(f" - Test Point: {test_point}")
|
||||
print(f" - Edge: {edge_type} at X={position}, Y range=[{range_start}, {range_end}]")
|
||||
print(f" - Expected: Cursor wraps to opposite edge")
|
||||
print(f" - Actual: No wrap occurred (edge incorrectly marked as non-outer)")
|
||||
print()
|
||||
|
||||
if len(failures) > 3:
|
||||
print(f" ... and {len(failures) - 3} more similar failures")
|
||||
print()
|
||||
|
||||
# Other failure types
|
||||
if 'single_monitor_edges' in by_test_type:
|
||||
print("### 2. Single Monitor Edge Detection Failures")
|
||||
print()
|
||||
print(f"**Count**: {len(by_test_type['single_monitor_edges'])} failures")
|
||||
print()
|
||||
print("Single monitor configurations should have exactly 4 outer edges.")
|
||||
print("This indicates a fundamental problem in baseline edge detection.")
|
||||
print()
|
||||
|
||||
if 'touching_monitors' in by_test_type:
|
||||
print("### 3. Adjacent Monitor Detection Failures")
|
||||
print()
|
||||
print(f"**Count**: {len(by_test_type['touching_monitors'])} failures")
|
||||
print()
|
||||
print("Adjacent monitors not being detected correctly by EdgesAreAdjacent().")
|
||||
print()
|
||||
|
||||
print("## Required Code Changes")
|
||||
print()
|
||||
print("### File: `MonitorTopology.cpp`")
|
||||
print()
|
||||
print("**Change 1**: Modify `IdentifyOuterEdges()` to support partial edge adjacency")
|
||||
print()
|
||||
print("Instead of marking entire edges as outer/non-outer, the code needs to:")
|
||||
print()
|
||||
print("1. **Segment edges** based on actual overlap regions with adjacent monitors")
|
||||
print("2. Create **sub-edges** for portions of an edge that have different outer status")
|
||||
print("3. Update `IsOnOuterEdge()` to check if the **cursor's specific position** is on an outer portion")
|
||||
print()
|
||||
print("**Proposed Approach**:")
|
||||
print()
|
||||
print("```cpp")
|
||||
print("// Instead of: edge.isOuter = true/false for entire edge")
|
||||
print("// Use: Store list of outer ranges for each edge")
|
||||
print()
|
||||
print("struct MonitorEdge {")
|
||||
print(" // ... existing fields ...")
|
||||
print(" std::vector<std::pair<int, int>> outerRanges; // Ranges where edge is outer")
|
||||
print("};")
|
||||
print()
|
||||
print("// In IdentifyOuterEdges():")
|
||||
print("// For each edge, find ALL adjacent opposite edges")
|
||||
print("// Calculate which portions of the edge have NO adjacent opposite")
|
||||
print("// Store these as outer ranges")
|
||||
print()
|
||||
print("// In IsOnOuterEdge():")
|
||||
print("// Check if cursor position falls within any outer range")
|
||||
print("if (edge.type == EdgeType::Left || edge.type == EdgeType::Right) {")
|
||||
print(" // Check if cursorPos.y is in any outer range")
|
||||
print("} else {")
|
||||
print(" // Check if cursorPos.x is in any outer range")
|
||||
print("}")
|
||||
print("```")
|
||||
print()
|
||||
print("**Change 2**: Update `EdgesAreAdjacent()` validation")
|
||||
print()
|
||||
print("The 50px tolerance logic is correct but needs to return overlap range info:")
|
||||
print()
|
||||
print("```cpp")
|
||||
print("struct AdjacencyResult {")
|
||||
print(" bool isAdjacent;")
|
||||
print(" int overlapStart; // Where the adjacency begins")
|
||||
print(" int overlapEnd; // Where the adjacency ends")
|
||||
print("};")
|
||||
print()
|
||||
print("AdjacencyResult CheckEdgeAdjacency(const MonitorEdge& edge1, ")
|
||||
print(" const MonitorEdge& edge2, ")
|
||||
print(" int tolerance);")
|
||||
print("```")
|
||||
print()
|
||||
print("## Test Validation")
|
||||
print()
|
||||
print("After implementing changes, run:")
|
||||
print("```bash")
|
||||
print("python monitor_layout_tests.py --max-monitors 10")
|
||||
print("```")
|
||||
print()
|
||||
print("Expected results:")
|
||||
print("- All 21+ configurations should pass")
|
||||
print("- Specifically, the 4K+1080p configuration should pass all 5 test points per edge")
|
||||
print("- Wrap calculation should work correctly at partial overlap boundaries")
|
||||
print()
|
||||
print("## Files to Modify")
|
||||
print()
|
||||
print("1. `MonitorTopology.h` - Update MonitorEdge structure")
|
||||
print("2. `MonitorTopology.cpp` - Implement segmented edge detection")
|
||||
print(" - `IdentifyOuterEdges()` - Main logic change")
|
||||
print(" - `IsOnOuterEdge()` - Check position against ranges")
|
||||
print(" - `EdgesAreAdjacent()` - Optionally return range info")
|
||||
print()
|
||||
print("```")
|
||||
|
||||
def run_analysis(self, detailed: bool = False, copilot_mode: bool = False):
|
||||
"""Run complete analysis"""
|
||||
if copilot_mode:
|
||||
self.generate_github_copilot_prompt()
|
||||
return
|
||||
|
||||
self.print_overview()
|
||||
|
||||
if not self.failures:
|
||||
print("\n✓ No failures to analyze!")
|
||||
return
|
||||
|
||||
by_test_type, by_config = self.analyze_failure_patterns()
|
||||
|
||||
# Specific analysis for wrap calculation failures
|
||||
if 'wrap_calculation' in by_test_type:
|
||||
self.analyze_wrap_calculation_failures(by_test_type['wrap_calculation'])
|
||||
|
||||
self.explain_common_issues()
|
||||
self.print_recommendations()
|
||||
|
||||
if detailed:
|
||||
self.detailed_failure_dump()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze CursorWrap test results"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report",
|
||||
default="test_report.json",
|
||||
help="Path to test report JSON file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--detailed",
|
||||
action="store_true",
|
||||
help="Show detailed failure listing"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--copilot",
|
||||
action="store_true",
|
||||
help="Generate GitHub Copilot-friendly fix prompt"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
analyzer = TestResultAnalyzer(args.report)
|
||||
analyzer.run_analysis(detailed=args.detailed, copilot_mode=args.copilot)
|
||||
|
||||
# Exit with error code if there were failures
|
||||
sys.exit(0 if not analyzer.failures else 1)
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Could not find report file: {args.report}")
|
||||
print("\nRun monitor_layout_tests.py first to generate the report.")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError:
|
||||
print(f"Error: Invalid JSON in report file: {args.report}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error analyzing report: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,892 +0,0 @@
|
||||
"""
|
||||
Monitor Layout Edge Detection Test Suite for CursorWrap
|
||||
|
||||
This script validates the edge detection and wrapping logic across thousands of
|
||||
monitor configurations without requiring the full PowerToys build environment.
|
||||
|
||||
Tests:
|
||||
- 1-4 monitor configurations
|
||||
- Common resolutions and DPI scales
|
||||
- Various arrangements (horizontal, vertical, L-shape, grid)
|
||||
- Edge detection (touching vs. gap)
|
||||
- Wrap calculations
|
||||
|
||||
Output: JSON report with failures for GitHub Copilot analysis
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
from enum import Enum
|
||||
import sys
|
||||
|
||||
# ============================================================================
|
||||
# Data Structures (mirrors C++ implementation)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonitorInfo:
|
||||
"""Represents a physical monitor"""
|
||||
left: int
|
||||
top: int
|
||||
right: int
|
||||
bottom: int
|
||||
dpi: int = 96
|
||||
primary: bool = False
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return self.right - self.left
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
return self.bottom - self.top
|
||||
|
||||
@property
|
||||
def center_x(self) -> int:
|
||||
return (self.left + self.right) // 2
|
||||
|
||||
@property
|
||||
def center_y(self) -> int:
|
||||
return (self.top + self.bottom) // 2
|
||||
|
||||
|
||||
class EdgeType(Enum):
|
||||
LEFT = "Left"
|
||||
RIGHT = "Right"
|
||||
TOP = "Top"
|
||||
BOTTOM = "Bottom"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Edge:
|
||||
"""Represents a monitor edge"""
|
||||
edge_type: EdgeType
|
||||
position: int # x for vertical, y for horizontal
|
||||
range_start: int
|
||||
range_end: int
|
||||
monitor_index: int
|
||||
|
||||
def overlaps(self, other: 'Edge', tolerance: int = 1) -> bool:
|
||||
"""Check if two edges overlap in their perpendicular range"""
|
||||
if self.edge_type != other.edge_type:
|
||||
return False
|
||||
if abs(self.position - other.position) > tolerance:
|
||||
return False
|
||||
return not (
|
||||
self.range_end <= other.range_start or other.range_end <= self.range_start)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestFailure:
|
||||
"""Records a test failure for analysis"""
|
||||
test_name: str
|
||||
monitor_config: str
|
||||
expected: str
|
||||
actual: str
|
||||
details: Dict
|
||||
|
||||
# ============================================================================
|
||||
# Edge Detection Logic (Python implementation of C++ logic)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MonitorTopology:
|
||||
"""Implements the edge detection logic to be validated"""
|
||||
|
||||
ADJACENCY_TOLERANCE = 50 # Pixels - tolerance for detecting adjacent edges (matches C++ implementation)
|
||||
EDGE_THRESHOLD = 1 # Pixels - cursor must be within this distance to trigger wrap
|
||||
|
||||
def __init__(self, monitors: List[MonitorInfo]):
|
||||
self.monitors = monitors
|
||||
self.outer_edges: List[Edge] = []
|
||||
self._detect_outer_edges()
|
||||
|
||||
def _detect_outer_edges(self):
|
||||
"""Detect which edges are outer (can wrap)"""
|
||||
all_edges = self._collect_all_edges()
|
||||
|
||||
for edge in all_edges:
|
||||
if self._is_outer_edge(edge, all_edges):
|
||||
self.outer_edges.append(edge)
|
||||
|
||||
def _collect_all_edges(self) -> List[Edge]:
|
||||
"""Collect all edges from all monitors"""
|
||||
edges = []
|
||||
|
||||
for idx, mon in enumerate(self.monitors):
|
||||
edges.append(
|
||||
Edge(
|
||||
EdgeType.LEFT,
|
||||
mon.left,
|
||||
mon.top,
|
||||
mon.bottom,
|
||||
idx))
|
||||
edges.append(
|
||||
Edge(
|
||||
EdgeType.RIGHT,
|
||||
mon.right,
|
||||
mon.top,
|
||||
mon.bottom,
|
||||
idx))
|
||||
edges.append(Edge(EdgeType.TOP, mon.top, mon.left, mon.right, idx))
|
||||
edges.append(
|
||||
Edge(
|
||||
EdgeType.BOTTOM,
|
||||
mon.bottom,
|
||||
mon.left,
|
||||
mon.right,
|
||||
idx))
|
||||
|
||||
return edges
|
||||
|
||||
def _is_outer_edge(self, edge: Edge, all_edges: List[Edge]) -> bool:
|
||||
"""
|
||||
Determine if an edge is "outer" (can wrap)
|
||||
|
||||
Rules:
|
||||
1. If edge has an adjacent opposite edge (within 50px tolerance AND overlapping range), it's NOT outer
|
||||
2. Otherwise, edge IS outer
|
||||
Note: This matches C++ EdgesAreAdjacent() logic
|
||||
"""
|
||||
opposite_type = self._get_opposite_edge_type(edge.edge_type)
|
||||
|
||||
# Find opposite edges that overlap in perpendicular range
|
||||
opposite_edges = [e for e in all_edges
|
||||
if e.edge_type == opposite_type
|
||||
and e.monitor_index != edge.monitor_index
|
||||
and self._ranges_overlap(edge.range_start, edge.range_end,
|
||||
e.range_start, e.range_end)]
|
||||
|
||||
if not opposite_edges:
|
||||
return True # No opposite edges = outer edge
|
||||
|
||||
# Check if any opposite edge is adjacent (within tolerance)
|
||||
for opp in opposite_edges:
|
||||
distance = abs(edge.position - opp.position)
|
||||
if distance <= self.ADJACENCY_TOLERANCE:
|
||||
return False # Adjacent edge found = not outer
|
||||
|
||||
return True # No adjacent edges = outer
|
||||
|
||||
@staticmethod
|
||||
def _get_opposite_edge_type(edge_type: EdgeType) -> EdgeType:
|
||||
"""Get the opposite edge type"""
|
||||
opposites = {
|
||||
EdgeType.LEFT: EdgeType.RIGHT,
|
||||
EdgeType.RIGHT: EdgeType.LEFT,
|
||||
EdgeType.TOP: EdgeType.BOTTOM,
|
||||
EdgeType.BOTTOM: EdgeType.TOP
|
||||
}
|
||||
return opposites[edge_type]
|
||||
|
||||
@staticmethod
|
||||
def _ranges_overlap(
|
||||
a_start: int,
|
||||
a_end: int,
|
||||
b_start: int,
|
||||
b_end: int) -> bool:
|
||||
"""Check if two 1D ranges overlap"""
|
||||
return not (a_end <= b_start or b_end <= a_start)
|
||||
|
||||
def calculate_wrap_position(self, x: int, y: int) -> Tuple[int, int]:
|
||||
"""Calculate where cursor should wrap to"""
|
||||
# Find which outer edge was crossed and calculate wrap
|
||||
# At corners, multiple edges may match - try all and return first successful wrap
|
||||
for edge in self.outer_edges:
|
||||
if self._is_on_edge(x, y, edge):
|
||||
new_x, new_y = self._wrap_from_edge(x, y, edge)
|
||||
if (new_x, new_y) != (x, y):
|
||||
# Wrap succeeded
|
||||
return (new_x, new_y)
|
||||
|
||||
return (x, y) # No wrap
|
||||
|
||||
def _is_on_edge(self, x: int, y: int, edge: Edge) -> bool:
|
||||
"""Check if point is on the given edge"""
|
||||
tolerance = 2 # Pixels
|
||||
|
||||
if edge.edge_type in (EdgeType.LEFT, EdgeType.RIGHT):
|
||||
return (abs(x - edge.position) <= tolerance and
|
||||
edge.range_start <= y <= edge.range_end)
|
||||
else:
|
||||
return (abs(y - edge.position) <= tolerance and
|
||||
edge.range_start <= x <= edge.range_end)
|
||||
|
||||
def _wrap_from_edge(self, x: int, y: int, edge: Edge) -> Tuple[int, int]:
|
||||
"""Calculate wrap destination from an outer edge"""
|
||||
opposite_type = self._get_opposite_edge_type(edge.edge_type)
|
||||
|
||||
# Find opposite outer edges that overlap
|
||||
opposite_edges = [e for e in self.outer_edges
|
||||
if e.edge_type == opposite_type
|
||||
and self._point_in_range(x, y, e)]
|
||||
|
||||
if not opposite_edges:
|
||||
return (x, y) # No wrap destination
|
||||
|
||||
# Find closest opposite edge
|
||||
target_edge = min(opposite_edges,
|
||||
key=lambda e: abs(e.position - edge.position))
|
||||
|
||||
# Calculate new position
|
||||
if edge.edge_type in (EdgeType.LEFT, EdgeType.RIGHT):
|
||||
return (target_edge.position, y)
|
||||
else:
|
||||
return (x, target_edge.position)
|
||||
|
||||
@staticmethod
|
||||
def _point_in_range(x: int, y: int, edge: Edge) -> bool:
|
||||
"""Check if point's perpendicular coordinate is in edge's range"""
|
||||
if edge.edge_type in (EdgeType.LEFT, EdgeType.RIGHT):
|
||||
return edge.range_start <= y <= edge.range_end
|
||||
else:
|
||||
return edge.range_start <= x <= edge.range_end
|
||||
|
||||
# ============================================================================
|
||||
# Test Configuration Generators
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestConfigGenerator:
|
||||
"""Generates comprehensive test configurations"""
|
||||
|
||||
# Common resolutions
|
||||
RESOLUTIONS = [
|
||||
(1920, 1080), # 1080p
|
||||
(2560, 1440), # 1440p
|
||||
(3840, 2160), # 4K
|
||||
(3440, 1440), # Ultrawide
|
||||
(1920, 1200), # 16:10
|
||||
]
|
||||
|
||||
# DPI scales
|
||||
DPI_SCALES = [96, 120, 144, 192] # 100%, 125%, 150%, 200%
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls, filepath: str) -> List[List[MonitorInfo]]:
|
||||
"""Load monitor configuration from captured JSON file"""
|
||||
# Handle UTF-8 with BOM (PowerShell default)
|
||||
with open(filepath, 'r', encoding='utf-8-sig') as f:
|
||||
data = json.load(f)
|
||||
|
||||
monitors = []
|
||||
for mon in data.get('monitors', []):
|
||||
monitor = MonitorInfo(
|
||||
left=mon['left'],
|
||||
top=mon['top'],
|
||||
right=mon['right'],
|
||||
bottom=mon['bottom'],
|
||||
dpi=mon.get('dpi', 96),
|
||||
primary=mon.get('primary', False)
|
||||
)
|
||||
monitors.append(monitor)
|
||||
|
||||
return [monitors] if monitors else []
|
||||
|
||||
@classmethod
|
||||
def generate_all_configs(cls,
|
||||
max_monitors: int = 4) -> List[List[MonitorInfo]]:
|
||||
"""Generate all test configurations"""
|
||||
configs = []
|
||||
|
||||
# Single monitor (baseline)
|
||||
configs.extend(cls._single_monitor_configs())
|
||||
|
||||
# Two monitors (most common)
|
||||
if max_monitors >= 2:
|
||||
configs.extend(cls._two_monitor_configs())
|
||||
|
||||
# Three monitors
|
||||
if max_monitors >= 3:
|
||||
configs.extend(cls._three_monitor_configs())
|
||||
|
||||
# Four monitors
|
||||
if max_monitors >= 4:
|
||||
configs.extend(cls._four_monitor_configs())
|
||||
|
||||
# Five+ monitors
|
||||
if max_monitors >= 5:
|
||||
configs.extend(cls._five_plus_monitor_configs(max_monitors))
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _single_monitor_configs(cls) -> List[List[MonitorInfo]]:
|
||||
"""Single monitor configurations"""
|
||||
configs = []
|
||||
|
||||
for width, height in cls.RESOLUTIONS[:3]: # Limit for single monitor
|
||||
for dpi in cls.DPI_SCALES[:2]: # Limit DPI variations
|
||||
mon = MonitorInfo(0, 0, width, height, dpi, True)
|
||||
configs.append([mon])
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _two_monitor_configs(cls) -> List[List[MonitorInfo]]:
|
||||
"""Two monitor configurations"""
|
||||
configs = []
|
||||
# Both 1080p for simplicity
|
||||
res1, res2 = cls.RESOLUTIONS[0], cls.RESOLUTIONS[0]
|
||||
|
||||
# Horizontal (touching)
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res1[0], res1[1], primary=True),
|
||||
MonitorInfo(res1[0], 0, res1[0] + res2[0], res2[1])
|
||||
])
|
||||
|
||||
# Vertical (touching)
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res1[0], res1[1], primary=True),
|
||||
MonitorInfo(0, res1[1], res2[0], res1[1] + res2[1])
|
||||
])
|
||||
|
||||
# Different resolutions
|
||||
res_big = cls.RESOLUTIONS[2] # 4K
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res1[0], res1[1], primary=True),
|
||||
MonitorInfo(res1[0], 0, res1[0] + res_big[0], res_big[1])
|
||||
])
|
||||
|
||||
# Offset alignment (common real-world scenario)
|
||||
offset = 200
|
||||
configs.append([
|
||||
MonitorInfo(0, offset, res1[0], offset + res1[1], primary=True),
|
||||
MonitorInfo(res1[0], 0, res1[0] + res2[0], res2[1])
|
||||
])
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _three_monitor_configs(cls) -> List[List[MonitorInfo]]:
|
||||
"""Three monitor configurations"""
|
||||
configs = []
|
||||
res = cls.RESOLUTIONS[0] # 1080p
|
||||
|
||||
# Linear horizontal
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(res[0], 0, res[0] * 2, res[1]),
|
||||
MonitorInfo(res[0] * 2, 0, res[0] * 3, res[1])
|
||||
])
|
||||
|
||||
# L-shape (common gaming setup)
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(res[0], 0, res[0] * 2, res[1]),
|
||||
MonitorInfo(0, res[1], res[0], res[1] * 2)
|
||||
])
|
||||
|
||||
# Vertical stack
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(0, res[1], res[0], res[1] * 2),
|
||||
MonitorInfo(0, res[1] * 2, res[0], res[1] * 3)
|
||||
])
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _four_monitor_configs(cls) -> List[List[MonitorInfo]]:
|
||||
"""Four monitor configurations"""
|
||||
configs = []
|
||||
res = cls.RESOLUTIONS[0] # 1080p
|
||||
|
||||
# 2x2 grid (classic)
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(res[0], 0, res[0] * 2, res[1]),
|
||||
MonitorInfo(0, res[1], res[0], res[1] * 2),
|
||||
MonitorInfo(res[0], res[1], res[0] * 2, res[1] * 2)
|
||||
])
|
||||
|
||||
# Linear horizontal
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(res[0], 0, res[0] * 2, res[1]),
|
||||
MonitorInfo(res[0] * 2, 0, res[0] * 3, res[1]),
|
||||
MonitorInfo(res[0] * 3, 0, res[0] * 4, res[1])
|
||||
])
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _five_plus_monitor_configs(cls, max_count: int) -> List[List[MonitorInfo]]:
|
||||
"""Five to ten monitor configurations"""
|
||||
configs = []
|
||||
res = cls.RESOLUTIONS[0] # 1080p
|
||||
|
||||
# Linear horizontal (5-10 monitors)
|
||||
for count in range(5, min(max_count + 1, 11)):
|
||||
monitor_list = []
|
||||
for i in range(count):
|
||||
is_primary = (i == 0)
|
||||
monitor_list.append(
|
||||
MonitorInfo(res[0] * i, 0, res[0] * (i + 1), res[1], primary=is_primary)
|
||||
)
|
||||
configs.append(monitor_list)
|
||||
|
||||
return configs
|
||||
|
||||
# ============================================================================
|
||||
# Test Validators
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class EdgeDetectionValidator:
|
||||
"""Validates edge detection logic"""
|
||||
|
||||
@staticmethod
|
||||
def validate_single_monitor(
|
||||
monitors: List[MonitorInfo]) -> Optional[TestFailure]:
|
||||
"""Single monitor should have 4 outer edges"""
|
||||
topology = MonitorTopology(monitors)
|
||||
expected_count = 4
|
||||
actual_count = len(topology.outer_edges)
|
||||
|
||||
if actual_count != expected_count:
|
||||
return TestFailure(
|
||||
test_name="single_monitor_edges",
|
||||
monitor_config=EdgeDetectionValidator._describe_config(
|
||||
monitors),
|
||||
expected=f"{expected_count} outer edges",
|
||||
actual=f"{actual_count} outer edges",
|
||||
details={"edges": [asdict(e) for e in topology.outer_edges]}
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_touching_monitors(
|
||||
monitors: List[MonitorInfo]) -> Optional[TestFailure]:
|
||||
"""Touching monitors should have no gap between them"""
|
||||
topology = MonitorTopology(monitors)
|
||||
|
||||
# For 2 touching monitors horizontally, expect 6 outer edges (not 8)
|
||||
if len(monitors) == 2:
|
||||
# Check if they're aligned horizontally and touching
|
||||
m1, m2 = monitors
|
||||
if m1.right == m2.left and m1.top == m2.top and m1.bottom == m2.bottom:
|
||||
expected = 6 # 2 internal edges removed
|
||||
actual = len(topology.outer_edges)
|
||||
if actual != expected:
|
||||
return TestFailure(
|
||||
test_name="touching_monitors",
|
||||
monitor_config=EdgeDetectionValidator._describe_config(
|
||||
monitors),
|
||||
expected=f"{expected} outer edges (2 touching edges removed)",
|
||||
actual=f"{actual} outer edges",
|
||||
details={"edges": [asdict(e)
|
||||
for e in topology.outer_edges]}
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_wrap_calculation(
|
||||
monitors: List[MonitorInfo]) -> List[TestFailure]:
|
||||
"""Validate cursor wrap calculations"""
|
||||
failures = []
|
||||
topology = MonitorTopology(monitors)
|
||||
|
||||
# Test wrapping at each outer edge with multiple points
|
||||
for edge in topology.outer_edges:
|
||||
test_points = EdgeDetectionValidator._get_test_points_on_edge(
|
||||
edge, monitors)
|
||||
|
||||
for test_point in test_points:
|
||||
x, y = test_point
|
||||
|
||||
# Check if there's actually a valid wrap destination
|
||||
# (some outer edges may not have opposite edges due to partial overlap)
|
||||
opposite_type = topology._get_opposite_edge_type(edge.edge_type)
|
||||
has_opposite = any(
|
||||
e.edge_type == opposite_type and
|
||||
topology._point_in_range(x, y, e)
|
||||
for e in topology.outer_edges
|
||||
)
|
||||
|
||||
if not has_opposite:
|
||||
# No wrap destination available - this is OK for partial overlaps
|
||||
continue
|
||||
|
||||
new_x, new_y = topology.calculate_wrap_position(x, y)
|
||||
|
||||
# Verify wrap happened (position changed)
|
||||
if (new_x, new_y) == (x, y):
|
||||
# Should have wrapped but didn't
|
||||
failure = TestFailure(
|
||||
test_name="wrap_calculation",
|
||||
monitor_config=EdgeDetectionValidator._describe_config(
|
||||
monitors),
|
||||
expected=f"Cursor should wrap from ({x},{y})",
|
||||
actual=f"No wrap occurred",
|
||||
details={
|
||||
"edge": asdict(edge),
|
||||
"test_point": (x, y)
|
||||
}
|
||||
)
|
||||
failures.append(failure)
|
||||
|
||||
return failures
|
||||
|
||||
@staticmethod
|
||||
def _get_test_points_on_edge(
|
||||
edge: Edge, monitors: List[MonitorInfo]) -> List[Tuple[int, int]]:
|
||||
"""Get multiple test points on the given edge (5 points: top/left corner, quarter, center, three-quarter, bottom/right corner)"""
|
||||
monitor = monitors[edge.monitor_index]
|
||||
points = []
|
||||
|
||||
if edge.edge_type == EdgeType.LEFT:
|
||||
x = monitor.left
|
||||
for ratio in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
||||
y = int(monitor.top + (monitor.height - 1) * ratio)
|
||||
points.append((x, y))
|
||||
elif edge.edge_type == EdgeType.RIGHT:
|
||||
x = monitor.right - 1
|
||||
for ratio in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
||||
y = int(monitor.top + (monitor.height - 1) * ratio)
|
||||
points.append((x, y))
|
||||
elif edge.edge_type == EdgeType.TOP:
|
||||
y = monitor.top
|
||||
for ratio in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
||||
x = int(monitor.left + (monitor.width - 1) * ratio)
|
||||
points.append((x, y))
|
||||
elif edge.edge_type == EdgeType.BOTTOM:
|
||||
y = monitor.bottom - 1
|
||||
for ratio in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
||||
x = int(monitor.left + (monitor.width - 1) * ratio)
|
||||
points.append((x, y))
|
||||
|
||||
return points
|
||||
|
||||
@staticmethod
|
||||
def _describe_config(monitors: List[MonitorInfo]) -> str:
|
||||
"""Generate human-readable config description"""
|
||||
if len(monitors) == 1:
|
||||
m = monitors[0]
|
||||
return f"Single {m.width}x{m.height} @{m.dpi}DPI"
|
||||
|
||||
desc = f"{len(monitors)} monitors: "
|
||||
for i, m in enumerate(monitors):
|
||||
desc += f"M{i}({m.width}x{m.height} at {m.left},{m.top}) "
|
||||
return desc.strip()
|
||||
|
||||
# ============================================================================
|
||||
# Test Runner
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestRunner:
|
||||
"""Orchestrates the test execution"""
|
||||
|
||||
def __init__(self, max_monitors: int = 10, verbose: bool = False, layout_file: str = None):
|
||||
self.max_monitors = max_monitors
|
||||
self.verbose = verbose
|
||||
self.layout_file = layout_file
|
||||
self.failures: List[TestFailure] = []
|
||||
self.test_count = 0
|
||||
self.passed_count = 0
|
||||
|
||||
def _print_layout_diagram(self, monitors: List[MonitorInfo]):
|
||||
"""Print a text-based diagram of the monitor layout"""
|
||||
print("\n" + "=" * 80)
|
||||
print("MONITOR LAYOUT DIAGRAM")
|
||||
print("=" * 80)
|
||||
|
||||
# Find bounds of entire desktop
|
||||
min_x = min(m.left for m in monitors)
|
||||
min_y = min(m.top for m in monitors)
|
||||
max_x = max(m.right for m in monitors)
|
||||
max_y = max(m.bottom for m in monitors)
|
||||
|
||||
# Calculate scale to fit in ~70 chars wide
|
||||
desktop_width = max_x - min_x
|
||||
desktop_height = max_y - min_y
|
||||
|
||||
# Scale factor: target 70 chars width
|
||||
scale = desktop_width / 70.0
|
||||
if scale < 1:
|
||||
scale = 1
|
||||
|
||||
# Create grid (70 chars wide, proportional height)
|
||||
grid_width = 70
|
||||
grid_height = max(10, int(desktop_height / scale))
|
||||
grid_height = min(grid_height, 30) # Cap at 30 lines
|
||||
|
||||
# Initialize grid with spaces
|
||||
grid = [[' ' for _ in range(grid_width)] for _ in range(grid_height)]
|
||||
|
||||
# Draw each monitor
|
||||
for idx, mon in enumerate(monitors):
|
||||
# Convert monitor coords to grid coords
|
||||
x1 = int((mon.left - min_x) / scale)
|
||||
y1 = int((mon.top - min_y) / scale)
|
||||
x2 = int((mon.right - min_x) / scale)
|
||||
y2 = int((mon.bottom - min_y) / scale)
|
||||
|
||||
# Clamp to grid
|
||||
x1 = max(0, min(x1, grid_width - 1))
|
||||
x2 = max(0, min(x2, grid_width))
|
||||
y1 = max(0, min(y1, grid_height - 1))
|
||||
y2 = max(0, min(y2, grid_height))
|
||||
|
||||
# Draw monitor border and fill
|
||||
char = str(idx) if idx < 10 else chr(65 + idx - 10) # 0-9, then A-Z
|
||||
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
if y < grid_height and x < grid_width:
|
||||
# Draw borders
|
||||
if y == y1 or y == y2 - 1:
|
||||
grid[y][x] = '─'
|
||||
elif x == x1 or x == x2 - 1:
|
||||
grid[y][x] = '│'
|
||||
else:
|
||||
grid[y][x] = char
|
||||
|
||||
# Draw corners
|
||||
if y1 < grid_height and x1 < grid_width:
|
||||
grid[y1][x1] = '┌'
|
||||
if y1 < grid_height and x2 - 1 < grid_width:
|
||||
grid[y1][x2 - 1] = '┐'
|
||||
if y2 - 1 < grid_height and x1 < grid_width:
|
||||
grid[y2 - 1][x1] = '└'
|
||||
if y2 - 1 < grid_height and x2 - 1 < grid_width:
|
||||
grid[y2 - 1][x2 - 1] = '┘'
|
||||
|
||||
# Print grid
|
||||
print()
|
||||
for row in grid:
|
||||
print(''.join(row))
|
||||
|
||||
# Print legend
|
||||
print("\n" + "-" * 80)
|
||||
print("MONITOR DETAILS:")
|
||||
print("-" * 80)
|
||||
for idx, mon in enumerate(monitors):
|
||||
char = str(idx) if idx < 10 else chr(65 + idx - 10)
|
||||
primary = " [PRIMARY]" if mon.primary else ""
|
||||
scaling = int((mon.dpi / 96.0) * 100)
|
||||
print(f" [{char}] Monitor {idx}{primary}")
|
||||
print(f" Position: ({mon.left}, {mon.top})")
|
||||
print(f" Size: {mon.width}x{mon.height}")
|
||||
print(f" DPI: {mon.dpi} ({scaling}% scaling)")
|
||||
print(f" Bounds: [{mon.left}, {mon.top}, {mon.right}, {mon.bottom}]")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
def run_all_tests(self):
|
||||
"""Execute all test configurations"""
|
||||
print("=" * 80)
|
||||
print("CursorWrap Monitor Layout Edge Detection Test Suite")
|
||||
print("=" * 80)
|
||||
|
||||
# Load or generate configs
|
||||
if self.layout_file:
|
||||
print(f"\nLoading monitor layout from {self.layout_file}...")
|
||||
configs = TestConfigGenerator.load_from_file(self.layout_file)
|
||||
# Show visual diagram for captured layouts
|
||||
if configs:
|
||||
self._print_layout_diagram(configs[0])
|
||||
else:
|
||||
print("\nGenerating test configurations...")
|
||||
configs = TestConfigGenerator.generate_all_configs(self.max_monitors)
|
||||
|
||||
total_tests = len(configs)
|
||||
print(f"Testing {total_tests} configuration(s)")
|
||||
print("=" * 80)
|
||||
|
||||
# Run tests
|
||||
for i, config in enumerate(configs, 1):
|
||||
self._run_test_config(config, i, total_tests)
|
||||
|
||||
# Report results
|
||||
self._print_summary()
|
||||
self._save_report()
|
||||
|
||||
def _run_test_config(
|
||||
self,
|
||||
monitors: List[MonitorInfo],
|
||||
iteration: int,
|
||||
total: int):
|
||||
"""Run all validators on a single configuration"""
|
||||
desc = EdgeDetectionValidator._describe_config(monitors)
|
||||
|
||||
if not self.verbose:
|
||||
# Minimal output: just progress
|
||||
progress = (iteration / total) * 100
|
||||
print(
|
||||
f"\r[{iteration}/{total}] {progress:5.1f}% - Testing: {desc[:60]:<60}", end="", flush=True)
|
||||
else:
|
||||
print(f"\n[{iteration}/{total}] Testing: {desc}")
|
||||
|
||||
# Run validators
|
||||
self.test_count += 1
|
||||
config_passed = True
|
||||
|
||||
# Single monitor validation
|
||||
if len(monitors) == 1:
|
||||
failure = EdgeDetectionValidator.validate_single_monitor(monitors)
|
||||
if failure:
|
||||
self.failures.append(failure)
|
||||
config_passed = False
|
||||
|
||||
# Touching monitors validation (2+ monitors)
|
||||
if len(monitors) >= 2:
|
||||
failure = EdgeDetectionValidator.validate_touching_monitors(monitors)
|
||||
if failure:
|
||||
self.failures.append(failure)
|
||||
config_passed = False
|
||||
|
||||
# Wrap calculation validation
|
||||
wrap_failures = EdgeDetectionValidator.validate_wrap_calculation(monitors)
|
||||
if wrap_failures:
|
||||
self.failures.extend(wrap_failures)
|
||||
config_passed = False
|
||||
|
||||
if config_passed:
|
||||
self.passed_count += 1
|
||||
|
||||
if self.verbose and not config_passed:
|
||||
print(f" ? FAILED ({len([f for f in self.failures if desc in f.monitor_config])} issues)")
|
||||
elif self.verbose:
|
||||
print(" ? PASSED")
|
||||
|
||||
def _print_summary(self):
|
||||
"""Print test summary"""
|
||||
print("\n\n" + "=" * 80)
|
||||
print("TEST SUMMARY")
|
||||
print("=" * 80)
|
||||
print(f"Total Configurations: {self.test_count}")
|
||||
print(f"Passed: {self.passed_count} ({self.passed_count/self.test_count*100:.1f}%)")
|
||||
print(f"Failed: {self.test_count - self.passed_count} ({(self.test_count - self.passed_count)/self.test_count*100:.1f}%)")
|
||||
print(f"Total Issues Found: {len(self.failures)}")
|
||||
print("=" * 80)
|
||||
|
||||
if self.failures:
|
||||
print("\n?? FAILURES DETECTED - See test_report.json for details")
|
||||
print("\nTop 5 Failure Types:")
|
||||
failure_types = {}
|
||||
for f in self.failures:
|
||||
failure_types[f.test_name] = failure_types.get(f.test_name, 0) + 1
|
||||
|
||||
for test_name, count in sorted(failure_types.items(), key=lambda x: x[1], reverse=True)[:5]:
|
||||
print(f" - {test_name}: {count} failures")
|
||||
else:
|
||||
print("\n? ALL TESTS PASSED!")
|
||||
|
||||
def _save_report(self):
|
||||
"""Save detailed JSON report"""
|
||||
|
||||
# Helper to convert enums to strings
|
||||
def convert_for_json(obj):
|
||||
if isinstance(obj, dict):
|
||||
return {k: convert_for_json(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [convert_for_json(item) for item in obj]
|
||||
elif isinstance(obj, Enum):
|
||||
return obj.value
|
||||
else:
|
||||
return obj
|
||||
|
||||
report = {
|
||||
"summary": {
|
||||
"total_configs": self.test_count,
|
||||
"passed": self.passed_count,
|
||||
"failed": self.test_count - self.passed_count,
|
||||
"total_issues": len(self.failures),
|
||||
"pass_rate": f"{self.passed_count/self.test_count*100:.2f}%"
|
||||
},
|
||||
"failures": convert_for_json([asdict(f) for f in self.failures]),
|
||||
"recommendations": self._generate_recommendations()
|
||||
}
|
||||
|
||||
output_file = "test_report.json"
|
||||
with open(output_file, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
print(f"\n?? Detailed report saved to: {output_file}")
|
||||
|
||||
def _generate_recommendations(self) -> List[str]:
|
||||
"""Generate recommendations based on failures"""
|
||||
recommendations = []
|
||||
|
||||
failure_types = {}
|
||||
for f in self.failures:
|
||||
failure_types[f.test_name] = failure_types.get(f.test_name, 0) + 1
|
||||
|
||||
if "single_monitor_edges" in failure_types:
|
||||
recommendations.append(
|
||||
"Single monitor edge detection failing - verify baseline case in MonitorTopology::_detect_outer_edges()"
|
||||
)
|
||||
|
||||
if "touching_monitors" in failure_types:
|
||||
recommendations.append(
|
||||
f"Adjacent monitor detection failing ({failure_types['touching_monitors']} cases) - "
|
||||
"review ADJACENCY_TOLERANCE (50px) and edge overlap logic in EdgesAreAdjacent()"
|
||||
)
|
||||
|
||||
if "wrap_calculation" in failure_types:
|
||||
recommendations.append(
|
||||
f"Wrap calculation failing ({failure_types['wrap_calculation']} cases) - "
|
||||
"review CursorWrapCore::HandleMouseMove() wrap destination logic"
|
||||
)
|
||||
|
||||
if not recommendations:
|
||||
recommendations.append("All tests passed - edge detection logic is working correctly!")
|
||||
|
||||
return recommendations
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point
|
||||
# ============================================================================
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="CursorWrap Monitor Layout Edge Detection Test Suite"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-monitors",
|
||||
type=int,
|
||||
default=10,
|
||||
help="Maximum number of monitors to test (1-10)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Enable verbose output"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--layout-file",
|
||||
type=str,
|
||||
help="Use captured monitor layout JSON file instead of generated configs"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.layout_file:
|
||||
# Validate max_monitors only for generated configs
|
||||
if args.max_monitors < 1 or args.max_monitors > 10:
|
||||
print("Error: max-monitors must be between 1 and 10")
|
||||
sys.exit(1)
|
||||
|
||||
runner = TestRunner(
|
||||
max_monitors=args.max_monitors,
|
||||
verbose=args.verbose,
|
||||
layout_file=args.layout_file
|
||||
)
|
||||
runner.run_all_tests()
|
||||
|
||||
# Exit with error code if tests failed
|
||||
sys.exit(0 if not runner.failures else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -506,58 +506,8 @@ public:
|
||||
return CallNextHookEx(nullptr, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
// Helper method to check if there's a monitor adjacent in coordinate space (not grid)
|
||||
bool HasAdjacentMonitorInCoordinateSpace(const RECT& currentMonitorRect, int direction)
|
||||
{
|
||||
// direction: 0=left, 1=right, 2=top, 3=bottom
|
||||
const int tolerance = 50; // Allow small gaps
|
||||
|
||||
for (const auto& monitor : m_monitors)
|
||||
{
|
||||
bool isAdjacent = false;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case 0: // Left - check if another monitor's right edge touches/overlaps our left edge
|
||||
isAdjacent = (abs(monitor.rect.right - currentMonitorRect.left) <= tolerance) &&
|
||||
(monitor.rect.bottom > currentMonitorRect.top + tolerance) &&
|
||||
(monitor.rect.top < currentMonitorRect.bottom - tolerance);
|
||||
break;
|
||||
|
||||
case 1: // Right - check if another monitor's left edge touches/overlaps our right edge
|
||||
isAdjacent = (abs(monitor.rect.left - currentMonitorRect.right) <= tolerance) &&
|
||||
(monitor.rect.bottom > currentMonitorRect.top + tolerance) &&
|
||||
(monitor.rect.top < currentMonitorRect.bottom - tolerance);
|
||||
break;
|
||||
|
||||
case 2: // Top - check if another monitor's bottom edge touches/overlaps our top edge
|
||||
isAdjacent = (abs(monitor.rect.bottom - currentMonitorRect.top) <= tolerance) &&
|
||||
(monitor.rect.right > currentMonitorRect.left + tolerance) &&
|
||||
(monitor.rect.left < currentMonitorRect.right - tolerance);
|
||||
break;
|
||||
|
||||
case 3: // Bottom - check if another monitor's top edge touches/overlaps our bottom edge
|
||||
isAdjacent = (abs(monitor.rect.top - currentMonitorRect.bottom) <= tolerance) &&
|
||||
(monitor.rect.right > currentMonitorRect.left + tolerance) &&
|
||||
(monitor.rect.left < currentMonitorRect.right - tolerance);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isAdjacent)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: Found adjacent monitor in coordinate space (direction {})", direction);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// *** COMPLETELY REWRITTEN CURSOR WRAPPING LOGIC ***
|
||||
// Implements vertical scrolling to bottom/top of vertical stack as requested
|
||||
// Only wraps when there's NO adjacent monitor in the coordinate space
|
||||
POINT HandleMouseMove(const POINT& currentPos)
|
||||
{
|
||||
POINT newPos = currentPos;
|
||||
@@ -596,22 +546,12 @@ public:
|
||||
|
||||
// *** VERTICAL WRAPPING LOGIC - CONFIRMED WORKING ***
|
||||
// Move to bottom of vertical stack when hitting top edge
|
||||
// Only wrap if there's NO adjacent monitor in the coordinate space
|
||||
if (currentPos.y <= currentMonitorInfo.rcMonitor.top)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: TOP EDGE DETECTED =======");
|
||||
#endif
|
||||
|
||||
// Check if there's an adjacent monitor above in coordinate space
|
||||
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 2))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists above (Windows will handle)");
|
||||
#endif
|
||||
return currentPos; // Let Windows handle natural cursor movement
|
||||
}
|
||||
|
||||
// Find the bottom-most monitor in the vertical stack (same column)
|
||||
HMONITOR bottomMonitor = nullptr;
|
||||
|
||||
@@ -664,15 +604,6 @@ public:
|
||||
Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: BOTTOM EDGE DETECTED =======");
|
||||
#endif
|
||||
|
||||
// Check if there's an adjacent monitor below in coordinate space
|
||||
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 3))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists below (Windows will handle)");
|
||||
#endif
|
||||
return currentPos; // Let Windows handle natural cursor movement
|
||||
}
|
||||
|
||||
// Find the top-most monitor in the vertical stack (same column)
|
||||
HMONITOR topMonitor = nullptr;
|
||||
|
||||
@@ -722,22 +653,13 @@ public:
|
||||
|
||||
// *** FIXED HORIZONTAL WRAPPING LOGIC ***
|
||||
// Move to opposite end of horizontal stack when hitting left/right edge
|
||||
// Only wrap if there's NO adjacent monitor in the coordinate space (let Windows handle natural transitions)
|
||||
// Only handle horizontal wrapping if we haven't already wrapped vertically
|
||||
if (!wrapped && currentPos.x <= currentMonitorInfo.rcMonitor.left)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: LEFT EDGE DETECTED =======");
|
||||
#endif
|
||||
|
||||
// Check if there's an adjacent monitor to the left in coordinate space
|
||||
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 0))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists to the left (Windows will handle)");
|
||||
#endif
|
||||
return currentPos; // Let Windows handle natural cursor movement
|
||||
}
|
||||
|
||||
// Find the right-most monitor in the horizontal stack (same row)
|
||||
HMONITOR rightMonitor = nullptr;
|
||||
|
||||
@@ -790,15 +712,6 @@ public:
|
||||
Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: RIGHT EDGE DETECTED =======");
|
||||
#endif
|
||||
|
||||
// Check if there's an adjacent monitor to the right in coordinate space
|
||||
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 1))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists to the right (Windows will handle)");
|
||||
#endif
|
||||
return currentPos; // Let Windows handle natural cursor movement
|
||||
}
|
||||
|
||||
// Find the left-most monitor in the horizontal stack (same row)
|
||||
HMONITOR leftMonitor = nullptr;
|
||||
|
||||
@@ -1068,104 +981,45 @@ void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
|
||||
}
|
||||
else
|
||||
{
|
||||
// For more than 2 monitors, use edge-based alignment algorithm
|
||||
// This ensures monitors with aligned edges (e.g., top edges at same Y) are grouped in same row
|
||||
|
||||
// Helper lambda to check if two ranges overlap or are adjacent (with tolerance)
|
||||
auto rangesOverlapOrTouch = [](int start1, int end1, int start2, int end2, int tolerance = 50) -> bool {
|
||||
// Check if ranges overlap or are within tolerance distance
|
||||
return (start1 <= end2 + tolerance) && (start2 <= end1 + tolerance);
|
||||
};
|
||||
|
||||
// Sort monitors by horizontal position (left edge) for column assignment
|
||||
std::vector<const MonitorInfo*> monitorsByX;
|
||||
for (const auto& monitor : monitors) {
|
||||
monitorsByX.push_back(&monitor);
|
||||
}
|
||||
std::sort(monitorsByX.begin(), monitorsByX.end(), [](const MonitorInfo* a, const MonitorInfo* b) {
|
||||
return a->rect.left < b->rect.left;
|
||||
});
|
||||
|
||||
// Sort monitors by vertical position (top edge) for row assignment
|
||||
std::vector<const MonitorInfo*> monitorsByY;
|
||||
for (const auto& monitor : monitors) {
|
||||
monitorsByY.push_back(&monitor);
|
||||
}
|
||||
std::sort(monitorsByY.begin(), monitorsByY.end(), [](const MonitorInfo* a, const MonitorInfo* b) {
|
||||
return a->rect.top < b->rect.top;
|
||||
});
|
||||
|
||||
// Assign rows based on vertical overlap - monitors that overlap vertically should be in same row
|
||||
std::map<const MonitorInfo*, int> monitorToRow;
|
||||
int currentRow = 0;
|
||||
|
||||
for (size_t i = 0; i < monitorsByY.size(); i++) {
|
||||
const auto* monitor = monitorsByY[i];
|
||||
|
||||
// Check if this monitor overlaps vertically with any monitor already assigned to current row
|
||||
bool foundOverlap = false;
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
const auto* other = monitorsByY[j];
|
||||
if (monitorToRow[other] == currentRow) {
|
||||
// Check vertical overlap
|
||||
if (rangesOverlapOrTouch(monitor->rect.top, monitor->rect.bottom,
|
||||
other->rect.top, other->rect.bottom)) {
|
||||
monitorToRow[monitor] = currentRow;
|
||||
foundOverlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundOverlap) {
|
||||
// Start new row if no overlap found and we have room
|
||||
if (currentRow < 2 && i < monitorsByY.size() - 1) {
|
||||
currentRow++;
|
||||
}
|
||||
monitorToRow[monitor] = currentRow;
|
||||
}
|
||||
// For more than 2 monitors, use the general algorithm
|
||||
RECT totalBounds = monitors[0].rect;
|
||||
for (const auto& monitor : monitors)
|
||||
{
|
||||
totalBounds.left = min(totalBounds.left, monitor.rect.left);
|
||||
totalBounds.top = min(totalBounds.top, monitor.rect.top);
|
||||
totalBounds.right = max(totalBounds.right, monitor.rect.right);
|
||||
totalBounds.bottom = max(totalBounds.bottom, monitor.rect.bottom);
|
||||
}
|
||||
|
||||
// Assign columns based on horizontal position (left-to-right order)
|
||||
// Monitors are already sorted by X coordinate (left edge)
|
||||
std::map<const MonitorInfo*, int> monitorToCol;
|
||||
int totalWidth = totalBounds.right - totalBounds.left;
|
||||
int totalHeight = totalBounds.bottom - totalBounds.top;
|
||||
int gridWidth = max(1, totalWidth / 3);
|
||||
int gridHeight = max(1, totalHeight / 3);
|
||||
|
||||
// For horizontal arrangement, distribute monitors evenly across columns
|
||||
if (monitorsByX.size() == 1) {
|
||||
// Single monitor - place in middle column
|
||||
monitorToCol[monitorsByX[0]] = 1;
|
||||
}
|
||||
else if (monitorsByX.size() == 2) {
|
||||
// Two monitors - place at opposite ends for wrapping
|
||||
monitorToCol[monitorsByX[0]] = 0; // Leftmost monitor
|
||||
monitorToCol[monitorsByX[1]] = 2; // Rightmost monitor
|
||||
}
|
||||
else {
|
||||
// Three or more monitors - distribute across grid
|
||||
for (size_t i = 0; i < monitorsByX.size() && i < 3; i++) {
|
||||
monitorToCol[monitorsByX[i]] = static_cast<int>(i);
|
||||
}
|
||||
// If more than 3 monitors, place extras in rightmost column
|
||||
for (size_t i = 3; i < monitorsByX.size(); i++) {
|
||||
monitorToCol[monitorsByX[i]] = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Place monitors in grid using the computed row/column assignments
|
||||
// Place monitors in the 3x3 grid based on their center points
|
||||
for (const auto& monitor : monitors)
|
||||
{
|
||||
HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
||||
int row = monitorToRow[&monitor];
|
||||
int col = monitorToCol[&monitor];
|
||||
|
||||
// Calculate center point of monitor
|
||||
int centerX = (monitor.rect.left + monitor.rect.right) / 2;
|
||||
int centerY = (monitor.rect.top + monitor.rect.bottom) / 2;
|
||||
|
||||
// Map to grid position
|
||||
int col = (centerX - totalBounds.left) / gridWidth;
|
||||
int row = (centerY - totalBounds.top) / gridHeight;
|
||||
|
||||
// Ensure we stay within bounds
|
||||
col = max(0, min(2, col));
|
||||
row = max(0, min(2, row));
|
||||
|
||||
grid[row][col] = hMonitor;
|
||||
monitorToPosition[hMonitor] = {row, col, true};
|
||||
positionToMonitor[{row, col}] = hMonitor;
|
||||
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: Monitor {} placed at grid[{}][{}] (left={}, top={}, right={}, bottom={})",
|
||||
monitor.monitorId, row, col,
|
||||
monitor.rect.left, monitor.rect.top, monitor.rect.right, monitor.rect.bottom);
|
||||
Logger::info(L"CursorWrap DEBUG: Monitor {} placed at grid[{}][{}], center=({}, {})",
|
||||
monitor.monitorId, row, col, centerX, centerY);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { },
|
||||
new CommandItem(_newExtension) { Title = _newExtension.Title },
|
||||
new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle },
|
||||
];
|
||||
|
||||
public override IFallbackCommandItem[] FallbackCommands() =>
|
||||
|
||||
@@ -134,9 +134,6 @@
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
PreviewKeyDown="CommandsDropdown_PreviewKeyDown"
|
||||
SelectionMode="Single">
|
||||
<ListView.Resources>
|
||||
<x:Boolean x:Key="ListViewItemSelectionIndicatorVisualEnabled">False</x:Boolean>
|
||||
</ListView.Resources>
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
|
||||
@@ -99,6 +99,8 @@ public sealed partial class WrapPanel : Panel
|
||||
set { SetValue(HorizontalSpacingProperty, value); }
|
||||
}
|
||||
|
||||
private bool IsSectionItem(UIElement element) => element is FrameworkElement fe && fe.DataContext is ListItemViewModel item && item.IsSectionOrSeparator;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
|
||||
/// </summary>
|
||||
@@ -348,7 +350,7 @@ public sealed partial class WrapPanel : Panel
|
||||
return;
|
||||
}
|
||||
|
||||
var isFullLine = GetIsFullLine(child);
|
||||
var isFullLine = IsSectionItem(child);
|
||||
var desiredMeasure = new UvMeasure(Orientation, child.DesiredSize);
|
||||
|
||||
if (isFullLine)
|
||||
|
||||
@@ -18,19 +18,8 @@ internal sealed partial class GridItemContainerStyleSelector : StyleSelector
|
||||
|
||||
public Style? Gallery { get; set; }
|
||||
|
||||
public Style? Section { get; set; }
|
||||
|
||||
public Style? Separator { get; set; }
|
||||
|
||||
protected override Style? SelectStyleCore(object item, DependencyObject container)
|
||||
{
|
||||
if (item is ListItemViewModel { IsSectionOrSeparator: true } listItem)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(listItem.Title)
|
||||
? Separator!
|
||||
: Section;
|
||||
}
|
||||
|
||||
return GridProperties switch
|
||||
{
|
||||
SmallGridPropertiesViewModel => Small,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
internal sealed partial class ListItemContainerStyleSelector : StyleSelector
|
||||
{
|
||||
public Style? Default { get; set; }
|
||||
|
||||
public Style? Section { get; set; }
|
||||
|
||||
public Style? Separator { get; set; }
|
||||
|
||||
protected override Style? SelectStyleCore(object item, DependencyObject container)
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
ListItemViewModel { IsSectionOrSeparator: true } listItemViewModel when string.IsNullOrWhiteSpace(listItemViewModel.Title) => Separator!,
|
||||
ListItemViewModel { IsSectionOrSeparator: true } => Section,
|
||||
_ => Default,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,6 @@
|
||||
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
|
||||
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
||||
|
||||
<x:Double x:Key="ListViewItemMinHeight">40</x:Double>
|
||||
<x:Double x:Key="ListViewSectionMinHeight">0</x:Double>
|
||||
<x:Double x:Key="ListViewSeparatorMinHeight">0</x:Double>
|
||||
|
||||
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
@@ -98,7 +94,6 @@
|
||||
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
@@ -160,70 +155,6 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="GridViewSectionItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="Padding" Value="4,8,12,0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Bottom" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSectionMinHeight}" />
|
||||
<Setter Property="cpcontrols:WrapPanel.IsFullLine" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="GridViewSeparatorItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="Padding" Value="4,4,12,4" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSeparatorMinHeight}" />
|
||||
<Setter Property="cpcontrols:WrapPanel.IsFullLine" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ListDefaultContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewItemMinHeight}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ListSectionContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="Padding" Value="16,8,12,0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Bottom" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSectionMinHeight}" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ListSeparatorContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="Padding" Value="16,4,12,4" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSeparatorMinHeight}" />
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
|
||||
<cpcontrols:Tag
|
||||
AutomationProperties.Name="{x:Bind Text, Mode=OneWay}"
|
||||
@@ -235,19 +166,6 @@
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<cmdpalUI:ListItemTemplateSelector
|
||||
x:Key="ListItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
ListItem="{StaticResource ListItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:ListItemContainerStyleSelector
|
||||
x:Key="ListItemContainerStyleSelector"
|
||||
Default="{StaticResource ListDefaultContainerStyle}"
|
||||
Section="{StaticResource ListSectionContainerStyle}"
|
||||
Separator="{StaticResource ListSeparatorContainerStyle}" />
|
||||
|
||||
<cmdpalUI:GridItemTemplateSelector
|
||||
x:Key="GridItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
@@ -255,16 +173,21 @@
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource GridSeparatorViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}"
|
||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:ListItemTemplateSelector
|
||||
x:Key="ListItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
ListItem="{StaticResource ListItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:GridItemContainerStyleSelector
|
||||
x:Key="GridItemContainerStyleSelector"
|
||||
Gallery="{StaticResource GalleryGridViewItemStyle}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource IconGridViewItemStyle}"
|
||||
Section="{StaticResource GridViewSectionItemStyle}"
|
||||
Separator="{StaticResource GridViewSeparatorItemStyle}"
|
||||
Small="{StaticResource IconGridViewItemStyle}" />
|
||||
|
||||
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
|
||||
@@ -332,21 +255,21 @@
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Margin="0,2,0,2"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSectionViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
Margin="0,8,0,0"
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
cpcontrols:WrapPanel.IsFullLine="True"
|
||||
ColumnSpacing="8"
|
||||
@@ -358,9 +281,13 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Foreground="{ThemeResource TextFillColorDisabled}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Section}" />
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -487,7 +414,7 @@
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="11"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
@@ -496,10 +423,6 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GridSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Rectangle Height="1" Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
@@ -525,7 +448,6 @@
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemContainerStyleSelector="{StaticResource ListItemContainerStyleSelector}"
|
||||
ItemTemplateSelector="{StaticResource ListItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
@@ -538,7 +460,7 @@
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="16,16"
|
||||
Padding="16,0"
|
||||
CanDragItems="True"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
@@ -555,10 +477,7 @@
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<GridView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cpcontrols:WrapPanel
|
||||
HorizontalSpacing="8"
|
||||
Orientation="Horizontal"
|
||||
VerticalSpacing="8" />
|
||||
<cpcontrols:WrapPanel HorizontalSpacing="8" Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</GridView.ItemsPanel>
|
||||
<GridView.ItemContainerTransitions>
|
||||
|
||||
@@ -18,9 +18,6 @@ internal static class BindTransformers
|
||||
public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
|
||||
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
public static Visibility EmptyOrWhitespaceToVisible(string? input)
|
||||
=> string.IsNullOrWhiteSpace(input) ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public static Visibility VisibleWhenAny(bool value1, bool value2)
|
||||
=> (value1 || value2) ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
@@ -126,16 +126,16 @@
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="DetailsSeparatorTemplate" x:DataType="coreViewModels:DetailsSeparatorViewModel">
|
||||
<StackPanel Margin="0,8,8,0" Orientation="Vertical">
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
Style="{StaticResource SeparatorKeyTextBlockStyle}"
|
||||
Text="{x:Bind Key, Mode=OneWay}"
|
||||
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToCollapsed(Key), Mode=OneWay, FallbackValue=Collapsed}" />
|
||||
<Border
|
||||
Margin="0,0,0,0"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToVisible(Key), Mode=OneWay, FallbackValue=Collapsed}" />
|
||||
BorderThickness="0,0,0,2">
|
||||
<TextBlock
|
||||
Margin="0,0,0,0"
|
||||
Style="{StaticResource SeparatorKeyTextBlockStyle}"
|
||||
Text="{x:Bind Key, Mode=OneWay}"
|
||||
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToCollapsed(Key), FallbackValue=Collapsed}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="DetailsTagsTemplate" x:DataType="coreViewModels:DetailsTagsViewModel">
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -34,56 +30,8 @@ public sealed partial class GeneralPage : Page
|
||||
{
|
||||
get
|
||||
{
|
||||
var versionNo = ResourceLoaderInstance.GetString("Settings_GeneralPage_VersionNo");
|
||||
if (!TryGetPackagedVersion(out var version) && !TryGetAssemblyVersion(out version))
|
||||
{
|
||||
version = "?";
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.CurrentCulture, versionNo, version);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetPackagedVersion(out string version)
|
||||
{
|
||||
version = string.Empty;
|
||||
try
|
||||
{
|
||||
// Package.Current throws InvalidOperationException if the app is not packaged
|
||||
var v = Package.Current.Id.Version;
|
||||
version = $"{v.Major}.{v.Minor}.{v.Build}.{v.Revision}";
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get version from the package", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetAssemblyVersion(out string version)
|
||||
{
|
||||
version = string.Empty;
|
||||
try
|
||||
{
|
||||
var processPath = Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(processPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var info = FileVersionInfo.GetVersionInfo(processPath);
|
||||
version = $"{info.FileMajorPart}.{info.FileMinorPart}.{info.FileBuildPart}.{info.FilePrivatePart}";
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get version from the executable", ex);
|
||||
return false;
|
||||
var version = Package.Current.Id.Version;
|
||||
return $"Version {version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,7 +724,4 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_ExtensionsPage_More_ReorderFallbacks_MenuFlyoutItem.Text" xml:space="preserve">
|
||||
<value>Manage fallback order</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
|
||||
<value>Version {0}</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -90,5 +90,20 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
|
||||
// Assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(displayName));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetTranslatedPluginDescriptionTest()
|
||||
{
|
||||
// Setup
|
||||
var provider = new TimeDateCommandsProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
var subtitle = commands[0].Subtitle;
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(subtitle));
|
||||
Assert.IsTrue(subtitle.Contains("Show time and date values in different formats"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
||||
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
||||
<VersionMajor>0</VersionMajor>
|
||||
<VersionMinor>8</VersionMinor>
|
||||
<VersionMinor>7</VersionMinor>
|
||||
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -15,6 +15,7 @@ public partial class CalculatorCommandProvider : CommandProvider
|
||||
private static ISettingsInterface settings = new SettingsManager();
|
||||
private readonly ListItem _listItem = new(new CalculatorListPage(settings))
|
||||
{
|
||||
Subtitle = Resources.calculator_top_level_subtitle,
|
||||
MoreCommands = [new CommandContextItem(((SettingsManager)settings).Settings.SettingsPage)],
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
|
||||
{
|
||||
Title = Properties.Resources.list_item_title,
|
||||
Subtitle = Properties.Resources.list_item_subtitle,
|
||||
Icon = Icons.ClipboardListIcon,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(_settingsManager.Settings.SettingsPage),
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command to preview a file using PowerToys Peek.
|
||||
/// </summary>
|
||||
public sealed partial class PeekFileCommand : InvokableCommand
|
||||
{
|
||||
private const string PeekExecutable = @"WinUI3Apps\PowerToys.Peek.UI.exe";
|
||||
|
||||
private static readonly Lazy<string> _peekPath = new(GetPeekExecutablePath);
|
||||
|
||||
private readonly string _fullPath;
|
||||
|
||||
public PeekFileCommand(string fullPath)
|
||||
{
|
||||
_fullPath = fullPath;
|
||||
Name = Resources.Indexer_Command_Peek;
|
||||
Icon = Icons.PeekIcon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Peek is available on this system.
|
||||
/// </summary>
|
||||
public static bool IsPeekAvailable => !string.IsNullOrEmpty(_peekPath.Value);
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
var peekExe = _peekPath.Value;
|
||||
if (string.IsNullOrEmpty(peekExe))
|
||||
{
|
||||
return CommandResult.ShowToast(Resources.Indexer_Command_Peek_NotAvailable);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo.FileName = peekExe;
|
||||
process.StartInfo.Arguments = $"\"{_fullPath}\"";
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExtensionHost.LogMessage($"Unable to launch Peek for {_fullPath}\n{ex}");
|
||||
return CommandResult.ShowToast(Resources.Indexer_Command_Peek_Failed);
|
||||
}
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
|
||||
private static string GetPeekExecutablePath()
|
||||
{
|
||||
var installPath = PowerToysPathResolver.GetPowerToysInstallPath();
|
||||
if (string.IsNullOrEmpty(installPath))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var peekPath = Path.Combine(installPath, PeekExecutable);
|
||||
return File.Exists(peekPath) ? peekPath : string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Core.Common.Commands;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Pages;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
@@ -97,13 +96,6 @@ internal sealed partial class IndexerListItem : ListItem
|
||||
}
|
||||
|
||||
commands.Add(new CommandContextItem(new OpenWithCommand(fullPath)));
|
||||
|
||||
// Add Peek command if available (only for files, not directories)
|
||||
if (!isDir && PeekFileCommand.IsPeekAvailable)
|
||||
{
|
||||
commands.Add(new CommandContextItem(new PeekFileCommand(fullPath)) { RequestedShortcut = KeyChords.Peek });
|
||||
}
|
||||
|
||||
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }) { RequestedShortcut = KeyChords.OpenFileLocation });
|
||||
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }) { RequestedShortcut = KeyChords.CopyFilePath });
|
||||
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)) { RequestedShortcut = KeyChords.OpenInConsole });
|
||||
|
||||
@@ -23,6 +23,4 @@ internal static class Icons
|
||||
internal static IconInfo FilesIcon { get; } = new("\uF571"); // PrintAllPages
|
||||
|
||||
internal static IconInfo FilterIcon { get; } = new("\uE71C"); // Filter
|
||||
|
||||
internal static IconInfo PeekIcon { get; } = IconHelpers.FromRelativePath("Assets\\Peek.png");
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
@@ -16,6 +14,4 @@ internal static class KeyChords
|
||||
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
|
||||
|
||||
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
|
||||
|
||||
internal static KeyChord Peek { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: (int)VirtualKey.Space);
|
||||
}
|
||||
|
||||
@@ -42,9 +42,6 @@
|
||||
<Content Update="Assets\FileExplorer.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\Peek.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -132,33 +132,6 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Peek preview.
|
||||
/// </summary>
|
||||
internal static string Indexer_Command_Peek {
|
||||
get {
|
||||
return ResourceManager.GetString("Indexer_Command_Peek", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to launch Peek.
|
||||
/// </summary>
|
||||
internal static string Indexer_Command_Peek_Failed {
|
||||
get {
|
||||
return ResourceManager.GetString("Indexer_Command_Peek_Failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to PowerToys Peek is not available.
|
||||
/// </summary>
|
||||
internal static string Indexer_Command_Peek_NotAvailable {
|
||||
get {
|
||||
return ResourceManager.GetString("Indexer_Command_Peek_NotAvailable", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Search all files.
|
||||
/// </summary>
|
||||
|
||||
@@ -202,13 +202,4 @@ You can try searching all files on this PC or adjust your indexing settings.</va
|
||||
<data name="Indexer_Filter_Files_Only" xml:space="preserve">
|
||||
<value>Files</value>
|
||||
</data>
|
||||
<data name="Indexer_Command_Peek" xml:space="preserve">
|
||||
<value>Peek preview</value>
|
||||
</data>
|
||||
<data name="Indexer_Command_Peek_NotAvailable" xml:space="preserve">
|
||||
<value>PowerToys Peek is not available</value>
|
||||
</data>
|
||||
<data name="Indexer_Command_Peek_Failed" xml:space="preserve">
|
||||
<value>Failed to launch Peek</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -31,6 +31,7 @@ public partial class RemoteDesktopCommandProvider : CommandProvider
|
||||
|
||||
listPageCommand = new CommandItem(listPage)
|
||||
{
|
||||
Subtitle = Resources.remotedesktop_subtitle,
|
||||
Icon = Icons.RDPIcon,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(settingsManager.Settings.SettingsPage),
|
||||
|
||||
@@ -39,6 +39,7 @@ public partial class ShellCommandsProvider : CommandProvider
|
||||
{
|
||||
Icon = Icons.RunV2Icon,
|
||||
Title = Resources.shell_command_name,
|
||||
Subtitle = Resources.cmd_plugin_description,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(Settings.SettingsPage),
|
||||
],
|
||||
|
||||
@@ -28,6 +28,7 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
|
||||
{
|
||||
Icon = _timeDateExtensionPage.Icon,
|
||||
Title = Resources.Microsoft_plugin_timedate_plugin_name,
|
||||
Subtitle = GetTranslatedPluginDescription(),
|
||||
MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ public partial class WindowWalkerCommandsProvider : CommandProvider
|
||||
_windowWalkerPageItem = new CommandItem(new WindowWalkerListPage())
|
||||
{
|
||||
Title = Resources.window_walker_top_level_command_title,
|
||||
Subtitle = Resources.windowwalker_name,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(Settings.SettingsPage),
|
||||
],
|
||||
|
||||
@@ -30,6 +30,7 @@ public sealed partial class WindowsSettingsCommandsProvider : CommandProvider
|
||||
_searchSettingsListItem = new CommandItem(new WindowsSettingsListPage(_windowsSettings))
|
||||
{
|
||||
Title = Resources.settings_title,
|
||||
Subtitle = Resources.settings_subtitle,
|
||||
};
|
||||
_fallback = new(_windowsSettings);
|
||||
|
||||
|
||||
@@ -35,10 +35,6 @@ internal sealed partial class SectionsIndexPage : ListPage
|
||||
{
|
||||
Title = "A Gallery grid page with sections",
|
||||
},
|
||||
new ListItem(new SampleListPageWithSections(new GalleryGridLayout() { ShowTitle = false, ShowSubtitle = false }))
|
||||
{
|
||||
Title = "A Gallery grid page without labels with sections",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Runtime.CompilerServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -15,7 +14,7 @@ public partial class BaseObservable : INotifyPropChanged
|
||||
{
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -23,37 +22,10 @@ public partial class BaseObservable : INotifyPropChanged
|
||||
// this can crash as we try to invoke the handlers from that process.
|
||||
// However, just catching it seems to still raise the event on the
|
||||
// new host?
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(propertyName!));
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(propertyName));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the backing field to the specified value and raises a property changed
|
||||
/// notification if the value is different from the current one.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property.</typeparam>
|
||||
/// <param name="field">A reference to the backing field for the property.</param>
|
||||
/// <param name="value">The new value to assign to the property.</param>
|
||||
/// <param name="propertyName">
|
||||
/// The name of the property. This is optional and is usually supplied
|
||||
/// automatically by the <see cref="CallerMemberNameAttribute"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the field was updated and a property changed
|
||||
/// notification was raised; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,31 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Command : BaseObservable, ICommand
|
||||
{
|
||||
public virtual string Name { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Name
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string Id { get; set; } = string.Empty;
|
||||
|
||||
public virtual IconInfo Icon { get; set => SetProperty(ref field, value); } = new();
|
||||
public virtual IconInfo Icon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
= new();
|
||||
|
||||
IIconInfo ICommand.Icon => Icon;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandContextItem : CommandItem, ICommandContextItem
|
||||
{
|
||||
public virtual bool IsCritical { get; set => SetProperty(ref field, value); }
|
||||
public virtual bool IsCritical { get; set; }
|
||||
|
||||
public virtual KeyChord RequestedShortcut { get; set => SetProperty(ref field, value); }
|
||||
public virtual KeyChord RequestedShortcut { get; set; }
|
||||
|
||||
public CommandContextItem(ICommand command)
|
||||
: base(command)
|
||||
|
||||
@@ -19,36 +19,44 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
private DataPackage? _dataPackage;
|
||||
private DataPackageView? _dataPackageView;
|
||||
|
||||
public virtual IIconInfo? Icon { get; set => SetProperty(ref field, value); }
|
||||
public virtual IIconInfo? Icon
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Title
|
||||
{
|
||||
get => !string.IsNullOrEmpty(_title) ? _title : _command?.Name ?? string.Empty;
|
||||
|
||||
set
|
||||
{
|
||||
var oldTitle = Title;
|
||||
_title = value;
|
||||
if (Title != oldTitle)
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Subtitle { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Subtitle
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Subtitle));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual ICommand? Command
|
||||
{
|
||||
get => _command;
|
||||
set
|
||||
{
|
||||
if (EqualityComparer<ICommand?>.Default.Equals(value, _command))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldTitle = Title;
|
||||
|
||||
if (_commandListener is not null)
|
||||
{
|
||||
_commandListener.Detach();
|
||||
@@ -63,8 +71,8 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
value.PropChanged += _commandListener.OnEvent;
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
if (string.IsNullOrEmpty(_title) && oldTitle != Title)
|
||||
OnPropertyChanged(nameof(Command));
|
||||
if (string.IsNullOrEmpty(_title))
|
||||
{
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
@@ -80,7 +88,17 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IContextItem[] MoreCommands { get; set => SetProperty(ref field, value); } = [];
|
||||
public virtual IContextItem[] MoreCommands
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(MoreCommands));
|
||||
}
|
||||
}
|
||||
|
||||
= [];
|
||||
|
||||
public DataPackage? DataPackage
|
||||
{
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandResult : ICommandResult
|
||||
{
|
||||
public ICommandResultArgs? Args { get; private init; }
|
||||
public ICommandResultArgs? Args { get; private set; }
|
||||
|
||||
public CommandResultKind Kind { get; private init; } = CommandResultKind.Dismiss;
|
||||
public CommandResultKind Kind { get; private set; } = CommandResultKind.Dismiss;
|
||||
|
||||
public static CommandResult Dismiss()
|
||||
{
|
||||
|
||||
@@ -10,9 +10,17 @@ public abstract partial class ContentPage : Page, IContentPage
|
||||
{
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
|
||||
public virtual IDetails? Details
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Details));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IContextItem[] Commands { get; set => SetProperty(ref field, value); } = [];
|
||||
public virtual IContextItem[] Commands { get; set; } = [];
|
||||
|
||||
public abstract IContent[] GetContent();
|
||||
|
||||
|
||||
@@ -7,15 +7,65 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Details : BaseObservable, IDetails, IExtendedAttributesProvider
|
||||
{
|
||||
public virtual IIconInfo HeroImage { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
public virtual IIconInfo HeroImage
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(HeroImage));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Title { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
= new IconInfo();
|
||||
|
||||
public virtual string Body { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Title
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IDetailsElement[] Metadata { get; set => SetProperty(ref field, value); } = [];
|
||||
= string.Empty;
|
||||
|
||||
public virtual ContentSize Size { get; set => SetProperty(ref field, value); } = ContentSize.Small;
|
||||
public virtual string Body
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Body));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual IDetailsElement[] Metadata
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Metadata));
|
||||
}
|
||||
}
|
||||
|
||||
= [];
|
||||
|
||||
public virtual ContentSize Size
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Size));
|
||||
}
|
||||
}
|
||||
|
||||
= ContentSize.Small;
|
||||
|
||||
public IDictionary<string, object>? GetProperties() => new ValueSet()
|
||||
{
|
||||
|
||||
@@ -6,9 +6,39 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Filter : BaseObservable, IFilter
|
||||
{
|
||||
public virtual IIconInfo Icon { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
public virtual IIconInfo Icon
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Id { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
= new IconInfo();
|
||||
|
||||
public virtual string Name { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Id
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,17 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class Filters : BaseObservable, IFilters
|
||||
{
|
||||
public string CurrentFilterId { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public string CurrentFilterId
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(CurrentFilterId));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
// This method should be overridden in derived classes to provide the actual filters.
|
||||
public abstract IFilterItem[] GetFilters();
|
||||
|
||||
@@ -6,11 +6,41 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class FormContent : BaseObservable, IFormContent
|
||||
{
|
||||
public virtual string DataJson { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string DataJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(DataJson));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string StateJson { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
= string.Empty;
|
||||
|
||||
public virtual string TemplateJson { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string StateJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(StateJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string TemplateJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(TemplateJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual ICommandResult SubmitForm(string inputs, string data) => SubmitForm(inputs);
|
||||
|
||||
|
||||
@@ -6,7 +6,27 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class GalleryGridLayout : BaseObservable, IGalleryGridLayout
|
||||
{
|
||||
public virtual bool ShowTitle { get; set => SetProperty(ref field, value); } = true;
|
||||
public virtual bool ShowTitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowTitle));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool ShowSubtitle { get; set => SetProperty(ref field, value); } = true;
|
||||
= true;
|
||||
|
||||
public virtual bool ShowSubtitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowSubtitle));
|
||||
}
|
||||
}
|
||||
|
||||
= true;
|
||||
}
|
||||
|
||||
@@ -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.Diagnostics.CodeAnalysis;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -6,13 +6,51 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ListItem : CommandItem, IListItem
|
||||
{
|
||||
public virtual ITag[] Tags { get; set => SetProperty(ref field, value); } = [];
|
||||
private ITag[] _tags = [];
|
||||
private IDetails? _details;
|
||||
|
||||
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
|
||||
private string _section = string.Empty;
|
||||
private string _textToSuggest = string.Empty;
|
||||
|
||||
public virtual string Section { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual ITag[] Tags
|
||||
{
|
||||
get => _tags;
|
||||
set
|
||||
{
|
||||
_tags = value;
|
||||
OnPropertyChanged(nameof(Tags));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string TextToSuggest { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual IDetails? Details
|
||||
{
|
||||
get => _details;
|
||||
set
|
||||
{
|
||||
_details = value;
|
||||
OnPropertyChanged(nameof(Details));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Section
|
||||
{
|
||||
get => _section;
|
||||
set
|
||||
{
|
||||
_section = value;
|
||||
OnPropertyChanged(nameof(Section));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string TextToSuggest
|
||||
{
|
||||
get => _textToSuggest;
|
||||
set
|
||||
{
|
||||
_textToSuggest = value;
|
||||
OnPropertyChanged(nameof(TextToSuggest));
|
||||
}
|
||||
}
|
||||
|
||||
public ListItem(ICommand command)
|
||||
: base(command)
|
||||
|
||||
@@ -8,23 +8,85 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ListPage : Page, IListPage
|
||||
{
|
||||
private string _placeholderText = string.Empty;
|
||||
private string _searchText = string.Empty;
|
||||
private bool _showDetails;
|
||||
private bool _hasMore;
|
||||
private IFilters? _filters;
|
||||
private IGridProperties? _gridProperties;
|
||||
private ICommandItem? _emptyContent;
|
||||
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
public virtual string PlaceholderText
|
||||
{
|
||||
get => _placeholderText;
|
||||
set
|
||||
{
|
||||
_placeholderText = value;
|
||||
OnPropertyChanged(nameof(PlaceholderText));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string PlaceholderText { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
_searchText = value;
|
||||
OnPropertyChanged(nameof(SearchText));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
|
||||
public virtual bool ShowDetails
|
||||
{
|
||||
get => _showDetails;
|
||||
set
|
||||
{
|
||||
_showDetails = value;
|
||||
OnPropertyChanged(nameof(ShowDetails));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool ShowDetails { get; set => SetProperty(ref field, value); }
|
||||
public virtual bool HasMoreItems
|
||||
{
|
||||
get => _hasMore;
|
||||
set
|
||||
{
|
||||
_hasMore = value;
|
||||
OnPropertyChanged(nameof(HasMoreItems));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool HasMoreItems { get; set => SetProperty(ref field, value); }
|
||||
public virtual IFilters? Filters
|
||||
{
|
||||
get => _filters;
|
||||
set
|
||||
{
|
||||
_filters = value;
|
||||
OnPropertyChanged(nameof(Filters));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IFilters? Filters { get; set => SetProperty(ref field, value); }
|
||||
public virtual IGridProperties? GridProperties
|
||||
{
|
||||
get => _gridProperties;
|
||||
set
|
||||
{
|
||||
_gridProperties = value;
|
||||
OnPropertyChanged(nameof(GridProperties));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IGridProperties? GridProperties { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual ICommandItem? EmptyContent { get; set => SetProperty(ref field, value); }
|
||||
public virtual ICommandItem? EmptyContent
|
||||
{
|
||||
get => _emptyContent;
|
||||
set
|
||||
{
|
||||
_emptyContent = value;
|
||||
OnPropertyChanged(nameof(EmptyContent));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IListItem[] GetItems() => [];
|
||||
|
||||
|
||||
@@ -6,7 +6,17 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class MarkdownContent : BaseObservable, IMarkdownContent
|
||||
{
|
||||
public virtual string Body { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Body
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Body));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public MarkdownContent()
|
||||
{
|
||||
|
||||
@@ -6,5 +6,15 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class MediumGridLayout : BaseObservable, IMediumGridLayout
|
||||
{
|
||||
public virtual bool ShowTitle { get; set => SetProperty(ref field, value); } = true;
|
||||
public virtual bool ShowTitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowTitle));
|
||||
}
|
||||
}
|
||||
|
||||
= true;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,37 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Page : Command, IPage
|
||||
{
|
||||
public virtual bool IsLoading { get; set => SetProperty(ref field, value); }
|
||||
private bool _loading;
|
||||
private string _title = string.Empty;
|
||||
private OptionalColor _accentColor;
|
||||
|
||||
public virtual string Title { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual bool IsLoading
|
||||
{
|
||||
get => _loading;
|
||||
set
|
||||
{
|
||||
_loading = value;
|
||||
OnPropertyChanged(nameof(IsLoading));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual OptionalColor AccentColor { get; set => SetProperty(ref field, value); }
|
||||
public virtual string Title
|
||||
{
|
||||
get => _title;
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual OptionalColor AccentColor
|
||||
{
|
||||
get => _accentColor;
|
||||
set
|
||||
{
|
||||
_accentColor = value;
|
||||
OnPropertyChanged(nameof(AccentColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,27 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ProgressState : BaseObservable, IProgressState
|
||||
{
|
||||
public virtual bool IsIndeterminate { get; set => SetProperty(ref field, value); }
|
||||
private bool _isIndeterminate;
|
||||
|
||||
public virtual uint ProgressPercent { get; set => SetProperty(ref field, value); }
|
||||
private uint _progressPercent;
|
||||
|
||||
public virtual bool IsIndeterminate
|
||||
{
|
||||
get => _isIndeterminate;
|
||||
set
|
||||
{
|
||||
_isIndeterminate = value;
|
||||
OnPropertyChanged(nameof(IsIndeterminate));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual uint ProgressPercent
|
||||
{
|
||||
get => _progressPercent;
|
||||
set
|
||||
{
|
||||
_progressPercent = value;
|
||||
OnPropertyChanged(nameof(ProgressPercent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class PropChangedEventArgs : IPropChangedEventArgs
|
||||
|
||||
@@ -12,6 +12,11 @@ public sealed partial class Section : IEnumerable<IListItem>
|
||||
|
||||
public string SectionTitle { get; set; } = string.Empty;
|
||||
|
||||
private Separator CreateSectionListItem()
|
||||
{
|
||||
return new Separator(SectionTitle);
|
||||
}
|
||||
|
||||
public Section(string sectionName, IListItem[] items)
|
||||
{
|
||||
SectionTitle = sectionName;
|
||||
@@ -28,11 +33,6 @@ public sealed partial class Section : IEnumerable<IListItem>
|
||||
{
|
||||
}
|
||||
|
||||
private Separator CreateSectionListItem()
|
||||
{
|
||||
return new Separator(SectionTitle);
|
||||
}
|
||||
|
||||
public IEnumerator<IListItem> GetEnumerator() => Items.ToList().GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Separator : BaseObservable, IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||
public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||
{
|
||||
public Separator(string? title = "")
|
||||
: base()
|
||||
{
|
||||
Section = title ?? string.Empty;
|
||||
Command = null;
|
||||
}
|
||||
|
||||
public IDetails? Details => null;
|
||||
|
||||
public string? Section { get; private set; }
|
||||
@@ -14,7 +21,7 @@ public partial class Separator : BaseObservable, IListItem, ISeparatorContextIte
|
||||
|
||||
public string? TextToSuggest => null;
|
||||
|
||||
public ICommand? Command => null;
|
||||
public ICommand? Command { get; private set; }
|
||||
|
||||
public IIconInfo? Icon => null;
|
||||
|
||||
@@ -25,19 +32,12 @@ public partial class Separator : BaseObservable, IListItem, ISeparatorContextIte
|
||||
public string? Title
|
||||
{
|
||||
get => Section;
|
||||
set
|
||||
{
|
||||
if (Section != value)
|
||||
{
|
||||
Section = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(Section);
|
||||
}
|
||||
}
|
||||
set => Section = value;
|
||||
}
|
||||
|
||||
public Separator(string? title = "")
|
||||
public event Windows.Foundation.TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
|
||||
{
|
||||
Section = title ?? string.Empty;
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,37 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class StatusMessage : BaseObservable, IStatusMessage
|
||||
{
|
||||
public virtual string Message { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Message
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Message));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual MessageState State { get; set => SetProperty(ref field, value); } = MessageState.Info;
|
||||
= string.Empty;
|
||||
|
||||
public virtual IProgressState? Progress { get; set => SetProperty(ref field, value); }
|
||||
public virtual MessageState State
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(State));
|
||||
}
|
||||
}
|
||||
|
||||
= MessageState.Info;
|
||||
|
||||
public virtual IProgressState? Progress
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Progress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,63 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Tag : BaseObservable, ITag
|
||||
{
|
||||
public virtual OptionalColor Foreground { get; set => SetProperty(ref field, value); }
|
||||
private OptionalColor _foreground;
|
||||
private OptionalColor _background;
|
||||
private string _text = string.Empty;
|
||||
|
||||
public virtual OptionalColor Background { get; set => SetProperty(ref field, value); }
|
||||
public virtual OptionalColor Foreground
|
||||
{
|
||||
get => _foreground;
|
||||
set
|
||||
{
|
||||
_foreground = value;
|
||||
OnPropertyChanged(nameof(Foreground));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IIconInfo Icon { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
public virtual OptionalColor Background
|
||||
{
|
||||
get => _background;
|
||||
set
|
||||
{
|
||||
_background = value;
|
||||
OnPropertyChanged(nameof(Background));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Text { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual IIconInfo Icon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string ToolTip { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
= new IconInfo();
|
||||
|
||||
public virtual string Text
|
||||
{
|
||||
get => _text;
|
||||
set
|
||||
{
|
||||
_text = value;
|
||||
OnPropertyChanged(nameof(Text));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string ToolTip
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ToolTip));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public Tag()
|
||||
{
|
||||
@@ -22,6 +70,6 @@ public partial class Tag : BaseObservable, ITag
|
||||
|
||||
public Tag(string text)
|
||||
{
|
||||
Text = text;
|
||||
_text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,19 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class TreeContent : BaseObservable, ITreeContent
|
||||
{
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public IContent[] Children { get; set; } = [];
|
||||
|
||||
public virtual IContent? RootContent { get; set => SetProperty(ref field, value); }
|
||||
public virtual IContent? RootContent
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(RootContent));
|
||||
}
|
||||
}
|
||||
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public virtual IContent[] GetChildren() => Children;
|
||||
|
||||
|
||||
@@ -191,22 +191,27 @@ bool EditorParameters::Save(const WorkAreaConfiguration& configuration, OnThread
|
||||
|
||||
monitorJson.dpi = dpi;
|
||||
|
||||
// Get DPI-unaware values for dimensions (virtual coordinates for WPF sizing)
|
||||
MONITORINFOEX monitorInfoUnaware{};
|
||||
MONITORINFOEX monitorInfo{};
|
||||
dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] {
|
||||
monitorInfoUnaware.cbSize = sizeof(monitorInfoUnaware);
|
||||
GetMonitorInfo(monitor, &monitorInfoUnaware);
|
||||
monitorInfo.cbSize = sizeof(monitorInfo);
|
||||
if (!GetMonitorInfo(monitor, &monitorInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
} }).wait();
|
||||
|
||||
// Dimensions in virtual coordinates (from DPI-unaware thread)
|
||||
monitorJson.monitorWidth = monitorInfoUnaware.rcMonitor.right - monitorInfoUnaware.rcMonitor.left;
|
||||
monitorJson.monitorHeight = monitorInfoUnaware.rcMonitor.bottom - monitorInfoUnaware.rcMonitor.top;
|
||||
monitorJson.workAreaWidth = monitorInfoUnaware.rcWork.right - monitorInfoUnaware.rcWork.left;
|
||||
monitorJson.workAreaHeight = monitorInfoUnaware.rcWork.bottom - monitorInfoUnaware.rcWork.top;
|
||||
float width = static_cast<float>(monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left);
|
||||
float height = static_cast<float>(monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top);
|
||||
DPIAware::Convert(monitor, width, height);
|
||||
|
||||
// Position in virtual coordinates (matched by DPI-unaware context in WPF editor)
|
||||
monitorJson.left = monitorInfoUnaware.rcWork.left;
|
||||
monitorJson.top = monitorInfoUnaware.rcWork.top;
|
||||
monitorJson.monitorWidth = static_cast<int>(std::roundf(width));
|
||||
monitorJson.monitorHeight = static_cast<int>(std::roundf(height));
|
||||
|
||||
// use dpi-unaware values
|
||||
monitorJson.top = monitorInfo.rcWork.top;
|
||||
monitorJson.left = monitorInfo.rcWork.left;
|
||||
monitorJson.workAreaWidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left;
|
||||
monitorJson.workAreaHeight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top;
|
||||
|
||||
argsJson.monitors.emplace_back(std::move(monitorJson));
|
||||
}
|
||||
|
||||
@@ -67,18 +67,10 @@ namespace FancyZonesEditor.Models
|
||||
Window.KeyUp += ((App)Application.Current).App_KeyUp;
|
||||
Window.KeyDown += ((App)Application.Current).App_KeyDown;
|
||||
|
||||
// Store for DPI-unaware positioning
|
||||
_virtualWorkArea = workArea;
|
||||
|
||||
// Set initial WPF properties
|
||||
Window.Left = workArea.X;
|
||||
Window.Top = workArea.Y;
|
||||
Window.Width = workArea.Width;
|
||||
Window.Height = workArea.Height;
|
||||
|
||||
// After HWND is created, reposition using DPI-unaware context
|
||||
// This matches the C++ backend which uses a DPI-unaware thread
|
||||
Window.SourceInitialized += OnWindowSourceInitialized;
|
||||
}
|
||||
|
||||
public Monitor(string monitorName, string monitorInstanceId, string monitorSerialNumber, string virtualDesktop, int dpi, Rect workArea, Size monitorSize)
|
||||
@@ -88,33 +80,16 @@ namespace FancyZonesEditor.Models
|
||||
}
|
||||
|
||||
private LayoutSettings _settings;
|
||||
private Rect _virtualWorkArea;
|
||||
|
||||
private void OnWindowSourceInitialized(object sender, EventArgs e)
|
||||
{
|
||||
// Reposition window using DPI-unaware context to match the virtual coordinates
|
||||
// from the FancyZones C++ backend (which uses a DPI-unaware thread)
|
||||
Utils.NativeMethods.SetWindowPositionDpiUnaware(
|
||||
Window,
|
||||
(int)_virtualWorkArea.X,
|
||||
(int)_virtualWorkArea.Y,
|
||||
(int)_virtualWorkArea.Width,
|
||||
(int)_virtualWorkArea.Height);
|
||||
}
|
||||
|
||||
public void Scale(double scaleFactor)
|
||||
{
|
||||
Device.Scale(scaleFactor);
|
||||
|
||||
_virtualWorkArea = Device.WorkAreaRect;
|
||||
|
||||
// Use DPI-unaware positioning
|
||||
Utils.NativeMethods.SetWindowPositionDpiUnaware(
|
||||
Window,
|
||||
(int)_virtualWorkArea.X,
|
||||
(int)_virtualWorkArea.Y,
|
||||
(int)_virtualWorkArea.Width,
|
||||
(int)_virtualWorkArea.Height);
|
||||
var workArea = Device.WorkAreaRect;
|
||||
Window.Left = workArea.X;
|
||||
Window.Top = workArea.Y;
|
||||
Window.Width = workArea.Width;
|
||||
Window.Height = workArea.Height;
|
||||
}
|
||||
|
||||
public void SetLayoutSettings(LayoutModel model)
|
||||
|
||||
@@ -69,11 +69,7 @@ namespace FancyZonesEditor.Utils
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert virtual coordinates to physical resolution by applying DPI scale
|
||||
double scale = DPI / 96.0;
|
||||
int physicalWidth = (int)Math.Round(ScreenBoundsWidth * scale);
|
||||
int physicalHeight = (int)Math.Round(ScreenBoundsHeight * scale);
|
||||
return physicalWidth + " × " + physicalHeight;
|
||||
return ScreenBoundsWidth + " × " + ScreenBoundsHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -17,48 +17,14 @@ namespace FancyZonesEditor.Utils
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||
|
||||
private const int GWL_EX_STYLE = -20;
|
||||
private const int WS_EX_APPWINDOW = 0x00040000;
|
||||
private const int WS_EX_TOOLWINDOW = 0x00000080;
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
|
||||
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
|
||||
|
||||
public static void SetWindowStyleToolWindow(Window hwnd)
|
||||
{
|
||||
var helper = new WindowInteropHelper(hwnd).Handle;
|
||||
_ = SetWindowLong(helper, GWL_EX_STYLE, (GetWindowLong(helper, GWL_EX_STYLE) | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates
|
||||
/// from the FancyZones C++ backend (which uses a DPI-unaware thread).
|
||||
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||
/// </summary>
|
||||
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
|
||||
{
|
||||
var helper = new WindowInteropHelper(window).Handle;
|
||||
if (helper != IntPtr.Zero)
|
||||
{
|
||||
// Temporarily switch to DPI-unaware context to position window.
|
||||
// This matches how the C++ backend gets coordinates via dpiUnawareThread.
|
||||
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
|
||||
try
|
||||
{
|
||||
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetThreadDpiAwarenessContext(oldContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,6 @@
|
||||
x:Name="OpenUriDialog"
|
||||
x:Uid="OpenUriDialog"
|
||||
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
|
||||
SecondaryButtonClick="OpenUriDialog_SecondaryButtonClick">
|
||||
<StackPanel Spacing="16">
|
||||
<controls:InfoBar
|
||||
x:Name="OpenUriWarningBanner"
|
||||
x:Uid="OpenUriWarningBanner"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Severity="Warning" />
|
||||
<TextBlock
|
||||
x:Name="OpenUriDialogContent"
|
||||
IsTextSelectionEnabled="True"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
SecondaryButtonClick="OpenUriDialog_SecondaryButtonClick" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -34,11 +34,6 @@ namespace Peek.FilePreviewer.Controls
|
||||
|
||||
private Color? _originalBackgroundColor;
|
||||
|
||||
/// <summary>
|
||||
/// URI of the current source being previewed (for resource filtering)
|
||||
/// </summary>
|
||||
private Uri? _currentSourceUri;
|
||||
|
||||
public delegate void NavigationCompletedHandler(WebView2? sender, CoreWebView2NavigationCompletedEventArgs? args);
|
||||
|
||||
public delegate void DOMContentLoadedHandler(CoreWebView2? sender, CoreWebView2DOMContentLoadedEventArgs? args);
|
||||
@@ -102,7 +97,6 @@ namespace Peek.FilePreviewer.Controls
|
||||
{
|
||||
this.InitializeComponent();
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", TempFolderPath.Path, EnvironmentVariableTarget.Process);
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--block-new-web-contents", EnvironmentVariableTarget.Process);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -111,7 +105,6 @@ namespace Peek.FilePreviewer.Controls
|
||||
{
|
||||
PreviewBrowser.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
|
||||
PreviewBrowser.CoreWebView2.ContextMenuRequested -= CoreWebView2_ContextMenuRequested;
|
||||
RemoveResourceFilter();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,14 +123,6 @@ namespace Peek.FilePreviewer.Controls
|
||||
{
|
||||
/* CoreWebView2.Navigate() will always trigger a navigation even if the content/URI is the same.
|
||||
* Use WebView2.Source to avoid re-navigating to the same content. */
|
||||
_currentSourceUri = Source;
|
||||
|
||||
// Only apply resource filter for non-dev files
|
||||
if (!IsDevFilePreview)
|
||||
{
|
||||
ApplyResourceFilter();
|
||||
}
|
||||
|
||||
PreviewBrowser.CoreWebView2.Navigate(Source.ToString());
|
||||
}
|
||||
}
|
||||
@@ -161,14 +146,10 @@ namespace Peek.FilePreviewer.Controls
|
||||
if (IsDevFilePreview)
|
||||
{
|
||||
PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
|
||||
|
||||
// Remove resource filter for dev files (Monaco needs to load resources)
|
||||
RemoveResourceFilter();
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewBrowser.CoreWebView2.ClearVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName);
|
||||
ApplyResourceFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,66 +283,6 @@ namespace Peek.FilePreviewer.Controls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies strict resource filtering for non-dev files to block external resources.
|
||||
/// This prevents XSS attacks and unwanted external content loading.
|
||||
/// </summary>
|
||||
private void ApplyResourceFilter()
|
||||
{
|
||||
// Remove existing handler to prevent duplicate subscriptions
|
||||
RemoveResourceFilter();
|
||||
|
||||
// Add filter and subscribe to resource requests
|
||||
PreviewBrowser.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All);
|
||||
PreviewBrowser.CoreWebView2.WebResourceRequested += CoreWebView2_WebResourceRequested;
|
||||
}
|
||||
|
||||
private void RemoveResourceFilter()
|
||||
{
|
||||
PreviewBrowser.CoreWebView2.WebResourceRequested -= CoreWebView2_WebResourceRequested;
|
||||
}
|
||||
|
||||
private void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWebView2WebResourceRequestedEventArgs args)
|
||||
{
|
||||
if (_currentSourceUri == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestUri = new Uri(args.Request.Uri);
|
||||
|
||||
// Allow loading the source file itself
|
||||
if (requestUri == _currentSourceUri)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// For local file:// resources, allow same directory and subdirectories
|
||||
if (requestUri.Scheme == "file" && _currentSourceUri.Scheme == "file")
|
||||
{
|
||||
try
|
||||
{
|
||||
var sourceDirectory = System.IO.Path.GetDirectoryName(_currentSourceUri.LocalPath);
|
||||
var requestPath = requestUri.LocalPath;
|
||||
|
||||
// Allow resources in the same directory or subdirectories
|
||||
if (!string.IsNullOrEmpty(sourceDirectory) &&
|
||||
requestPath.StartsWith(sourceDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If path processing fails, block for security
|
||||
}
|
||||
}
|
||||
|
||||
// Block all other resources including http(s) requests to prevent external tracking,
|
||||
// data exfiltration, and XSS attacks
|
||||
args.Response = PreviewBrowser.CoreWebView2.Environment.CreateWebResourceResponse(null, 403, "Forbidden", null);
|
||||
}
|
||||
|
||||
private void CoreWebView2_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)
|
||||
{
|
||||
// If the file being previewed is HTML or HTM, reset the background color to its original state.
|
||||
@@ -423,7 +344,7 @@ namespace Peek.FilePreviewer.Controls
|
||||
|
||||
private async Task ShowOpenUriDialogAsync(Uri uri)
|
||||
{
|
||||
OpenUriDialogContent.Text = uri.ToString();
|
||||
OpenUriDialog.Content = uri.ToString();
|
||||
var result = await OpenUriDialog.ShowAsync();
|
||||
|
||||
if (result == ContentDialogResult.Primary)
|
||||
@@ -435,7 +356,7 @@ namespace Peek.FilePreviewer.Controls
|
||||
private void OpenUriDialog_SecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
var dataPackage = new DataPackage();
|
||||
dataPackage.SetText(OpenUriDialogContent.Text);
|
||||
dataPackage.SetText(sender.Content.ToString());
|
||||
Clipboard.SetContent(dataPackage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,41 +113,38 @@ namespace Peek.FilePreviewer.Previewers
|
||||
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
string extension = File.Extension;
|
||||
bool isHtml = File.Extension == ".html" || File.Extension == ".htm";
|
||||
bool isMarkdown = File.Extension == ".md";
|
||||
bool isSvg = File.Extension == ".svg";
|
||||
|
||||
// Default: non-dev file preview with standard context menu
|
||||
IsDevFilePreview = false;
|
||||
CustomContextMenu = false;
|
||||
bool supportedByMonaco = MonacoHelper.SupportedMonacoFileTypes.Contains(File.Extension);
|
||||
bool useMonaco = supportedByMonaco && !isHtml && !isMarkdown && !isSvg;
|
||||
|
||||
// Determine preview strategy based on file type priority
|
||||
if (extension == ".md")
|
||||
IsDevFilePreview = supportedByMonaco;
|
||||
CustomContextMenu = useMonaco;
|
||||
|
||||
if (useMonaco)
|
||||
{
|
||||
// Markdown files use custom renderer
|
||||
var raw = await ReadHelper.Read(File.Path.ToString());
|
||||
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, File.Extension, TempFolderPath.Path, _previewSettings.SourceCodeTryFormat, _previewSettings.SourceCodeWrapText, _previewSettings.SourceCodeStickyScroll, _previewSettings.SourceCodeFontSize, _previewSettings.SourceCodeMinimap));
|
||||
}
|
||||
else if (isMarkdown)
|
||||
{
|
||||
IsDevFilePreview = false;
|
||||
var raw = await ReadHelper.Read(File.Path.ToString());
|
||||
Preview = new Uri(MarkdownHelper.PreviewTempFile(raw, File.Path, TempFolderPath.Path));
|
||||
}
|
||||
else if (extension == ".svg")
|
||||
else if (isSvg)
|
||||
{
|
||||
// SVG files are rendered directly by WebView2 for better compatibility
|
||||
// with complex SVGs from Adobe Illustrator, Inkscape, etc.
|
||||
IsDevFilePreview = false;
|
||||
Preview = new Uri(File.Path);
|
||||
}
|
||||
else if (extension == ".html" || extension == ".htm")
|
||||
{
|
||||
// Simple html file to preview. Shouldn't do things like enabling scripts or using a virtual mapped directory.
|
||||
Preview = new Uri(File.Path);
|
||||
}
|
||||
else if (MonacoHelper.SupportedMonacoFileTypes.Contains(extension))
|
||||
{
|
||||
// Source code files use Monaco editor
|
||||
IsDevFilePreview = true;
|
||||
CustomContextMenu = true;
|
||||
var raw = await ReadHelper.Read(File.Path.ToString());
|
||||
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, extension, TempFolderPath.Path, _previewSettings.SourceCodeTryFormat, _previewSettings.SourceCodeWrapText, _previewSettings.SourceCodeStickyScroll, _previewSettings.SourceCodeFontSize, _previewSettings.SourceCodeMinimap));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback for other supported file types (e.g., PDF)
|
||||
// Simple html file to preview. Shouldn't do things like enabling scripts or using a virtual mapped directory.
|
||||
IsDevFilePreview = false;
|
||||
Preview = new Uri(File.Path);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -250,17 +250,9 @@
|
||||
<comment>Dialog showed when an URI is clicked. Button to copy the URI.</comment>
|
||||
</data>
|
||||
<data name="OpenUriDialog.Title" xml:space="preserve">
|
||||
<value>Do you want Peek to open this link?</value>
|
||||
<value>Do you want Peek to open the external application?</value>
|
||||
<comment>Title of the dialog showed when an URI is clicked,"Peek" is the name of the utility. </comment>
|
||||
</data>
|
||||
<data name="OpenUriWarningBanner.Title" xml:space="preserve">
|
||||
<value>Security Warning</value>
|
||||
<comment>Title of the warning banner in the open URI dialog.</comment>
|
||||
</data>
|
||||
<data name="OpenUriWarningBanner.Message" xml:space="preserve">
|
||||
<value>This link will open externally. Make sure you trust the source before proceeding.</value>
|
||||
<comment>Warning message displayed in the banner when opening an external URI.</comment>
|
||||
</data>
|
||||
<data name="ReadableString_BytesString" xml:space="preserve">
|
||||
<value> ({1:N0} bytes)</value>
|
||||
<comment>Displays total number of bytes. Don't localize the "{1:N0}" part.</comment>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user