Compare commits

...

12 Commits

Author SHA1 Message Date
Kazeem Quadri
1b6b446915 Add Drag and Drop For Environment Variables (#40105)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This PR introduces drag-and-drop functionality for environment
variables, allowing users to easily update the order of variables based
on their preferences. Users can now rearrange variables directly in the
UI by dragging and dropping, making it more intuitive and efficient to
customise the order without manual editing.

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

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
Updated `EnvironmentVariablesMainPage.xaml` to use a `ListView` instead
of an `ItemsControl`, enabling item dragging and reordering. The new
`ListView` features a grid layout with a `FontIcon`, `TextBox`, and
`Button`, while maintaining visibility control through data binding.

Added `EditVariableValuesList_DragItemsCompleted` method in
`EnvironmentVariablesMainPage.xaml.cs` to update the text box with the
new order of items after drag-and-drop operations, ensuring consistency
with the underlying data model.
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
- Manually tested the affected functionality.
- Change was minimal and did not impact existing automated tests.
- Verified that the new/modified feature works as expected and did not
cause regressions in related areas.

---------

Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2026-01-05 09:30:02 +08:00
safocl
78b0139bc3 powerrename: fix union usage (#42845)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

- [x] Closes: #42843
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
fix undefined behavior when using union in the `IFACEMETHODIMP
CPowerRenameRegEx::PutFileTime(_In_ SYSTEMTIME fileTime)` function

a69f7fa806/src/modules/powerrename/lib/PowerRenameRegEx.cpp (L299)

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-04 19:33:16 +08:00
Kai Tao
709c4bbf6b Cmdpal: PowerToys extension localization (#44520)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Localization for cmdpal powertoys extension
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-04 15:18:27 +08:00
Ruben Fricke
bb3435322f Fix broken style.md links in devdocs (#44457)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Fix two broken links to style.md in devcods. This file was moved to
development/style.md in PR #43399, but these references were not
updated.

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

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
Fixed broken links in:
  - doc/devdocs/readme.md (line 60)
  - doc/devdocs/guidance.md (line 61)
  
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually verified that the links resolve correctly.
2026-01-04 12:23:45 +08:00
NOXX - Commiter
d43e1d8cb2 Add Antygravity plugin to third-party plugins list (#44392)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-04 11:12:47 +08:00
Shawn Yuan
1a145fd136 Fix peek issue (#44456)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request focuses on improving the security and behavior of
Markdown file previews. The main changes include disabling HTML
rendering in Markdown to prevent potential security issues, and ensuring
that the `IsDevFilePreview` flag is set correctly when previewing
Markdown files.

**Markdown rendering and preview behavior:**

* Disabled HTML rendering in the Markdown pipeline by adding
`.DisableHtml()` in `MarkdownHelper.MarkdownHtml`, which helps prevent
XSS and other security issues in file previews.
* Explicitly set `IsDevFilePreview` to `false` when handling Markdown
files in `WebBrowserPreviewer`, ensuring correct preview state.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-29 14:23:16 +08:00
leileizhang
673cd5aba3 Add standard CLI support for Image Resizer (#44287)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Adds a dedicated command-line interface (CLI) executable for Image
Resizer (PowerToys.ImageResizerCLI.exe)

## Command
`PowerToys.ImageResizerCLI.exe [options] [files...]`

## Options (High Level)

| Option (aliases) | Description |
|-----------------|-------------|
| `--help` | Show help |
| `--show-config` | Print current effective configuration |
| `--destination`, `-d` | Output directory (optional) |
| `--width`, `-w` | Width |
| `--height`, `-h` | Height |
| `--unit`, `-u` | Unit (Pixel / Percent / Inch / Centimeter) |
| `--fit`, `-f` | Fit mode (Fill / Fit / Stretch) |
| `--size`, `-s` | Preset size index (supports `0` for Custom) |
| `--shrink-only` | Only shrink (do not enlarge) |
| `--replace` | Replace original |
| `--ignore-orientation` | Ignore EXIF orientation |
| `--remove-metadata` | Strip metadata |
| `--quality`, `-q` | JPEG quality (1–100) |
| `--keep-date-modified` | Preserve source last-write time |
| `--file-name` | Output filename format |

## Example usage
```
# Show help
PowerToys.ImageResizerCLI.exe --help

# Show current config
PowerToys.ImageResizerCLI.exe --show-config

# Resize with explicit dimensions
PowerToys.ImageResizerCLI.exe --width 800 --height 600 .\image.png

# Use preset size 0 (Custom) and output to a folder
PowerToys.ImageResizerCLI.exe --size 0 -d "C:\Output" .\photo.png

# Preserve source LastWriteTime
PowerToys.ImageResizerCLI.exe --width 800 --height 600 --keep-date-modified -d "C:\Output" .\image.png
```

![imageresize](https://github.com/user-attachments/assets/437fc1c2-b655-4168-9c85-b1561eeef3b4)

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-26 12:54:47 +08:00
Dave Rayment
97997035f7 [Awake] Fix issues with help and error text not being visible when running Awake via the command line (#41774)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This fixes issues when running Awake via the command line. It allows for
the display of help/usage information, parsing errors, and normal
logging information to the user, whereas these were not shown
previously.

Note: the GPO check is now deliberately placed _after_ the parameter
parsing, changing previous behaviour. This lets the user view help
information about Awake even if they cannot yet run the application
because of a policy rule. There is no change to the GPO check itself.

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

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
Awake is compiled as a Windows Executable. When run via the command
line, it does not have a console to which it can log information or
errors. The application does open its own console under certain
circumstances, but this occurs _after_ command line parameter parsing is
done, which means errors and help information cannot be displayed.

This fix attaches to the parent console and moves the parameter parsing
to the start of `Main` so the errors and usage information can now be
seen:

### Help/usage information
<img width="1449" height="501" alt="image"
src="https://github.com/user-attachments/assets/e4d02501-1484-4f5d-a00a-606aaf13973e"
/>

### Parsing error display
<img width="1458" height="570" alt="image"
src="https://github.com/user-attachments/assets/66405db9-0b65-4f07-9af9-d22ecd0da2ba"
/>

### Normal operation
<img width="1585" height="640" alt="image"
src="https://github.com/user-attachments/assets/d393e1dd-6d0f-43d1-9b1c-4922c8aab40f"
/>

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
- Tested that all modes still perform as expected from both the command
line and via PowerToys Runner / settings file.
- Confirmed that there were no side-effects from attaching to the
console when running in non-command line mode (`AttachConsole` fails in
that instance and no other changes are apparent).

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-12-25 16:31:58 +08:00
Dustin L. Howett
59962ffd3a wip: Okay, disable caching for now (#43126) 2025-12-25 12:33:43 +08:00
leileizhang
3f106344b3 [FancyZones CLI] Add localization and telemetry support (#44421)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This PR adds comprehensive localization and telemetry support to the
FancyZones CLI, improving user experience for international users and
enabling usage tracking for product insights.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-25 12:23:25 +08:00
Shawn Yuan
ab531b2620 Fix empty endpoint issue (#44415)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request introduces a small but important improvement to the
handling of AI provider endpoint configuration in the advanced paste
settings. Now, if an endpoint is required but not provided by the user,
a placeholder value will be set automatically.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-25 11:57:51 +08:00
Dave Rayment
48e95caf39 [PowerRename] Fix Unicode characters and non-breaking spaces not being correctly normalized before matching (#43972)
## Summary of the Pull Request
Fixes PowerRename failing to normalise different Unicode forms before
matching. This results in filenames containing visually identical
characters to the search term from failing to match because their
underlying binary representations differ.

This affects renaming files created on macOS which names files in NFD
(decomposed form) rather than Windows' NFC (precomposed form).

Additionally, this fixes matching to filenames containing non-breaking
space characters, which can be created by automated systems and web
downloaders. Previously, the NBSP character would fail to match a normal
space.

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

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
The underlying issue is a binary mismatch between:

1. Precomposed characters (NFC) typed by Windows users, e.g. `U+0439` -
`й`.
2. Decomposed characters (NFD) found in filenames from other platforms
(or copied from text), e.g. `U+0438` `U+0306` - `и` + `̆ `.
3. Standard spaces (`U+0020`) versus non-breaking spaces (`U+00A0`).

### Updates to PowerRenameRegex.cpp

I added a `SanitizeAndNormalize` function which replaces all
non-breaking spaces with standard spaces and normalises the string to
**Normalization Form C** using Win32's `NormalizeString`.

`PutSearchTerm` and `PutReplaceTerm` now normalise input immediately
before performing any other processing.

`Replace` now normalises the `source` filename before processing.

I updated the RegEx path to ensure it runs against the normalised
`sourceToUse` string instead of the raw `source` string; otherwise regex
matches would fail.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually tested the use case detailed in #43971 with the following
filenames:

- `Testй NFC.txt`
- `Testй NFD.txt`

Result:
<img width="1097" height="542" alt="image"
src="https://github.com/user-attachments/assets/55dd4f01-8ec9-462c-a20f-dd246c368cf5"
/>

There are two new unit tests which exercise both the non-breaking space
and Unicode form normalisation issues. These run on both the Boost- and
non-Boost test paths, adding four tests to the total. All new tests fail
as expected on the prior code and all PowerRename tests pass
successfully with the changes in this PR:

<img width="606" height="276" alt="image"
src="https://github.com/user-attachments/assets/08dc01f6-201c-4d56-8f34-e5043e3d1e86"
/>
2025-12-25 11:34:32 +08:00
106 changed files with 5781 additions and 408 deletions

View File

@@ -330,6 +330,9 @@ HHH
riday
YYY
# Unicode
precomposed
# GitHub issue/PR commands
azp
feedbackhub

View File

@@ -131,6 +131,8 @@
"PowerToys.ImageResizer.exe",
"PowerToys.ImageResizer.dll",
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
"PowerToys.ImageResizerExt.dll",
"PowerToys.ImageResizerContextMenu.dll",
"ImageResizerContextMenuPackage.msix",

View File

@@ -444,6 +444,10 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.FancyZones_ZoneWindowKeyUp</td>
<td>Occurs when a key is released while interacting with zones.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZones_CLICommand</td>
<td>Triggered when a FancyZones CLI command is executed, logging the command name and success status.</td>
</tr>
</table>
### FileExplorerAddOns

View File

@@ -459,6 +459,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/imageresizer/Tests/">
<Project Path="src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj">

View File

@@ -0,0 +1,93 @@
# CLI Conventions
This document describes the conventions for implementing command-line interfaces (CLI) in PowerToys modules.
## Library
Use the **System.CommandLine** library for CLI argument parsing. This is already defined in `Directory.Packages.props`:
```xml
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
```
Add the reference to your project:
```xml
<PackageReference Include="System.CommandLine" />
```
## Option Naming and Definition
- Use `--kebab-case` for long form (e.g., `--shrink-only`).
- Use single `-x` for short form (e.g., `-s`, `-w`).
- Define aliases as static readonly arrays: `["--silent", "-s"]`.
- Create options using `Option<T>` with descriptive help text.
- Add validators for options that require range or format checking.
## RootCommand Setup
- Create a `RootCommand` with a brief description.
- Add all options and arguments to the command.
## Parsing
- Use `Parser(rootCommand).Parse(args)` to parse CLI arguments.
- Extract option values using `parseResult.GetValueForOption()`.
- Note: Use `Parser` directly; `RootCommand.Parse()` may not be available with the pinned System.CommandLine version.
### Parse/Validation Errors
- On parse/validation errors, print error messages and usage, then exit with non-zero code.
## Examples
Reference implementations:
- Awake: `src/modules/Awake/Awake/Program.cs`
- ImageResizer: `src/modules/imageresizer/ui/Cli/`
## Help Output
- Provide a `PrintUsage()` method for custom help formatting if needed.
## Best Practices
1. **Consistency**: Follow existing module patterns.
2. **Documentation**: Always provide help text for each option.
3. **Validation**: Validate input and provide clear error messages.
4. **Atomicity**: Make one logical change per PR; avoid drive-by refactors.
5. **Build/Test Discipline**: Build and test synchronously, one terminal per operation.
6. **Style**: Follow repo analyzers (`.editorconfig`, StyleCop) and formatting rules.
## Logging Requirements
- Use `ManagedCommon.Logger` for consistent logging.
- Initialize logging early in `Main()`.
- Use dual output (console + log file) for errors and warnings to ensure visibility.
- Reference: `src/modules/imageresizer/ui/Cli/CliLogger.cs`
## Error Handling
### Exit Codes
- `0`: Success
- `1`: General error (parsing, validation, runtime)
- `2`: Invalid arguments (optional)
### Exception Handling
- Always wrap `Main()` in try-catch for unhandled exceptions.
- Log exceptions before exiting with non-zero code.
- Display user-friendly error messages to stderr.
- Preserve detailed stack traces in log files only.
## Testing Requirements
- Include tests for argument parsing, validation, and edge cases.
- Place CLI tests in module-specific test projects (e.g., `src/modules/[module]/tests/*CliTests.cs`).
## Signing and Deployment
- CLI executables are signed automatically in CI/CD.
- **New CLI tools**: Add your executable and dll to `.pipelines/ESRPSigning_core.json` in the signing list.
- CLI executables are deployed alongside their parent module (e.g., `C:\Program Files\PowerToys\modules\[ModuleName]\`).
- Use self-contained deployment (import `Common.SelfContained.props`).

View File

@@ -58,8 +58,8 @@ string validUIDisplayString = Resources.ValidUIDisplayString;
## More On Coding Guidance
Please review these brief docs below relating to our coding standards, etc.
* [Coding Style](./style.md)
* [Code Organization](./readme.md)
* [Coding Style](development/style.md)
* [Code Organization](readme.md)
[VS Resource Editor]: https://learn.microsoft.com/cpp/windows/resource-editors?view=vs-2019

View File

@@ -57,7 +57,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
## Rules
- **Follow the pattern of what you already see in the code.**
- [Coding style](style.md).
- [Coding style](development/style.md).
- Try to package new functionality/components into libraries that have nicely defined interfaces.
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.

View File

@@ -50,6 +50,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. |
| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. |
| [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI |
| [Open With Antygravity](https://github.com/artickc/PowerToys-Run-Antygravity) | [artickc](https://github.com/artickc) | Open Visual Studio, VS Code recents with Cursor AI |
| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. |
| [QuickAI](https://github.com/ruslanlap/PowerToysRun-QuickAi) | [ruslanlap](https://github.com/ruslanlap) | AI-powered assistance with instant, smart responses from multiple providers (Groq, Together, Fireworks, OpenRouter, Cohere) |

View File

@@ -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);

View File

@@ -466,27 +466,39 @@
TextChanged="EditVariableDialogValueTxtBox_TextChanged"
TextWrapping="Wrap" />
<MenuFlyoutSeparator Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}" />
<ItemsControl
<ListView
x:Name="EditVariableValuesList"
Margin="0,-8,0,12"
HorizontalAlignment="Stretch"
AllowDrop="True"
CanDragItems="True"
CanReorderItems="True"
DragItemsCompleted="EditVariableValuesList_DragItemsCompleted"
ItemsSource="{Binding ValuesList, Mode=TwoWay}"
Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.ItemTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<FontIcon
Grid.Column="0"
Margin="0,0,8,0"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE759;" />
<TextBox
Grid.Column="1"
Background="Transparent"
BorderBrush="Transparent"
LostFocus="EditVariableValuesListTextBox_LostFocus"
Text="{Binding Text}" />
<Button
x:Uid="More_Options_Button"
Grid.Column="1"
Grid.Column="2"
VerticalAlignment="Center"
Content="&#xE712;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
@@ -523,8 +535,8 @@
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</ScrollViewer>
</ContentDialog>

View File

@@ -16,6 +16,8 @@ namespace EnvironmentVariablesUILib
{
public sealed partial class EnvironmentVariablesMainPage : Page
{
private const string ValueListSeparator = ";";
private sealed class RelayCommandParameter
{
public RelayCommandParameter(Variable variable, VariablesSet set)
@@ -440,7 +442,7 @@ namespace EnvironmentVariablesUILib
variable.ValuesList.Move(index, index - 1);
}
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
@@ -461,7 +463,7 @@ namespace EnvironmentVariablesUILib
variable.ValuesList.Move(index, index + 1);
}
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
@@ -476,7 +478,7 @@ namespace EnvironmentVariablesUILib
var variable = EditVariableDialog.DataContext as Variable;
variable.ValuesList.Remove(listItem);
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
@@ -492,7 +494,7 @@ namespace EnvironmentVariablesUILib
var index = variable.ValuesList.IndexOf(listItem);
variable.ValuesList.Insert(index, new Variable.ValuesListItem { Text = string.Empty });
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
@@ -510,7 +512,7 @@ namespace EnvironmentVariablesUILib
var index = variable.ValuesList.IndexOf(listItem);
variable.ValuesList.Insert(index + 1, new Variable.ValuesListItem { Text = string.Empty });
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
@@ -532,7 +534,7 @@ namespace EnvironmentVariablesUILib
listItem.Text = (sender as TextBox)?.Text;
var variable = EditVariableDialog.DataContext as Variable;
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
@@ -548,5 +550,16 @@ namespace EnvironmentVariablesUILib
CancelAddVariable();
ConfirmAddVariableBtn.IsEnabled = false;
}
private void EditVariableValuesList_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
{
if (EditVariableDialog.DataContext is Variable variable && variable.ValuesList != null)
{
var newValues = string.Join(ValueListSeparator, variable.ValuesList.Select(x => x.Text));
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
}
}
}
}

View File

@@ -28,6 +28,13 @@ namespace Awake.Core.Native
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AttachConsole(int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern void FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle);

View File

@@ -49,5 +49,8 @@ namespace Awake.Core.Native
// Menu Item Info Flags
internal const uint MNS_AUTO_DISMISS = 0x10000000;
internal const uint MIM_STYLE = 0x00000010;
// Attach Console
internal const int ATTACH_PARENT_PROCESS = -1;
}
}

View File

@@ -51,6 +51,24 @@ namespace Awake
private static async Task<int> Main(string[] args)
{
var rootCommand = BuildRootCommand();
Bridge.AttachConsole(Core.Native.Constants.ATTACH_PARENT_PROCESS);
var parseResult = rootCommand.Parse(args);
if (parseResult.Tokens.Any(t => t.Value.ToLowerInvariant() is "--help" or "-h" or "-?"))
{
// Print help and exit.
return rootCommand.Invoke(args);
}
if (parseResult.Errors.Count > 0)
{
// Shows errors and returns non-zero.
return rootCommand.Invoke(args);
}
_settingsUtils = SettingsUtils.Default;
LockMutex = new Mutex(true, Core.Constants.AppName, out bool instantiated);
@@ -107,116 +125,97 @@ namespace Awake
Bridge.GetPwrCapabilities(out _powerCapabilities);
Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities, _serializerOptions));
Logger.LogInfo("Parsing parameters...");
Option<bool> configOption = new(_aliasesConfigOption, () => false, Resources.AWAKE_CMD_HELP_CONFIG_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> displayOption = new(_aliasesDisplayOption, () => true, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> timeOption = new(_aliasesTimeOption, () => 0, Resources.AWAKE_CMD_HELP_TIME_OPTION)
{
Arity = ArgumentArity.ExactlyOne,
IsRequired = false,
};
Option<int> pidOption = new(_aliasesPidOption, () => 0, Resources.AWAKE_CMD_HELP_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<string> expireAtOption = new(_aliasesExpireAtOption, () => string.Empty, Resources.AWAKE_CMD_HELP_EXPIRE_AT_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> parentPidOption = new(_aliasesParentPidOption, () => false, Resources.AWAKE_CMD_PARENT_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
timeOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"Interval in --time-limit could not be parsed correctly. Check that the value is valid and doesn't exceed 4,294,967,295. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
pidOption.AddValidator(result =>
{
if (result.Tokens.Count == 0)
{
return;
}
string tokenValue = result.Tokens[0].Value;
if (!int.TryParse(tokenValue, out int parsed))
{
string errorMessage = $"PID value in --pid could not be parsed correctly. Check that the value is valid and falls within the boundaries of Windows PID process limits. Value used: {tokenValue}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
return;
}
if (parsed <= 0)
{
string errorMessage = $"PID value in --pid must be a positive integer. Value used: {parsed}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
return;
}
// Process existence check. (We also re-validate just before binding.)
if (!ProcessExists(parsed))
{
string errorMessage = $"No running process found with an ID of {parsed}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
expireAtOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !DateTimeOffset.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"Date and time value in --expire-at could not be parsed correctly. Check that the value is valid date and time. Refer to https://aka.ms/powertoys/awake for format examples. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
RootCommand? rootCommand =
[
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption,
parentPidOption,
];
rootCommand.Description = Core.Constants.AppName;
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
return rootCommand.InvokeAsync(args).Result;
return await rootCommand.InvokeAsync(args);
}
}
}
private static RootCommand BuildRootCommand()
{
Logger.LogInfo("Parsing parameters...");
Option<bool> configOption = new(_aliasesConfigOption, () => false, Resources.AWAKE_CMD_HELP_CONFIG_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> displayOption = new(_aliasesDisplayOption, () => true, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> timeOption = new(_aliasesTimeOption, () => 0, Resources.AWAKE_CMD_HELP_TIME_OPTION)
{
Arity = ArgumentArity.ExactlyOne,
IsRequired = false,
};
Option<int> pidOption = new(_aliasesPidOption, () => 0, Resources.AWAKE_CMD_HELP_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<string> expireAtOption = new(_aliasesExpireAtOption, () => string.Empty, Resources.AWAKE_CMD_HELP_EXPIRE_AT_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> parentPidOption = new(_aliasesParentPidOption, () => false, Resources.AWAKE_CMD_PARENT_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
timeOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"Interval in --time-limit could not be parsed correctly. Check that the value is valid and doesn't exceed 4,294,967,295. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
pidOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !int.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"PID value in --pid could not be parsed correctly. Check that the value is valid and falls within the boundaries of Windows PID process limits. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
expireAtOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !DateTimeOffset.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"Date and time value in --expire-at could not be parsed correctly. Check that the value is valid date and time. Refer to https://aka.ms/powertoys/awake for format examples. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
RootCommand? rootCommand =
[
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption,
parentPidOption,
];
rootCommand.Description = Core.Constants.AppName;
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
return rootCommand;
}
private static void AwakeUnhandledExceptionCatcher(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception exception)
@@ -264,6 +263,7 @@ namespace Awake
if (pid == 0 && !useParentPid)
{
Logger.LogInfo("No PID specified. Allocating console...");
Bridge.FreeConsole();
AllocateLocalConsole();
}
else

View File

@@ -8,6 +8,7 @@ using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Pages;
@@ -24,15 +25,15 @@ internal sealed partial class FancyZonesMonitorListItem : ListItem
var pickerPage = new FancyZonesMonitorLayoutPickerPage(monitor)
{
Name = "Set active layout",
Name = Resources.FancyZones_SetActiveLayout,
};
MoreCommands =
[
new CommandContextItem(pickerPage)
{
Title = "Set active layout",
Subtitle = "Pick a layout for this monitor",
Title = Resources.FancyZones_SetActiveLayout,
Subtitle = Resources.FancyZones_PickLayoutForMonitor,
},
];
}
@@ -42,14 +43,14 @@ internal sealed partial class FancyZonesMonitorListItem : ListItem
var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString();
var tags = new List<IDetailsElement>
{
DetailTag("Monitor", monitor.Data.Monitor),
DetailTag("Instance", monitor.Data.MonitorInstanceId),
DetailTag("Serial", monitor.Data.MonitorSerialNumber),
DetailTag("Number", monitor.Data.MonitorNumber.ToString(CultureInfo.InvariantCulture)),
DetailTag("Virtual desktop", currentVirtualDesktop),
DetailTag("Work area", $"{monitor.Data.LeftCoordinate},{monitor.Data.TopCoordinate} {monitor.Data.WorkAreaWidth}\u00D7{monitor.Data.WorkAreaHeight}"),
DetailTag("Resolution", $"{monitor.Data.MonitorWidth}\u00D7{monitor.Data.MonitorHeight}"),
DetailTag("DPI", monitor.Data.Dpi.ToString(CultureInfo.InvariantCulture)),
DetailTag(Resources.FancyZones_Monitor, monitor.Data.Monitor),
DetailTag(Resources.FancyZones_Instance, monitor.Data.MonitorInstanceId),
DetailTag(Resources.FancyZones_Serial, monitor.Data.MonitorSerialNumber),
DetailTag(Resources.FancyZones_Number, monitor.Data.MonitorNumber.ToString(CultureInfo.InvariantCulture)),
DetailTag(Resources.FancyZones_VirtualDesktop, currentVirtualDesktop),
DetailTag(Resources.FancyZones_WorkArea, $"{monitor.Data.LeftCoordinate},{monitor.Data.TopCoordinate} {monitor.Data.WorkAreaWidth}\u00D7{monitor.Data.WorkAreaHeight}"),
DetailTag(Resources.FancyZones_Resolution, $"{monitor.Data.MonitorWidth}\u00D7{monitor.Data.MonitorHeight}"),
DetailTag(Resources.FancyZones_DPI, monitor.Data.Dpi.ToString(CultureInfo.InvariantCulture)),
};
return new Details
@@ -68,7 +69,7 @@ internal sealed partial class FancyZonesMonitorListItem : ListItem
Key = key,
Data = new DetailsTags
{
Tags = [new Tag(string.IsNullOrWhiteSpace(value) ? "n/a" : value)],
Tags = [new Tag(string.IsNullOrWhiteSpace(value) ? Resources.Common_NotAvailable : value)],
},
};
}

View File

@@ -5,15 +5,24 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using WorkspacesCsharpLibrary.Data;
namespace PowerToysExtension.Commands;
internal sealed partial class WorkspaceListItem : ListItem
{
private static readonly CompositeFormat ApplicationsFormat = CompositeFormat.Parse(Resources.Workspaces_Applications_Format);
private static readonly CompositeFormat LastLaunchedFormat = CompositeFormat.Parse(Resources.Workspaces_LastLaunched_Format);
private static readonly CompositeFormat ApplicationsCountFormat = CompositeFormat.Parse(Resources.Workspaces_ApplicationsCount_Format);
private static readonly CompositeFormat MinAgoFormat = CompositeFormat.Parse(Resources.Workspaces_MinAgo_Format);
private static readonly CompositeFormat HrAgoFormat = CompositeFormat.Parse(Resources.Workspaces_HrAgo_Format);
private static readonly CompositeFormat DaysAgoFormat = CompositeFormat.Parse(Resources.Workspaces_DaysAgo_Format);
public WorkspaceListItem(ProjectWrapper workspace, IconInfo icon)
: base(new LaunchWorkspaceCommand(workspace.Id))
{
@@ -28,13 +37,13 @@ internal sealed partial class WorkspaceListItem : ListItem
var appCount = workspace.Applications?.Count ?? 0;
var appsText = appCount switch
{
0 => "No applications",
_ => string.Format(CultureInfo.CurrentCulture, "{0} applications", appCount),
0 => Resources.Workspaces_NoApplications,
_ => string.Format(CultureInfo.CurrentCulture, ApplicationsFormat, appCount),
};
var lastLaunched = workspace.LastLaunchedTime > 0
? $"Last launched {FormatRelativeTime(workspace.LastLaunchedTime)}"
: "Never launched";
? string.Format(CultureInfo.CurrentCulture, LastLaunchedFormat, FormatRelativeTime(workspace.LastLaunchedTime))
: Resources.Workspaces_NeverLaunched;
return $"{appsText} \u2022 {lastLaunched}";
}
@@ -44,15 +53,15 @@ internal sealed partial class WorkspaceListItem : ListItem
var appCount = workspace.Applications?.Count ?? 0;
var body = appCount switch
{
0 => "No applications in this workspace",
1 => "1 application",
_ => $"{appCount} applications",
0 => Resources.Workspaces_NoApplicationsInWorkspace,
1 => Resources.Workspaces_OneApplication,
_ => string.Format(CultureInfo.CurrentCulture, ApplicationsCountFormat, appCount),
};
return new Details
{
HeroImage = icon,
Title = workspace.Name ?? "Workspace",
Title = workspace.Name ?? Resources.Workspaces_Workspace,
Body = body,
Metadata = BuildAppMetadata(workspace),
};
@@ -68,7 +77,7 @@ internal sealed partial class WorkspaceListItem : ListItem
var elements = new List<IDetailsElement>();
foreach (var app in workspace.Applications)
{
var appName = string.IsNullOrWhiteSpace(app.Application) ? "App" : app.Application;
var appName = string.IsNullOrWhiteSpace(app.Application) ? Resources.Workspaces_App : app.Application;
var title = string.IsNullOrWhiteSpace(app.Title) ? appName : app.Title;
var tags = new List<ITag>();
@@ -99,19 +108,19 @@ internal sealed partial class WorkspaceListItem : ListItem
if (delta.TotalMinutes < 1)
{
return "just now";
return Resources.Workspaces_JustNow;
}
if (delta.TotalMinutes < 60)
{
return string.Format(CultureInfo.CurrentCulture, "{0} min ago", (int)delta.TotalMinutes);
return string.Format(CultureInfo.CurrentCulture, MinAgoFormat, (int)delta.TotalMinutes);
}
if (delta.TotalHours < 24)
{
return string.Format(CultureInfo.CurrentCulture, "{0} hr ago", (int)delta.TotalHours);
return string.Format(CultureInfo.CurrentCulture, HrAgoFormat, (int)delta.TotalHours);
}
return string.Format(CultureInfo.CurrentCulture, "{0} days ago", (int)delta.TotalDays);
return string.Format(CultureInfo.CurrentCulture, DaysAgoFormat, (int)delta.TotalDays);
}
}

View File

@@ -4,13 +4,16 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using FancyZonesEditorCommon.Data;
using FancyZonesEditorCommon.Utils;
using ManagedCommon;
using PowerToysExtension.Properties;
using FZPaths = FancyZonesEditorCommon.Data.FancyZonesPaths;
@@ -20,6 +23,15 @@ internal static class FancyZonesDataService
{
private const string ZeroUuid = "{00000000-0000-0000-0000-000000000000}";
private static readonly CompositeFormat ReadMonitorDataFailedFormat = CompositeFormat.Parse(Resources.FancyZones_ReadMonitorDataFailed_Format);
private static readonly CompositeFormat WriteAppliedLayoutsFailedFormat = CompositeFormat.Parse(Resources.FancyZones_WriteAppliedLayoutsFailed_Format);
private static readonly CompositeFormat LayoutAppliedNotifyFailedFormat = CompositeFormat.Parse(Resources.FancyZones_LayoutAppliedNotifyFailed_Format);
private static readonly CompositeFormat TemplateFormat = CompositeFormat.Parse(Resources.FancyZones_Template_Format);
private static readonly CompositeFormat ZonesFormat = CompositeFormat.Parse(Resources.FancyZones_Zones_Format);
private static readonly CompositeFormat CustomGridZonesFormat = CompositeFormat.Parse(Resources.FancyZones_CustomGrid_Zones_Format);
private static readonly CompositeFormat CustomCanvasZonesFormat = CompositeFormat.Parse(Resources.FancyZones_CustomCanvas_Zones_Format);
private static readonly CompositeFormat CustomZonesFormat = CompositeFormat.Parse(Resources.FancyZones_Custom_Zones_Format);
public static bool TryGetMonitors(out IReadOnlyList<FancyZonesMonitorDescriptor> monitors, out string error)
{
monitors = Array.Empty<FancyZonesMonitorDescriptor>();
@@ -31,7 +43,7 @@ internal static class FancyZonesDataService
{
if (!File.Exists(FZPaths.EditorParameters))
{
error = "FancyZones monitor data not found. Open FancyZones Editor once to initialize.";
error = Resources.FancyZones_MonitorDataNotFound;
Logger.LogWarning($"TryGetMonitors: File not found. Path={FZPaths.EditorParameters}");
return false;
}
@@ -43,7 +55,7 @@ internal static class FancyZonesDataService
var editorMonitors = editorParams.Monitors;
if (editorMonitors is null || editorMonitors.Count == 0)
{
error = "No FancyZones monitors found.";
error = Resources.FancyZones_NoFancyZonesMonitorsFound;
Logger.LogWarning($"TryGetMonitors: No monitors in file.");
return false;
}
@@ -56,7 +68,7 @@ internal static class FancyZonesDataService
}
catch (Exception ex)
{
error = $"Failed to read FancyZones monitor data: {ex.Message}";
error = string.Format(CultureInfo.CurrentCulture, ReadMonitorDataFailedFormat, ex.Message);
Logger.LogError($"TryGetMonitors: Exception. Message={ex.Message} Stack={ex.StackTrace}");
return false;
}
@@ -204,7 +216,7 @@ internal static class FancyZonesDataService
}
catch (Exception ex)
{
return (false, $"Failed to write applied layouts: {ex.Message}");
return (false, string.Format(CultureInfo.CurrentCulture, WriteAppliedLayoutsFailedFormat, ex.Message));
}
try
@@ -213,10 +225,10 @@ internal static class FancyZonesDataService
}
catch (Exception ex)
{
return (true, $"Layout applied, but FancyZones could not be notified: {ex.Message}");
return (true, string.Format(CultureInfo.CurrentCulture, LayoutAppliedNotifyFailedFormat, ex.Message));
}
return (true, "Layout applied.");
return (true, Resources.FancyZones_LayoutApplied);
}
private static AppliedLayouts.AppliedLayoutWrapper? FindAppliedLayoutEntry(AppliedLayouts.AppliedLayoutsListWrapper file, EditorParameters.NativeMonitorDataWrapper monitor, string virtualDesktopId)
@@ -293,8 +305,8 @@ internal static class FancyZonesDataService
var zoneCount = type.Equals("blank", StringComparison.OrdinalIgnoreCase)
? 0
: template.ZoneCount > 0 ? template.ZoneCount : 3;
var title = $"Template: {type}";
var subtitle = $"{zoneCount} zones";
var title = string.Format(CultureInfo.CurrentCulture, TemplateFormat, type);
var subtitle = string.Format(CultureInfo.CurrentCulture, ZonesFormat, zoneCount);
yield return new FancyZonesLayoutDescriptor
{
@@ -357,9 +369,9 @@ internal static class FancyZonesDataService
var title = custom.Name.Trim();
var subtitle = customType switch
{
"grid" => $"Custom grid \u2022 {applied.ZoneCount} zones",
"canvas" => $"Custom canvas \u2022 {applied.ZoneCount} zones",
_ => $"Custom \u2022 {applied.ZoneCount} zones",
"grid" => string.Format(CultureInfo.CurrentCulture, CustomGridZonesFormat, applied.ZoneCount),
"canvas" => string.Format(CultureInfo.CurrentCulture, CustomCanvasZonesFormat, applied.ZoneCount),
_ => string.Format(CultureInfo.CurrentCulture, CustomZonesFormat, applied.ZoneCount),
};
yield return new FancyZonesLayoutDescriptor

View File

@@ -61,6 +61,21 @@
<ProjectReference Include="..\..\..\Workspaces\Workspaces.ModuleServices\Workspaces.ModuleServices.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<PropertyGroup>
<!-- Always build/publish AOT so the extension ships as native code -->
<SelfContained>true</SelfContained>

View File

@@ -7,6 +7,7 @@ using Common.UI;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Modules;
@@ -22,8 +23,8 @@ internal sealed class AdvancedPasteModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new OpenAdvancedPasteCommand())
{
Title = "Open Advanced Paste",
Subtitle = "Launch the Advanced Paste UI",
Title = Resources.AdvancedPaste_Open_Title,
Subtitle = Resources.AdvancedPaste_Open_Subtitle,
Icon = icon,
};
}
@@ -31,7 +32,7 @@ internal sealed class AdvancedPasteModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Advanced Paste settings",
Subtitle = Resources.AdvancedPaste_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class AlwaysOnTopModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.AlwaysOnTop, title))
{
Title = title,
Subtitle = "Open Always On Top settings",
Subtitle = Resources.AlwaysOnTop_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -10,6 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Modules;
@@ -26,7 +27,7 @@ internal sealed class AwakeModuleCommandProvider : ModuleCommandProvider
items.Add(new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Awake settings",
Subtitle = Resources.Awake_Settings_Subtitle,
Icon = moduleIcon,
});
@@ -49,40 +50,40 @@ internal sealed class AwakeModuleCommandProvider : ModuleCommandProvider
statusItem = new ListItem(new CommandItem(refreshCommand))
{
Title = "Awake: Current status",
Title = Resources.Awake_Status_Title,
Subtitle = AwakeStatusService.GetStatusSubtitle(),
Icon = icon,
};
items.Add(statusItem);
items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake indefinitely", () => AwakeService.Instance.SetIndefiniteAsync(), "Awake set to indefinite", refreshStatus))
items.Add(new ListItem(new StartAwakeCommand(Resources.Awake_KeepIndefinite_Title, () => AwakeService.Instance.SetIndefiniteAsync(), Resources.Awake_SetIndefinite_Toast, refreshStatus))
{
Title = "Awake: Keep awake indefinitely",
Subtitle = "Run Awake in indefinite mode",
Title = Resources.Awake_KeepIndefinite_Title,
Subtitle = Resources.Awake_KeepIndefinite_Subtitle,
Icon = icon,
});
items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 30 minutes", () => AwakeService.Instance.SetTimedAsync(30), "Awake set for 30 minutes", refreshStatus))
items.Add(new ListItem(new StartAwakeCommand(Resources.Awake_Keep30Min_Title, () => AwakeService.Instance.SetTimedAsync(30), Resources.Awake_Set30Min_Toast, refreshStatus))
{
Title = "Awake: Keep awake for 30 minutes",
Subtitle = "Run Awake timed for 30 minutes",
Title = Resources.Awake_Keep30Min_Title,
Subtitle = Resources.Awake_Keep30Min_Subtitle,
Icon = icon,
});
items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 1 hour", () => AwakeService.Instance.SetTimedAsync(60), "Awake set for 1 hour", refreshStatus))
items.Add(new ListItem(new StartAwakeCommand(Resources.Awake_Keep1Hour_Title, () => AwakeService.Instance.SetTimedAsync(60), Resources.Awake_Set1Hour_Toast, refreshStatus))
{
Title = "Awake: Keep awake for 1 hour",
Subtitle = "Run Awake timed for 1 hour",
Title = Resources.Awake_Keep1Hour_Title,
Subtitle = Resources.Awake_Keep1Hour_Subtitle,
Icon = icon,
});
items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 2 hours", () => AwakeService.Instance.SetTimedAsync(120), "Awake set for 2 hours", refreshStatus))
items.Add(new ListItem(new StartAwakeCommand(Resources.Awake_Keep2Hours_Title, () => AwakeService.Instance.SetTimedAsync(120), Resources.Awake_Set2Hours_Toast, refreshStatus))
{
Title = "Awake: Keep awake for 2 hours",
Subtitle = "Run Awake timed for 2 hours",
Title = Resources.Awake_Keep2Hours_Title,
Subtitle = Resources.Awake_Keep2Hours_Subtitle,
Icon = icon,
});
items.Add(new ListItem(new StopAwakeCommand(refreshStatus))
{
Title = "Awake: Turn off",
Subtitle = "Switch Awake back to Off",
Title = Resources.Awake_TurnOff_Title,
Subtitle = Resources.Awake_TurnOff_Subtitle,
Icon = icon,
});

View File

@@ -8,6 +8,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Modules;
@@ -24,7 +25,7 @@ internal sealed class ColorPickerModuleCommandProvider : ModuleCommandProvider
commands.Add(new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Color Picker settings",
Subtitle = Resources.ColorPicker_Settings_Subtitle,
Icon = icon,
});
@@ -36,15 +37,15 @@ internal sealed class ColorPickerModuleCommandProvider : ModuleCommandProvider
// Direct entries in the module list.
commands.Add(new ListItem(new OpenColorPickerCommand())
{
Title = "Open Color Picker",
Subtitle = "Start a color pick session",
Title = Resources.ColorPicker_Open_Title,
Subtitle = Resources.ColorPicker_Open_Subtitle,
Icon = icon,
});
commands.Add(new ListItem(new CommandItem(new ColorPickerSavedColorsPage()))
{
Title = "Saved colors",
Subtitle = "Browse and copy saved colors",
Title = Resources.ColorPicker_SavedColors_Title,
Subtitle = Resources.ColorPicker_SavedColors_Subtitle,
Icon = icon,
});

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class CommandNotFoundModuleCommandProvider : ModuleCommandProvid
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.CmdNotFound, title))
{
Title = title,
Subtitle = "Open Command Not Found settings",
Subtitle = Resources.CommandNotFound_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -22,15 +23,15 @@ internal sealed class CropAndLockModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new CropAndLockReparentCommand())
{
Title = "Crop and Lock (Reparent)",
Subtitle = "Create a cropped reparented window",
Title = Resources.CropAndLock_Reparent_Title,
Subtitle = Resources.CropAndLock_Reparent_Subtitle,
Icon = icon,
};
yield return new ListItem(new CropAndLockThumbnailCommand())
{
Title = "Crop and Lock (Thumbnail)",
Subtitle = "Create a cropped thumbnail window",
Title = Resources.CropAndLock_Thumbnail_Title,
Subtitle = Resources.CropAndLock_Thumbnail_Subtitle,
Icon = icon,
};
}
@@ -38,7 +39,7 @@ internal sealed class CropAndLockModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Crop and Lock settings",
Subtitle = Resources.CropAndLock_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -22,15 +23,15 @@ internal sealed class EnvironmentVariablesModuleCommandProvider : ModuleCommandP
{
yield return new ListItem(new OpenEnvironmentVariablesCommand())
{
Title = "Open Environment Variables",
Subtitle = "Launch Environment Variables editor",
Title = Resources.EnvironmentVariables_Open_Title,
Subtitle = Resources.EnvironmentVariables_Open_Subtitle,
Icon = icon,
};
yield return new ListItem(new OpenEnvironmentVariablesAdminCommand())
{
Title = "Open Environment Variables (Admin)",
Subtitle = "Launch Environment Variables editor as admin",
Title = Resources.EnvironmentVariables_OpenAdmin_Title,
Subtitle = Resources.EnvironmentVariables_OpenAdmin_Subtitle,
Icon = icon,
};
}
@@ -38,7 +39,7 @@ internal sealed class EnvironmentVariablesModuleCommandProvider : ModuleCommandP
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Environment Variables settings",
Subtitle = Resources.EnvironmentVariables_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -7,6 +7,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -23,22 +24,22 @@ internal sealed class FancyZonesModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new CommandItem(new FancyZonesLayoutsPage()))
{
Title = "FancyZones: Layouts",
Subtitle = "Apply a layout to all monitors or a specific monitor",
Title = Resources.FancyZones_Layouts_Title,
Subtitle = Resources.FancyZones_Layouts_Subtitle,
Icon = icon,
};
yield return new ListItem(new CommandItem(new FancyZonesMonitorsPage()))
{
Title = "FancyZones: Monitors",
Subtitle = "Identify monitors and apply layouts",
Title = Resources.FancyZones_Monitors_Title,
Subtitle = Resources.FancyZones_Monitors_Subtitle,
Icon = icon,
};
yield return new ListItem(new OpenFancyZonesEditorCommand())
{
Title = "Open FancyZones Editor",
Subtitle = "Launch layout editor",
Title = Resources.FancyZones_OpenEditor_Title,
Subtitle = Resources.FancyZones_OpenEditor_Subtitle,
Icon = icon,
};
}
@@ -46,7 +47,7 @@ internal sealed class FancyZonesModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open FancyZones settings",
Subtitle = Resources.FancyZones_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class FileExplorerAddonsModuleCommandProvider : ModuleCommandPro
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.FileExplorer, title))
{
Title = title,
Subtitle = "Open File Explorer add-ons settings",
Subtitle = Resources.FileExplorerAddons_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class FileLocksmithModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.FileLocksmith, title))
{
Title = title,
Subtitle = "Open File Locksmith settings",
Subtitle = Resources.FileLocksmith_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -22,15 +23,15 @@ internal sealed class HostsModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new OpenHostsEditorCommand())
{
Title = "Open Hosts File Editor",
Subtitle = "Launch Hosts File Editor",
Title = Resources.Hosts_Open_Title,
Subtitle = Resources.Hosts_Open_Subtitle,
Icon = icon,
};
yield return new ListItem(new OpenHostsEditorAdminCommand())
{
Title = "Open Hosts File Editor (Admin)",
Subtitle = "Launch Hosts File Editor as admin",
Title = Resources.Hosts_OpenAdmin_Title,
Subtitle = Resources.Hosts_OpenAdmin_Subtitle,
Icon = icon,
};
}
@@ -38,7 +39,7 @@ internal sealed class HostsModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Hosts File Editor settings",
Subtitle = Resources.Hosts_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class ImageResizerModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.ImageResizer, title))
{
Title = title,
Subtitle = "Open Image Resizer settings",
Subtitle = Resources.ImageResizer_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.KBM, title))
{
Title = title,
Subtitle = "Open Keyboard Manager settings",
Subtitle = Resources.KeyboardManager_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -24,8 +25,8 @@ internal sealed class LightSwitchModuleCommandProvider : ModuleCommandProvider
{
items.Add(new ListItem(new ToggleLightSwitchCommand())
{
Title = "Toggle Light Switch",
Subtitle = "Toggle system/apps theme immediately",
Title = Resources.LightSwitch_Toggle_Title,
Subtitle = Resources.LightSwitch_Toggle_Subtitle,
Icon = icon,
});
}
@@ -33,7 +34,7 @@ internal sealed class LightSwitchModuleCommandProvider : ModuleCommandProvider
items.Add(new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Light Switch settings",
Subtitle = Resources.LightSwitch_Settings_Subtitle,
Icon = icon,
});

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -22,8 +23,8 @@ internal sealed class MouseUtilsModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new ToggleFindMyMouseCommand())
{
Title = "Trigger Find My Mouse",
Subtitle = "Focus the mouse pointer",
Title = Resources.MouseUtils_FindMyMouse_Title,
Subtitle = Resources.MouseUtils_FindMyMouse_Subtitle,
Icon = icon,
};
}
@@ -32,8 +33,8 @@ internal sealed class MouseUtilsModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new ToggleMouseHighlighterCommand())
{
Title = "Toggle Mouse Highlighter",
Subtitle = "Highlight mouse clicks",
Title = Resources.MouseUtils_Highlighter_Title,
Subtitle = Resources.MouseUtils_Highlighter_Subtitle,
Icon = icon,
};
}
@@ -42,8 +43,8 @@ internal sealed class MouseUtilsModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new ToggleMouseCrosshairsCommand())
{
Title = "Toggle Mouse Crosshairs",
Subtitle = "Enable or disable pointer crosshairs",
Title = Resources.MouseUtils_Crosshairs_Title,
Subtitle = Resources.MouseUtils_Crosshairs_Subtitle,
Icon = icon,
};
}
@@ -52,8 +53,8 @@ internal sealed class MouseUtilsModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new ToggleCursorWrapCommand())
{
Title = "Toggle Cursor Wrap",
Subtitle = "Wrap the cursor across monitor edges",
Title = Resources.MouseUtils_CursorWrap_Title,
Subtitle = Resources.MouseUtils_CursorWrap_Subtitle,
Icon = icon,
};
}
@@ -62,8 +63,8 @@ internal sealed class MouseUtilsModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new ShowMouseJumpPreviewCommand())
{
Title = "Show Mouse Jump Preview",
Subtitle = "Jump the pointer to a target",
Title = Resources.MouseUtils_MouseJump_Title,
Subtitle = Resources.MouseUtils_MouseJump_Subtitle,
Icon = icon,
};
}
@@ -71,7 +72,7 @@ internal sealed class MouseUtilsModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Mouse Utilities settings",
Subtitle = Resources.MouseUtils_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class MouseWithoutBordersModuleCommandProvider : ModuleCommandPr
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.MouseWithoutBorders, title))
{
Title = title,
Subtitle = "Open Mouse Without Borders settings",
Subtitle = Resources.MouseWithoutBorders_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class NewPlusModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.NewPlus, title))
{
Title = title,
Subtitle = "Open New+ settings",
Subtitle = Resources.NewPlus_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class PeekModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.Peek, title))
{
Title = title,
Subtitle = "Open Peek settings",
Subtitle = Resources.Peek_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class PowerRenameModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerRename, title))
{
Title = title,
Subtitle = "Open PowerRename settings",
Subtitle = Resources.PowerRename_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class PowerToysRunModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerLauncher, title))
{
Title = title,
Subtitle = "Open PowerToys Run settings",
Subtitle = Resources.PowerToysRun_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -20,7 +21,7 @@ internal sealed class QuickAccentModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerAccent, title))
{
Title = title,
Subtitle = "Open Quick Accent settings",
Subtitle = Resources.QuickAccent_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -22,8 +23,8 @@ internal sealed class RegistryPreviewModuleCommandProvider : ModuleCommandProvid
{
yield return new ListItem(new OpenRegistryPreviewCommand())
{
Title = "Open Registry Preview",
Subtitle = "Launch Registry Preview",
Title = Resources.RegistryPreview_Open_Title,
Subtitle = Resources.RegistryPreview_Open_Subtitle,
Icon = icon,
};
}
@@ -31,7 +32,7 @@ internal sealed class RegistryPreviewModuleCommandProvider : ModuleCommandProvid
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Registry Preview settings",
Subtitle = Resources.RegistryPreview_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -22,8 +23,8 @@ internal sealed class ScreenRulerModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new ToggleScreenRulerCommand())
{
Title = "Toggle Screen Ruler",
Subtitle = "Start or close Screen Ruler",
Title = Resources.ScreenRuler_Toggle_Title,
Subtitle = Resources.ScreenRuler_Toggle_Subtitle,
Icon = icon,
};
}
@@ -31,7 +32,7 @@ internal sealed class ScreenRulerModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Screen Ruler settings",
Subtitle = Resources.ScreenRuler_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -22,8 +23,8 @@ internal sealed class ShortcutGuideModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new ToggleShortcutGuideCommand())
{
Title = "Toggle Shortcut Guide",
Subtitle = "Show or hide Shortcut Guide",
Title = Resources.ShortcutGuide_Toggle_Title,
Subtitle = Resources.ShortcutGuide_Toggle_Subtitle,
Icon = icon,
};
}
@@ -31,7 +32,7 @@ internal sealed class ShortcutGuideModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Shortcut Guide settings",
Subtitle = Resources.ShortcutGuide_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -22,8 +23,8 @@ internal sealed class TextExtractorModuleCommandProvider : ModuleCommandProvider
{
yield return new ListItem(new ToggleTextExtractorCommand())
{
Title = "Toggle Text Extractor",
Subtitle = "Start or close Text Extractor",
Title = Resources.TextExtractor_Toggle_Title,
Subtitle = Resources.TextExtractor_Toggle_Subtitle,
Icon = icon,
};
}
@@ -31,7 +32,7 @@ internal sealed class TextExtractorModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Text Extractor settings",
Subtitle = Resources.TextExtractor_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -7,6 +7,7 @@ using Common.UI;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using Workspaces.ModuleServices;
using WorkspacesCsharpLibrary.Data;
@@ -25,7 +26,7 @@ internal sealed class WorkspacesModuleCommandProvider : ModuleCommandProvider
items.Add(new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open Workspaces settings",
Subtitle = Resources.Workspaces_Settings_Subtitle,
Icon = moduleIcon,
});
@@ -37,8 +38,8 @@ internal sealed class WorkspacesModuleCommandProvider : ModuleCommandProvider
// Settings entry plus common actions.
items.Add(new ListItem(new OpenWorkspaceEditorCommand())
{
Title = "Workspaces: Open editor",
Subtitle = "Create or edit workspaces",
Title = Resources.Workspaces_OpenEditor_Title,
Subtitle = Resources.Workspaces_OpenEditor_Subtitle,
Icon = icon,
});

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
namespace PowerToysExtension.Modules;
@@ -21,40 +22,40 @@ internal sealed class ZoomItModuleCommandProvider : ModuleCommandProvider
if (ModuleEnablementService.IsModuleEnabled(module))
{
// Action commands via ZoomIt IPC
yield return new ListItem(new ZoomItActionCommand("zoom", "ZoomIt: Zoom"))
yield return new ListItem(new ZoomItActionCommand("zoom", Resources.ZoomIt_Zoom_Title))
{
Title = "ZoomIt: Zoom",
Subtitle = "Enter zoom mode",
Title = Resources.ZoomIt_Zoom_Title,
Subtitle = Resources.ZoomIt_Zoom_Subtitle,
Icon = icon,
};
yield return new ListItem(new ZoomItActionCommand("draw", "ZoomIt: Draw"))
yield return new ListItem(new ZoomItActionCommand("draw", Resources.ZoomIt_Draw_Title))
{
Title = "ZoomIt: Draw",
Subtitle = "Enter drawing mode",
Title = Resources.ZoomIt_Draw_Title,
Subtitle = Resources.ZoomIt_Draw_Subtitle,
Icon = icon,
};
yield return new ListItem(new ZoomItActionCommand("break", "ZoomIt: Break"))
yield return new ListItem(new ZoomItActionCommand("break", Resources.ZoomIt_Break_Title))
{
Title = "ZoomIt: Break",
Subtitle = "Enter break timer",
Title = Resources.ZoomIt_Break_Title,
Subtitle = Resources.ZoomIt_Break_Subtitle,
Icon = icon,
};
yield return new ListItem(new ZoomItActionCommand("liveZoom", "ZoomIt: Live Zoom"))
yield return new ListItem(new ZoomItActionCommand("liveZoom", Resources.ZoomIt_LiveZoom_Title))
{
Title = "ZoomIt: Live Zoom",
Subtitle = "Toggle live zoom",
Title = Resources.ZoomIt_LiveZoom_Title,
Subtitle = Resources.ZoomIt_LiveZoom_Subtitle,
Icon = icon,
};
yield return new ListItem(new ZoomItActionCommand("snip", "ZoomIt: Snip"))
yield return new ListItem(new ZoomItActionCommand("snip", Resources.ZoomIt_Snip_Title))
{
Title = "ZoomIt: Snip",
Subtitle = "Enter snip mode",
Title = Resources.ZoomIt_Snip_Title,
Subtitle = Resources.ZoomIt_Snip_Subtitle,
Icon = icon,
};
yield return new ListItem(new ZoomItActionCommand("record", "ZoomIt: Record"))
yield return new ListItem(new ZoomItActionCommand("record", Resources.ZoomIt_Record_Title))
{
Title = "ZoomIt: Record",
Subtitle = "Start recording",
Title = Resources.ZoomIt_Record_Title,
Subtitle = Resources.ZoomIt_Record_Subtitle,
Icon = icon,
};
}
@@ -62,7 +63,7 @@ internal sealed class ZoomItModuleCommandProvider : ModuleCommandProvider
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open ZoomIt settings",
Subtitle = Resources.ZoomIt_Settings_Subtitle,
Icon = icon,
};
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using ColorPicker.ModuleServices;
@@ -10,24 +11,27 @@ using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Pages;
internal sealed partial class ColorPickerSavedColorsPage : DynamicListPage
{
private static readonly CompositeFormat NoMatchingSavedColorsFormat = CompositeFormat.Parse(Resources.ColorPicker_NoMatchingSavedColors_Subtitle);
private readonly CommandItem _emptyContent;
public ColorPickerSavedColorsPage()
{
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("ColorPicker.png");
Title = "Saved colors";
Title = Resources.ColorPicker_SavedColors_Title;
Name = "ColorPickerSavedColors";
Id = "com.microsoft.powertoys.colorpicker.savedColors";
_emptyContent = new CommandItem()
{
Title = "No saved colors",
Subtitle = "Pick a color first, then try again.",
Title = Resources.ColorPicker_NoSavedColors_Title,
Subtitle = Resources.ColorPicker_NoSavedColors_Subtitle,
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("ColorPicker.png"),
};
@@ -70,8 +74,8 @@ internal sealed partial class ColorPickerSavedColorsPage : DynamicListPage
public override void UpdateSearchText(string oldSearch, string newSearch)
{
_emptyContent.Subtitle = string.IsNullOrWhiteSpace(newSearch)
? "Pick a color first, then try again."
: $"No saved colors matching '{newSearch}'";
? Resources.ColorPicker_NoSavedColors_Subtitle
: string.Format(CultureInfo.CurrentCulture, NoMatchingSavedColorsFormat, newSearch);
RaiseItemsChanged(0);
}

View File

@@ -11,6 +11,7 @@ using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Pages;
@@ -21,13 +22,13 @@ internal sealed partial class FancyZonesLayoutsPage : DynamicListPage
public FancyZonesLayoutsPage()
{
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
Name = Title = "FancyZones Layouts";
Name = Title = Resources.FancyZones_Layouts_Title;
Id = "com.microsoft.cmdpal.powertoys.fancyzones.layouts";
_emptyMessage = new CommandItem()
{
Title = "No layouts found",
Subtitle = "Open FancyZones Editor once to initialize layouts.",
Title = Resources.FancyZones_NoLayoutsFound_Title,
Subtitle = Resources.FancyZones_NoLayoutsFound_Subtitle,
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"),
};
EmptyContent = _emptyMessage;

View File

@@ -4,16 +4,21 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Pages;
internal sealed partial class FancyZonesMonitorLayoutPickerPage : DynamicListPage
{
private static readonly CompositeFormat SetActiveLayoutForFormat = CompositeFormat.Parse(Resources.FancyZones_SetActiveLayoutFor_Format);
private readonly FancyZonesMonitorDescriptor _monitor;
private readonly CommandItem _emptyMessage;
@@ -21,13 +26,13 @@ internal sealed partial class FancyZonesMonitorLayoutPickerPage : DynamicListPag
{
_monitor = monitor;
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
Name = Title = $"Set active layout for {_monitor.Title}";
Name = Title = string.Format(CultureInfo.CurrentCulture, SetActiveLayoutForFormat, _monitor.Title);
Id = $"com.microsoft.cmdpal.powertoys.fancyzones.monitor.{_monitor.Index}.layouts";
_emptyMessage = new CommandItem()
{
Title = "No layouts found",
Subtitle = "Open FancyZones Editor once to initialize layouts.",
Title = Resources.FancyZones_NoLayoutsFound_Title,
Subtitle = Resources.FancyZones_NoLayoutsFound_Subtitle,
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"),
};
EmptyContent = _emptyMessage;

View File

@@ -4,26 +4,31 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Pages;
internal sealed partial class FancyZonesMonitorsPage : DynamicListPage
{
private static readonly CompositeFormat CurrentLayoutFormat = CompositeFormat.Parse(Resources.FancyZones_CurrentLayout_Format);
private readonly CommandItem _emptyMessage;
public FancyZonesMonitorsPage()
{
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
Name = Title = "FancyZones Monitors";
Name = Title = Resources.FancyZones_Monitors_Title;
Id = "com.microsoft.cmdpal.powertoys.fancyzones.monitors";
_emptyMessage = new CommandItem()
{
Title = "No monitors found",
Subtitle = "Open FancyZones Editor once to initialize monitor data.",
Title = Resources.FancyZones_NoMonitorsFound_Title,
Subtitle = Resources.FancyZones_NoMonitorsFound_Subtitle,
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"),
};
EmptyContent = _emptyMessage;
@@ -55,8 +60,8 @@ internal sealed partial class FancyZonesMonitorsPage : DynamicListPage
}
var layoutDescription = FancyZonesDataService.TryGetAppliedLayoutForMonitor(monitor.Data, out var applied) && applied is not null
? $"Current layout: {applied.Value.Type}"
: "Current layout: unknown";
? string.Format(CultureInfo.CurrentCulture, CurrentLayoutFormat, applied.Value.Type)
: Resources.FancyZones_CurrentLayout_Unknown;
var item = new FancyZonesMonitorListItem(monitor, layoutDescription, monitorIcon);
items.Add(item);

View File

@@ -6,6 +6,7 @@ using Awake.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Properties;
namespace PowerToysExtension;
@@ -14,32 +15,32 @@ internal sealed partial class PowerToysExtensionPage : ListPage
public PowerToysExtensionPage()
{
Icon = Helpers.PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
Title = "PowerToys";
Name = "PowerToys commands";
Title = Resources.PowerToys_DisplayName;
Name = Resources.PowerToysExtension_CommandsName;
}
public override IListItem[] GetItems()
{
return [
new ListItem(new LaunchModuleCommand("PowerToys", executableName: "PowerToys.exe", displayName: "Open PowerToys"))
new ListItem(new LaunchModuleCommand("PowerToys", executableName: "PowerToys.exe", displayName: Resources.PowerToysExtension_OpenPowerToys_Title))
{
Title = "Open PowerToys",
Subtitle = "Launch the PowerToys shell",
Title = Resources.PowerToysExtension_OpenPowerToys_Title,
Subtitle = Resources.PowerToysExtension_OpenPowerToys_Subtitle,
},
new ListItem(new OpenPowerToysSettingsCommand("PowerToys", "General"))
{
Title = "Open PowerToys settings",
Subtitle = "Open the main PowerToys settings window",
Title = Resources.PowerToysExtension_OpenSettings_Title,
Subtitle = Resources.PowerToysExtension_OpenSettings_Subtitle,
},
new ListItem(new OpenPowerToysSettingsCommand("Workspaces", "Workspaces"))
{
Title = "Open Workspaces settings",
Subtitle = "Jump directly to Workspaces settings",
Title = Resources.PowerToysExtension_OpenWorkspacesSettings_Title,
Subtitle = Resources.PowerToysExtension_OpenWorkspacesSettings_Subtitle,
},
new ListItem(new OpenWorkspaceEditorCommand())
{
Title = "Open Workspaces editor",
Subtitle = "Launch the Workspaces editor",
Title = Resources.PowerToysExtension_OpenWorkspacesEditor_Title,
Subtitle = Resources.PowerToysExtension_OpenWorkspacesEditor_Subtitle,
},
];
}

View File

@@ -5,6 +5,7 @@
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Pages;
@@ -15,13 +16,13 @@ internal sealed partial class PowerToysListPage : ListPage
public PowerToysListPage()
{
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
Name = Title = "PowerToys";
Name = Title = Resources.PowerToys_DisplayName;
Id = "com.microsoft.cmdpal.powertoys";
SettingsChangeNotifier.SettingsChanged += OnSettingsChanged;
_empty = new CommandItem()
{
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"),
Title = "No matching module found",
Title = Resources.PowerToys_NoMatchingModule,
Subtitle = SearchText,
};
EmptyContent = _empty;

View File

@@ -7,6 +7,7 @@ using ManagedCommon;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension;
@@ -14,7 +15,7 @@ public sealed partial class PowerToysCommandsProvider : CommandProvider
{
public PowerToysCommandsProvider()
{
DisplayName = "PowerToys";
DisplayName = Resources.PowerToys_DisplayName;
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
}
@@ -22,8 +23,8 @@ public sealed partial class PowerToysCommandsProvider : CommandProvider
[
new CommandItem(new Pages.PowerToysListPage())
{
Title = "PowerToys",
Subtitle = "PowerToys commands and settings",
Title = Resources.PowerToys_DisplayName,
Subtitle = Resources.PowerToys_Subtitle,
}
];

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Properties;
namespace PowerToysExtension;
@@ -15,13 +16,13 @@ public partial class PowerToysExtensionCommandsProvider : CommandProvider
public PowerToysExtensionCommandsProvider()
{
DisplayName = "PowerToys";
DisplayName = Resources.PowerToys_DisplayName;
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
_commands = [
new CommandItem(new Pages.PowerToysListPage())
{
Title = "PowerToys",
Subtitle = "PowerToys commands and settings",
Title = Resources.PowerToys_DisplayName,
Subtitle = Resources.PowerToys_Subtitle,
},
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,630 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- PowerToys CommandsProvider -->
<data name="PowerToys_DisplayName" xml:space="preserve">
<value>PowerToys</value>
</data>
<data name="PowerToys_Subtitle" xml:space="preserve">
<value>PowerToys commands and settings</value>
</data>
<data name="PowerToys_NoMatchingModule" xml:space="preserve">
<value>No matching module found</value>
</data>
<!-- PowerToys Extension Page -->
<data name="PowerToysExtension_CommandsName" xml:space="preserve">
<value>PowerToys commands</value>
</data>
<data name="PowerToysExtension_OpenPowerToys_Title" xml:space="preserve">
<value>Open PowerToys</value>
</data>
<data name="PowerToysExtension_OpenPowerToys_Subtitle" xml:space="preserve">
<value>Launch the PowerToys shell</value>
</data>
<data name="PowerToysExtension_OpenSettings_Title" xml:space="preserve">
<value>Open PowerToys settings</value>
</data>
<data name="PowerToysExtension_OpenSettings_Subtitle" xml:space="preserve">
<value>Open the main PowerToys settings window</value>
</data>
<data name="PowerToysExtension_OpenWorkspacesSettings_Title" xml:space="preserve">
<value>Open Workspaces settings</value>
</data>
<data name="PowerToysExtension_OpenWorkspacesSettings_Subtitle" xml:space="preserve">
<value>Jump directly to Workspaces settings</value>
</data>
<data name="PowerToysExtension_OpenWorkspacesEditor_Title" xml:space="preserve">
<value>Open Workspaces editor</value>
</data>
<data name="PowerToysExtension_OpenWorkspacesEditor_Subtitle" xml:space="preserve">
<value>Launch the Workspaces editor</value>
</data>
<!-- Advanced Paste Module -->
<data name="AdvancedPaste_Open_Title" xml:space="preserve">
<value>Open Advanced Paste</value>
</data>
<data name="AdvancedPaste_Open_Subtitle" xml:space="preserve">
<value>Launch the Advanced Paste UI</value>
</data>
<data name="AdvancedPaste_Settings_Subtitle" xml:space="preserve">
<value>Open Advanced Paste settings</value>
</data>
<!-- Always On Top Module -->
<data name="AlwaysOnTop_Settings_Subtitle" xml:space="preserve">
<value>Open Always On Top settings</value>
</data>
<!-- Awake Module -->
<data name="Awake_Settings_Subtitle" xml:space="preserve">
<value>Open Awake settings</value>
</data>
<data name="Awake_Status_Title" xml:space="preserve">
<value>Awake: Current status</value>
</data>
<data name="Awake_KeepIndefinite_Title" xml:space="preserve">
<value>Awake: Keep awake indefinitely</value>
</data>
<data name="Awake_KeepIndefinite_Subtitle" xml:space="preserve">
<value>Run Awake in indefinite mode</value>
</data>
<data name="Awake_Keep30Min_Title" xml:space="preserve">
<value>Awake: Keep awake for 30 minutes</value>
</data>
<data name="Awake_Keep30Min_Subtitle" xml:space="preserve">
<value>Run Awake timed for 30 minutes</value>
</data>
<data name="Awake_Keep1Hour_Title" xml:space="preserve">
<value>Awake: Keep awake for 1 hour</value>
</data>
<data name="Awake_Keep1Hour_Subtitle" xml:space="preserve">
<value>Run Awake timed for 1 hour</value>
</data>
<data name="Awake_Keep2Hours_Title" xml:space="preserve">
<value>Awake: Keep awake for 2 hours</value>
</data>
<data name="Awake_Keep2Hours_Subtitle" xml:space="preserve">
<value>Run Awake timed for 2 hours</value>
</data>
<data name="Awake_TurnOff_Title" xml:space="preserve">
<value>Awake: Turn off</value>
</data>
<data name="Awake_TurnOff_Subtitle" xml:space="preserve">
<value>Switch Awake back to Off</value>
</data>
<data name="Awake_SetIndefinite_Toast" xml:space="preserve">
<value>Awake set to indefinite</value>
</data>
<data name="Awake_Set30Min_Toast" xml:space="preserve">
<value>Awake set for 30 minutes</value>
</data>
<data name="Awake_Set1Hour_Toast" xml:space="preserve">
<value>Awake set for 1 hour</value>
</data>
<data name="Awake_Set2Hours_Toast" xml:space="preserve">
<value>Awake set for 2 hours</value>
</data>
<!-- Color Picker Module -->
<data name="ColorPicker_Settings_Subtitle" xml:space="preserve">
<value>Open Color Picker settings</value>
</data>
<data name="ColorPicker_Open_Title" xml:space="preserve">
<value>Open Color Picker</value>
</data>
<data name="ColorPicker_Open_Subtitle" xml:space="preserve">
<value>Start a color pick session</value>
</data>
<data name="ColorPicker_SavedColors_Title" xml:space="preserve">
<value>Saved colors</value>
</data>
<data name="ColorPicker_SavedColors_Subtitle" xml:space="preserve">
<value>Browse and copy saved colors</value>
</data>
<data name="ColorPicker_NoSavedColors_Title" xml:space="preserve">
<value>No saved colors</value>
</data>
<data name="ColorPicker_NoSavedColors_Subtitle" xml:space="preserve">
<value>Pick a color first, then try again.</value>
</data>
<data name="ColorPicker_NoMatchingSavedColors_Subtitle" xml:space="preserve">
<value>No saved colors matching '{0}'</value>
</data>
<!-- Command Not Found Module -->
<data name="CommandNotFound_Settings_Subtitle" xml:space="preserve">
<value>Open Command Not Found settings</value>
</data>
<!-- Crop And Lock Module -->
<data name="CropAndLock_Reparent_Title" xml:space="preserve">
<value>Crop and Lock (Reparent)</value>
</data>
<data name="CropAndLock_Reparent_Subtitle" xml:space="preserve">
<value>Create a cropped reparented window</value>
</data>
<data name="CropAndLock_Thumbnail_Title" xml:space="preserve">
<value>Crop and Lock (Thumbnail)</value>
</data>
<data name="CropAndLock_Thumbnail_Subtitle" xml:space="preserve">
<value>Create a cropped thumbnail window</value>
</data>
<data name="CropAndLock_Settings_Subtitle" xml:space="preserve">
<value>Open Crop and Lock settings</value>
</data>
<!-- Environment Variables Module -->
<data name="EnvironmentVariables_Open_Title" xml:space="preserve">
<value>Open Environment Variables</value>
</data>
<data name="EnvironmentVariables_Open_Subtitle" xml:space="preserve">
<value>Launch Environment Variables editor</value>
</data>
<data name="EnvironmentVariables_OpenAdmin_Title" xml:space="preserve">
<value>Open Environment Variables (Admin)</value>
</data>
<data name="EnvironmentVariables_OpenAdmin_Subtitle" xml:space="preserve">
<value>Launch Environment Variables editor as admin</value>
</data>
<data name="EnvironmentVariables_Settings_Subtitle" xml:space="preserve">
<value>Open Environment Variables settings</value>
</data>
<!-- FancyZones Module -->
<data name="FancyZones_Layouts_Title" xml:space="preserve">
<value>FancyZones: Layouts</value>
</data>
<data name="FancyZones_Layouts_Subtitle" xml:space="preserve">
<value>Apply a layout to all monitors or a specific monitor</value>
</data>
<data name="FancyZones_Monitors_Title" xml:space="preserve">
<value>FancyZones: Monitors</value>
</data>
<data name="FancyZones_Monitors_Subtitle" xml:space="preserve">
<value>Identify monitors and apply layouts</value>
</data>
<data name="FancyZones_OpenEditor_Title" xml:space="preserve">
<value>Open FancyZones Editor</value>
</data>
<data name="FancyZones_OpenEditor_Subtitle" xml:space="preserve">
<value>Launch layout editor</value>
</data>
<data name="FancyZones_Settings_Subtitle" xml:space="preserve">
<value>Open FancyZones settings</value>
</data>
<data name="FancyZones_LayoutsPage_Title" xml:space="preserve">
<value>FancyZones Layouts</value>
</data>
<data name="FancyZones_NoLayoutsFound_Title" xml:space="preserve">
<value>No layouts found</value>
</data>
<data name="FancyZones_NoLayoutsFound_Subtitle" xml:space="preserve">
<value>Open FancyZones Editor once to initialize layouts.</value>
</data>
<data name="FancyZones_ApplyTo_Format" xml:space="preserve">
<value>Apply to {0}</value>
</data>
<data name="FancyZones_MonitorsPage_Title" xml:space="preserve">
<value>FancyZones Monitors</value>
</data>
<data name="FancyZones_NoMonitorsFound_Title" xml:space="preserve">
<value>No monitors found</value>
</data>
<data name="FancyZones_NoMonitorsFound_Subtitle" xml:space="preserve">
<value>Open FancyZones Editor once to initialize monitor data.</value>
</data>
<data name="FancyZones_SetActiveLayout" xml:space="preserve">
<value>Set active layout</value>
</data>
<data name="FancyZones_PickLayoutForMonitor" xml:space="preserve">
<value>Pick a layout for this monitor</value>
</data>
<data name="FancyZones_SetActiveLayoutFor_Format" xml:space="preserve">
<value>Set active layout for {0}</value>
</data>
<data name="FancyZones_CurrentLayout_Format" xml:space="preserve">
<value>Current layout: {0}</value>
</data>
<data name="FancyZones_CurrentLayout_Unknown" xml:space="preserve">
<value>Current layout: unknown</value>
</data>
<data name="FancyZones_Template_Format" xml:space="preserve">
<value>Template: {0}</value>
</data>
<data name="FancyZones_Zones_Format" xml:space="preserve">
<value>{0} zones</value>
</data>
<data name="FancyZones_CustomGrid_Zones_Format" xml:space="preserve">
<value>Custom grid • {0} zones</value>
</data>
<data name="FancyZones_CustomCanvas_Zones_Format" xml:space="preserve">
<value>Custom canvas • {0} zones</value>
</data>
<data name="FancyZones_Custom_Zones_Format" xml:space="preserve">
<value>Custom • {0} zones</value>
</data>
<data name="FancyZones_LayoutApplied" xml:space="preserve">
<value>Layout applied.</value>
</data>
<data name="FancyZones_LayoutAppliedNotifyFailed_Format" xml:space="preserve">
<value>Layout applied, but FancyZones could not be notified: {0}</value>
</data>
<data name="FancyZones_WriteAppliedLayoutsFailed_Format" xml:space="preserve">
<value>Failed to write applied layouts: {0}</value>
</data>
<data name="FancyZones_MonitorDataNotFound" xml:space="preserve">
<value>FancyZones monitor data not found. Open FancyZones Editor once to initialize.</value>
</data>
<data name="FancyZones_NoFancyZonesMonitorsFound" xml:space="preserve">
<value>No FancyZones monitors found.</value>
</data>
<data name="FancyZones_ReadMonitorDataFailed_Format" xml:space="preserve">
<value>Failed to read FancyZones monitor data: {0}</value>
</data>
<!-- File Explorer Addons Module -->
<data name="FileExplorerAddons_Settings_Subtitle" xml:space="preserve">
<value>Open File Explorer add-ons settings</value>
</data>
<!-- File Locksmith Module -->
<data name="FileLocksmith_Settings_Subtitle" xml:space="preserve">
<value>Open File Locksmith settings</value>
</data>
<!-- Hosts Module -->
<data name="Hosts_Open_Title" xml:space="preserve">
<value>Open Hosts File Editor</value>
</data>
<data name="Hosts_Open_Subtitle" xml:space="preserve">
<value>Launch Hosts File Editor</value>
</data>
<data name="Hosts_OpenAdmin_Title" xml:space="preserve">
<value>Open Hosts File Editor (Admin)</value>
</data>
<data name="Hosts_OpenAdmin_Subtitle" xml:space="preserve">
<value>Launch Hosts File Editor as admin</value>
</data>
<data name="Hosts_Settings_Subtitle" xml:space="preserve">
<value>Open Hosts File Editor settings</value>
</data>
<!-- Image Resizer Module -->
<data name="ImageResizer_Settings_Subtitle" xml:space="preserve">
<value>Open Image Resizer settings</value>
</data>
<!-- Keyboard Manager Module -->
<data name="KeyboardManager_Settings_Subtitle" xml:space="preserve">
<value>Open Keyboard Manager settings</value>
</data>
<!-- Light Switch Module -->
<data name="LightSwitch_Toggle_Title" xml:space="preserve">
<value>Light Switch: Toggle theme</value>
</data>
<data name="LightSwitch_Toggle_Subtitle" xml:space="preserve">
<value>Toggle system/apps theme immediately</value>
</data>
<data name="LightSwitch_Settings_Subtitle" xml:space="preserve">
<value>Open Light Switch settings</value>
</data>
<!-- Mouse Utils Module -->
<data name="MouseUtils_FindMyMouse_Title" xml:space="preserve">
<value>Trigger Find My Mouse</value>
</data>
<data name="MouseUtils_FindMyMouse_Subtitle" xml:space="preserve">
<value>Focus the mouse pointer</value>
</data>
<data name="MouseUtils_Highlighter_Title" xml:space="preserve">
<value>Toggle Mouse Highlighter</value>
</data>
<data name="MouseUtils_Highlighter_Subtitle" xml:space="preserve">
<value>Highlight mouse clicks</value>
</data>
<data name="MouseUtils_Crosshairs_Title" xml:space="preserve">
<value>Toggle Mouse Crosshairs</value>
</data>
<data name="MouseUtils_Crosshairs_Subtitle" xml:space="preserve">
<value>Enable or disable pointer crosshairs</value>
</data>
<data name="MouseUtils_CursorWrap_Title" xml:space="preserve">
<value>Toggle Cursor Wrap</value>
</data>
<data name="MouseUtils_CursorWrap_Subtitle" xml:space="preserve">
<value>Wrap the cursor across monitor edges</value>
</data>
<data name="MouseUtils_MouseJump_Title" xml:space="preserve">
<value>Show Mouse Jump Preview</value>
</data>
<data name="MouseUtils_MouseJump_Subtitle" xml:space="preserve">
<value>Jump the pointer to a target</value>
</data>
<data name="MouseUtils_Settings_Subtitle" xml:space="preserve">
<value>Open Mouse Utilities settings</value>
</data>
<!-- Mouse Without Borders Module -->
<data name="MouseWithoutBorders_Settings_Subtitle" xml:space="preserve">
<value>Open Mouse Without Borders settings</value>
</data>
<!-- New+ Module -->
<data name="NewPlus_Settings_Subtitle" xml:space="preserve">
<value>Open New+ settings</value>
</data>
<!-- Peek Module -->
<data name="Peek_Settings_Subtitle" xml:space="preserve">
<value>Open Peek settings</value>
</data>
<!-- PowerRename Module -->
<data name="PowerRename_Settings_Subtitle" xml:space="preserve">
<value>Open PowerRename settings</value>
</data>
<!-- PowerToys Run Module -->
<data name="PowerToysRun_Settings_Subtitle" xml:space="preserve">
<value>Open PowerToys Run settings</value>
</data>
<!-- Quick Accent Module -->
<data name="QuickAccent_Settings_Subtitle" xml:space="preserve">
<value>Open Quick Accent settings</value>
</data>
<!-- Registry Preview Module -->
<data name="RegistryPreview_Open_Title" xml:space="preserve">
<value>Open Registry Preview</value>
</data>
<data name="RegistryPreview_Open_Subtitle" xml:space="preserve">
<value>Launch Registry Preview</value>
</data>
<data name="RegistryPreview_Settings_Subtitle" xml:space="preserve">
<value>Open Registry Preview settings</value>
</data>
<!-- Screen Ruler Module -->
<data name="ScreenRuler_Toggle_Title" xml:space="preserve">
<value>Toggle Screen Ruler</value>
</data>
<data name="ScreenRuler_Toggle_Subtitle" xml:space="preserve">
<value>Start or close Screen Ruler</value>
</data>
<data name="ScreenRuler_Settings_Subtitle" xml:space="preserve">
<value>Open Screen Ruler settings</value>
</data>
<!-- Shortcut Guide Module -->
<data name="ShortcutGuide_Toggle_Title" xml:space="preserve">
<value>Toggle Shortcut Guide</value>
</data>
<data name="ShortcutGuide_Toggle_Subtitle" xml:space="preserve">
<value>Show or hide Shortcut Guide</value>
</data>
<data name="ShortcutGuide_Settings_Subtitle" xml:space="preserve">
<value>Open Shortcut Guide settings</value>
</data>
<!-- Text Extractor Module -->
<data name="TextExtractor_Toggle_Title" xml:space="preserve">
<value>Toggle Text Extractor</value>
</data>
<data name="TextExtractor_Toggle_Subtitle" xml:space="preserve">
<value>Start or close Text Extractor</value>
</data>
<data name="TextExtractor_Settings_Subtitle" xml:space="preserve">
<value>Open Text Extractor settings</value>
</data>
<!-- Workspaces Module -->
<data name="Workspaces_Settings_Subtitle" xml:space="preserve">
<value>Open Workspaces settings</value>
</data>
<data name="Workspaces_OpenEditor_Title" xml:space="preserve">
<value>Workspaces: Open editor</value>
</data>
<data name="Workspaces_OpenEditor_Subtitle" xml:space="preserve">
<value>Create or edit workspaces</value>
</data>
<data name="Workspaces_NoApplications" xml:space="preserve">
<value>No applications</value>
</data>
<data name="Workspaces_Applications_Format" xml:space="preserve">
<value>{0} applications</value>
</data>
<data name="Workspaces_LastLaunched_Format" xml:space="preserve">
<value>Last launched {0}</value>
</data>
<data name="Workspaces_NeverLaunched" xml:space="preserve">
<value>Never launched</value>
</data>
<data name="Workspaces_NoApplicationsInWorkspace" xml:space="preserve">
<value>No applications in this workspace</value>
</data>
<data name="Workspaces_OneApplication" xml:space="preserve">
<value>1 application</value>
</data>
<data name="Workspaces_ApplicationsCount_Format" xml:space="preserve">
<value>{0} applications</value>
</data>
<data name="Workspaces_Workspace" xml:space="preserve">
<value>Workspace</value>
</data>
<data name="Workspaces_App" xml:space="preserve">
<value>App</value>
</data>
<data name="Workspaces_JustNow" xml:space="preserve">
<value>just now</value>
</data>
<data name="Workspaces_MinAgo_Format" xml:space="preserve">
<value>{0} min ago</value>
</data>
<data name="Workspaces_HrAgo_Format" xml:space="preserve">
<value>{0} hr ago</value>
</data>
<data name="Workspaces_DaysAgo_Format" xml:space="preserve">
<value>{0} days ago</value>
</data>
<!-- ZoomIt Module -->
<data name="ZoomIt_Zoom_Title" xml:space="preserve">
<value>ZoomIt: Zoom</value>
</data>
<data name="ZoomIt_Zoom_Subtitle" xml:space="preserve">
<value>Enter zoom mode</value>
</data>
<data name="ZoomIt_Draw_Title" xml:space="preserve">
<value>ZoomIt: Draw</value>
</data>
<data name="ZoomIt_Draw_Subtitle" xml:space="preserve">
<value>Enter drawing mode</value>
</data>
<data name="ZoomIt_Break_Title" xml:space="preserve">
<value>ZoomIt: Break</value>
</data>
<data name="ZoomIt_Break_Subtitle" xml:space="preserve">
<value>Enter break timer</value>
</data>
<data name="ZoomIt_LiveZoom_Title" xml:space="preserve">
<value>ZoomIt: Live Zoom</value>
</data>
<data name="ZoomIt_LiveZoom_Subtitle" xml:space="preserve">
<value>Toggle live zoom</value>
</data>
<data name="ZoomIt_Snip_Title" xml:space="preserve">
<value>ZoomIt: Snip</value>
</data>
<data name="ZoomIt_Snip_Subtitle" xml:space="preserve">
<value>Enter snip mode</value>
</data>
<data name="ZoomIt_Record_Title" xml:space="preserve">
<value>ZoomIt: Record</value>
</data>
<data name="ZoomIt_Record_Subtitle" xml:space="preserve">
<value>Start recording</value>
</data>
<data name="ZoomIt_Settings_Subtitle" xml:space="preserve">
<value>Open ZoomIt settings</value>
</data>
<!-- FancyZones Monitor Details -->
<data name="FancyZones_Monitor" xml:space="preserve">
<value>Monitor</value>
</data>
<data name="FancyZones_Instance" xml:space="preserve">
<value>Instance</value>
</data>
<data name="FancyZones_Serial" xml:space="preserve">
<value>Serial</value>
</data>
<data name="FancyZones_Number" xml:space="preserve">
<value>Number</value>
</data>
<data name="FancyZones_VirtualDesktop" xml:space="preserve">
<value>Virtual desktop</value>
</data>
<data name="FancyZones_WorkArea" xml:space="preserve">
<value>Work area</value>
</data>
<data name="FancyZones_Resolution" xml:space="preserve">
<value>Resolution</value>
</data>
<data name="FancyZones_DPI" xml:space="preserve">
<value>DPI</value>
</data>
<data name="Common_NotAvailable" xml:space="preserve">
<value>N/A</value>
</data>
</root>

View File

@@ -8,6 +8,8 @@ using System.CommandLine.Invocation;
using FancyZonesCLI;
using FancyZonesCLI.CommandLine;
using FancyZonesCLI.Telemetry;
using Microsoft.PowerToys.Telemetry;
namespace FancyZonesCLI.CommandLine.Commands;
@@ -24,12 +26,14 @@ internal abstract class FancyZonesBaseCommand : Command
private void InvokeInternal(InvocationContext context)
{
Logger.LogInfo($"Executing command '{Name}'");
bool successful = false;
if (!FancyZonesCliGuards.IsFancyZonesRunning())
{
Logger.LogWarning($"Command '{Name}' blocked: FancyZones is not running");
context.Console.Error.Write($"Error: FancyZones is not running. Start PowerToys (FancyZones) and retry.{Environment.NewLine}");
context.Console.Error.Write($"{Properties.Resources.error_fancyzones_not_running}{Environment.NewLine}");
context.ExitCode = 1;
LogTelemetry(successful: false);
return;
}
@@ -37,6 +41,7 @@ internal abstract class FancyZonesBaseCommand : Command
{
string output = Execute(context);
context.ExitCode = 0;
successful = true;
Logger.LogInfo($"Command '{Name}' completed successfully");
Logger.LogDebug($"Command '{Name}' output length: {output?.Length ?? 0}");
@@ -52,6 +57,28 @@ internal abstract class FancyZonesBaseCommand : Command
Logger.LogError($"Command '{Name}' failed", ex);
context.Console.Error.Write($"Error: {ex.Message}{Environment.NewLine}");
context.ExitCode = 1;
successful = false;
}
finally
{
LogTelemetry(successful);
}
}
private void LogTelemetry(bool successful)
{
try
{
PowerToysTelemetry.Log.WriteEvent(new FancyZonesCLICommandEvent
{
CommandName = Name,
Successful = successful,
});
}
catch (Exception ex)
{
// Don't fail the command if telemetry logging fails
Logger.LogError($"Failed to log telemetry for command '{Name}'", ex);
}
}
}

View File

@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
{
public GetActiveLayoutCommand()
: base("get-active-layout", "Show currently active layout")
: base("get-active-layout", Properties.Resources.cmd_get_active_layout)
{
AddAlias("active");
}
@@ -28,7 +28,7 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
if (editorParams.Monitors == null || editorParams.Monitors.Count == 0)
{
throw new InvalidOperationException("Could not get current monitor information.");
throw new InvalidOperationException(Properties.Resources.get_active_layout_no_monitor_info);
}
// Read applied layouts.
@@ -36,11 +36,11 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
if (appliedLayouts.AppliedLayouts == null)
{
return "No layouts configured.";
return Properties.Resources.get_active_layout_no_layouts;
}
var sb = new System.Text.StringBuilder();
sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n");
sb.AppendLine($"\n{Properties.Resources.get_active_layout_header}\n");
// Show only layouts for currently connected monitors.
for (int i = 0; i < editorParams.Monitors.Count; i++)
@@ -71,7 +71,7 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
}
else
{
sb.AppendLine(" No layout applied");
sb.AppendLine(Properties.Resources.get_active_layout_no_layout);
}
if (i < editorParams.Monitors.Count - 1)

View File

@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand
{
public GetHotkeysCommand()
: base("get-hotkeys", "List all layout hotkeys")
: base("get-hotkeys", Properties.Resources.cmd_get_hotkeys)
{
AddAlias("hk");
}
@@ -26,12 +26,12 @@ internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand
if (hotkeys.LayoutHotkeys == null || hotkeys.LayoutHotkeys.Count == 0)
{
return "No hotkeys configured.";
return Properties.Resources.get_hotkeys_no_hotkeys;
}
var sb = new System.Text.StringBuilder();
sb.AppendLine("=== Layout Hotkeys ===\n");
sb.AppendLine("Press Win + Ctrl + Alt + <number> to switch layouts:\n");
sb.AppendLine($"{Properties.Resources.get_hotkeys_header}\n");
sb.AppendLine($"{Properties.Resources.get_hotkeys_instruction}\n");
foreach (var hotkey in hotkeys.LayoutHotkeys.OrderBy(h => h.Key))
{

View File

@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
{
public GetLayoutsCommand()
: base("get-layouts", "List available layouts")
: base("get-layouts", Properties.Resources.cmd_get_layouts)
{
AddAlias("ls");
}
@@ -61,7 +61,7 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
if (customLayouts.CustomLayouts != null)
{
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.CustomLayouts.Count} total) ===");
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_layouts_custom_header, customLayouts.CustomLayouts.Count));
for (int i = 0; i < customLayouts.CustomLayouts.Count; i++)
{
@@ -92,8 +92,8 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
// Add note for canvas layouts.
if (isCanvasLayout)
{
sb.AppendLine("\n Note: Canvas layout preview is approximate.");
sb.AppendLine(" Open FancyZones Editor for precise zone boundaries.");
sb.AppendLine($"\n {Properties.Resources.get_layouts_canvas_note}");
sb.AppendLine($" {Properties.Resources.get_layouts_canvas_detail}");
}
if (i < customLayouts.CustomLayouts.Count - 1)
@@ -102,7 +102,7 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
}
}
sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout <UUID>' to apply a layout.");
sb.AppendLine($"\n{Properties.Resources.get_layouts_usage}");
}
return sb.ToString().TrimEnd();

View File

@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand
{
public GetMonitorsCommand()
: base("get-monitors", "List monitors and FancyZones metadata")
: base("get-monitors", Properties.Resources.cmd_get_monitors)
{
AddAlias("m");
}
@@ -31,19 +31,19 @@ internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to read monitor information. {ex.Message}{Environment.NewLine}Note: Ensure FancyZones is running to get current monitor information.", ex);
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_monitors_error, ex.Message), ex);
}
if (editorParams.Monitors == null || editorParams.Monitors.Count == 0)
{
return "No monitors found.";
return Properties.Resources.get_monitors_no_monitors;
}
// Also read applied layouts to show which layout is active on each monitor.
var appliedLayouts = FancyZonesDataIO.ReadAppliedLayouts();
var sb = new System.Text.StringBuilder();
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Monitors ({editorParams.Monitors.Count} total) ===");
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_monitors_header, editorParams.Monitors.Count));
sb.AppendLine();
for (int i = 0; i < editorParams.Monitors.Count; i++)

View File

@@ -5,6 +5,7 @@
using System;
using System.CommandLine.Invocation;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
@@ -13,7 +14,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand
{
public OpenEditorCommand()
: base("open-editor", "Launch FancyZones layout editor")
: base("open-editor", Properties.Resources.cmd_open_editor)
{
AddAlias("e");
}
@@ -38,7 +39,7 @@ internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to request FancyZones Editor launch. {ex.Message}", ex);
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.open_editor_error, ex.Message), ex);
}
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.CommandLine.Invocation;
using System.Diagnostics;
using System.Globalization;
using System.IO;
namespace FancyZonesCLI.CommandLine.Commands;
@@ -12,7 +13,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand
{
public OpenSettingsCommand()
: base("open-settings", "Open FancyZones settings page")
: base("open-settings", Properties.Resources.cmd_open_settings)
{
AddAlias("settings");
}
@@ -37,14 +38,14 @@ internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand
if (process == null)
{
throw new InvalidOperationException("PowerToys.exe failed to start.");
throw new InvalidOperationException(Properties.Resources.open_settings_error_not_started);
}
return string.Empty;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to open FancyZones Settings. {ex.Message}", ex);
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.open_settings_error, ex.Message), ex);
}
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Globalization;
using FancyZonesEditorCommon.Data;
using FancyZonesEditorCommon.Utils;
@@ -16,11 +17,11 @@ internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand
private readonly Argument<int> _key;
public RemoveHotkeyCommand()
: base("remove-hotkey", "Remove hotkey assignment")
: base("remove-hotkey", Properties.Resources.cmd_remove_hotkey)
{
AddAlias("rhk");
_key = new Argument<int>("key", "Hotkey index (0-9)");
_key = new Argument<int>("key", Properties.Resources.remove_hotkey_arg_key);
AddArgument(_key);
}
@@ -33,14 +34,14 @@ internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand
if (hotkeysWrapper.LayoutHotkeys == null)
{
return "No hotkeys configured.";
return Properties.Resources.remove_hotkey_no_hotkeys;
}
var hotkeysList = hotkeysWrapper.LayoutHotkeys;
var removed = hotkeysList.RemoveAll(h => h.Key == key);
if (removed == 0)
{
return $"No hotkey assigned to key {key}";
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.remove_hotkey_not_found, key);
}
// Save.

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Globalization;
using System.Linq;
using FancyZonesEditorCommon.Data;
@@ -19,12 +20,12 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
private readonly Argument<string> _layout;
public SetHotkeyCommand()
: base("set-hotkey", "Assign hotkey (0-9) to a custom layout")
: base("set-hotkey", Properties.Resources.cmd_set_hotkey)
{
AddAlias("shk");
_key = new Argument<int>("key", "Hotkey index (0-9)");
_layout = new Argument<string>("layout", "Custom layout UUID");
_key = new Argument<int>("key", Properties.Resources.set_hotkey_arg_key);
_layout = new Argument<string>("layout", Properties.Resources.set_hotkey_arg_layout);
AddArgument(_key);
AddArgument(_layout);
@@ -38,7 +39,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
if (key < 0 || key > 9)
{
throw new InvalidOperationException("Key must be between 0 and 9.");
throw new InvalidOperationException(Properties.Resources.set_hotkey_error_invalid_key);
}
// Editor only allows assigning hotkeys to existing custom layouts.
@@ -59,7 +60,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
if (!matchedLayout.HasValue)
{
throw new InvalidOperationException($"Layout '{layout}' is not a custom layout UUID.");
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layout));
}
string layoutName = matchedLayout.Value.Name;

View File

@@ -26,14 +26,14 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
private readonly Option<bool> _all;
public SetLayoutCommand()
: base("set-layout", "Set layout by UUID or template name")
: base("set-layout", Properties.Resources.cmd_set_layout)
{
AddAlias("s");
_layoutId = new Argument<string>("layout", "Layout UUID or template type (e.g. focus, columns)");
_layoutId = new Argument<string>("layout", Properties.Resources.set_layout_arg_layout);
AddArgument(_layoutId);
_monitor = new Option<int?>(AliasesMonitor, "Apply to monitor N (1-based)");
_monitor = new Option<int?>(AliasesMonitor, Properties.Resources.set_layout_opt_monitor);
_monitor.AddValidator(result =>
{
if (result.Tokens.Count == 0)
@@ -44,11 +44,11 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
int? monitor = result.GetValueOrDefault<int?>();
if (monitor.HasValue && monitor.Value < 1)
{
result.ErrorMessage = "Monitor index must be >= 1.";
result.ErrorMessage = Properties.Resources.set_layout_error_monitor_index;
}
});
_all = new Option<bool>(AliasesAll, "Apply to all monitors");
_all = new Option<bool>(AliasesAll, Properties.Resources.set_layout_opt_all);
AddOption(_monitor);
AddOption(_all);
@@ -60,7 +60,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
if (monitor.HasValue && all)
{
commandResult.ErrorMessage = "Cannot specify both --monitor and --all.";
commandResult.ErrorMessage = Properties.Resources.set_layout_error_both_options;
}
});
}
@@ -97,15 +97,15 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
{
if (all)
{
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to all monitors.", layout);
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_all, layout);
}
if (monitor.HasValue)
{
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor {1}.", layout, monitor.Value);
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_monitor, layout, monitor.Value);
}
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor 1.", layout);
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_default, layout);
}
private static (CustomLayouts.CustomLayoutWrapper? TargetCustomLayout, LayoutTemplates.TemplateLayoutWrapper? TargetTemplate) ResolveTargetLayout(string layout)
@@ -127,10 +127,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
if (!targetCustomLayout.HasValue && !targetTemplate.HasValue)
{
throw new InvalidOperationException(
$"Layout '{layout}' not found{Environment.NewLine}" +
"Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')" +
$"{Environment.NewLine} For custom layouts, use the UUID from 'get-layouts'");
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_not_found, layout));
}
return (targetCustomLayout, targetTemplate);
@@ -197,7 +194,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
int monitorIndex = monitor.Value - 1; // Convert to 0-based.
if (monitorIndex < 0 || monitorIndex >= editorParams.Monitors.Count)
{
throw new InvalidOperationException($"Monitor {monitor.Value} not found. Available monitors: 1-{editorParams.Monitors.Count}");
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_monitor_not_found, monitor.Value, editorParams.Monitors.Count));
}
result.Add(monitorIndex);
@@ -250,7 +247,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
if (newLayouts.Count == 0)
{
throw new InvalidOperationException("Internal error - no monitors to update.");
throw new InvalidOperationException(Properties.Resources.set_layout_error_no_monitors);
}
return newLayouts;
@@ -306,7 +303,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
}
else
{
throw new InvalidOperationException($"Unsupported custom layout type '{targetCustomLayout.Value.Type}'.");
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_unsupported_type, targetCustomLayout.Value.Type));
}
return (
@@ -329,7 +326,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
targetTemplate.Value.SensitivityRadius);
}
throw new InvalidOperationException("Internal error - no layout selected.");
throw new InvalidOperationException(Properties.Resources.set_layout_error_no_layout);
}
private static AppliedLayouts.AppliedLayoutsListWrapper MergeWithHistoricalLayouts(

View File

@@ -13,7 +13,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AssemblyName>FancyZonesCLI</AssemblyName>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852</NoWarn>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852;CA1863;CA1305</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -24,6 +24,22 @@
<ItemGroup>
<ProjectReference Include="..\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->

View File

@@ -0,0 +1,353 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FancyZonesCLI.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FancyZonesCLI.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
internal static string error_fancyzones_not_running {
get {
return ResourceManager.GetString("error_fancyzones_not_running", resourceCulture);
}
}
internal static string cmd_get_active_layout {
get {
return ResourceManager.GetString("cmd_get_active_layout", resourceCulture);
}
}
internal static string get_active_layout_no_monitor_info {
get {
return ResourceManager.GetString("get_active_layout_no_monitor_info", resourceCulture);
}
}
internal static string get_active_layout_no_layouts {
get {
return ResourceManager.GetString("get_active_layout_no_layouts", resourceCulture);
}
}
internal static string get_active_layout_header {
get {
return ResourceManager.GetString("get_active_layout_header", resourceCulture);
}
}
internal static string get_active_layout_no_layout {
get {
return ResourceManager.GetString("get_active_layout_no_layout", resourceCulture);
}
}
internal static string cmd_get_layouts {
get {
return ResourceManager.GetString("cmd_get_layouts", resourceCulture);
}
}
internal static string get_layouts_templates_header {
get {
return ResourceManager.GetString("get_layouts_templates_header", resourceCulture);
}
}
internal static string get_layouts_custom_header {
get {
return ResourceManager.GetString("get_layouts_custom_header", resourceCulture);
}
}
internal static string get_layouts_canvas_note {
get {
return ResourceManager.GetString("get_layouts_canvas_note", resourceCulture);
}
}
internal static string get_layouts_canvas_detail {
get {
return ResourceManager.GetString("get_layouts_canvas_detail", resourceCulture);
}
}
internal static string get_layouts_usage {
get {
return ResourceManager.GetString("get_layouts_usage", resourceCulture);
}
}
internal static string cmd_get_monitors {
get {
return ResourceManager.GetString("cmd_get_monitors", resourceCulture);
}
}
internal static string get_monitors_error {
get {
return ResourceManager.GetString("get_monitors_error", resourceCulture);
}
}
internal static string get_monitors_no_monitors {
get {
return ResourceManager.GetString("get_monitors_no_monitors", resourceCulture);
}
}
internal static string get_monitors_header {
get {
return ResourceManager.GetString("get_monitors_header", resourceCulture);
}
}
internal static string cmd_set_layout {
get {
return ResourceManager.GetString("cmd_set_layout", resourceCulture);
}
}
internal static string set_layout_arg_layout {
get {
return ResourceManager.GetString("set_layout_arg_layout", resourceCulture);
}
}
internal static string set_layout_opt_monitor {
get {
return ResourceManager.GetString("set_layout_opt_monitor", resourceCulture);
}
}
internal static string set_layout_opt_all {
get {
return ResourceManager.GetString("set_layout_opt_all", resourceCulture);
}
}
internal static string set_layout_error_monitor_index {
get {
return ResourceManager.GetString("set_layout_error_monitor_index", resourceCulture);
}
}
internal static string set_layout_error_both_options {
get {
return ResourceManager.GetString("set_layout_error_both_options", resourceCulture);
}
}
internal static string set_layout_error_not_found {
get {
return ResourceManager.GetString("set_layout_error_not_found", resourceCulture);
}
}
internal static string set_layout_error_monitor_not_found {
get {
return ResourceManager.GetString("set_layout_error_monitor_not_found", resourceCulture);
}
}
internal static string set_layout_error_no_monitors {
get {
return ResourceManager.GetString("set_layout_error_no_monitors", resourceCulture);
}
}
internal static string set_layout_error_unsupported_type {
get {
return ResourceManager.GetString("set_layout_error_unsupported_type", resourceCulture);
}
}
internal static string set_layout_error_no_layout {
get {
return ResourceManager.GetString("set_layout_error_no_layout", resourceCulture);
}
}
internal static string set_layout_success_all {
get {
return ResourceManager.GetString("set_layout_success_all", resourceCulture);
}
}
internal static string set_layout_success_monitor {
get {
return ResourceManager.GetString("set_layout_success_monitor", resourceCulture);
}
}
internal static string set_layout_success_default {
get {
return ResourceManager.GetString("set_layout_success_default", resourceCulture);
}
}
internal static string cmd_open_editor {
get {
return ResourceManager.GetString("cmd_open_editor", resourceCulture);
}
}
internal static string open_editor_error {
get {
return ResourceManager.GetString("open_editor_error", resourceCulture);
}
}
internal static string cmd_open_settings {
get {
return ResourceManager.GetString("cmd_open_settings", resourceCulture);
}
}
internal static string open_settings_error_not_started {
get {
return ResourceManager.GetString("open_settings_error_not_started", resourceCulture);
}
}
internal static string open_settings_error {
get {
return ResourceManager.GetString("open_settings_error", resourceCulture);
}
}
internal static string cmd_set_hotkey {
get {
return ResourceManager.GetString("cmd_set_hotkey", resourceCulture);
}
}
internal static string set_hotkey_arg_key {
get {
return ResourceManager.GetString("set_hotkey_arg_key", resourceCulture);
}
}
internal static string set_hotkey_arg_layout {
get {
return ResourceManager.GetString("set_hotkey_arg_layout", resourceCulture);
}
}
internal static string set_hotkey_error_invalid_key {
get {
return ResourceManager.GetString("set_hotkey_error_invalid_key", resourceCulture);
}
}
internal static string set_hotkey_error_not_custom {
get {
return ResourceManager.GetString("set_hotkey_error_not_custom", resourceCulture);
}
}
internal static string cmd_remove_hotkey {
get {
return ResourceManager.GetString("cmd_remove_hotkey", resourceCulture);
}
}
internal static string remove_hotkey_arg_key {
get {
return ResourceManager.GetString("remove_hotkey_arg_key", resourceCulture);
}
}
internal static string remove_hotkey_no_hotkeys {
get {
return ResourceManager.GetString("remove_hotkey_no_hotkeys", resourceCulture);
}
}
internal static string remove_hotkey_not_found {
get {
return ResourceManager.GetString("remove_hotkey_not_found", resourceCulture);
}
}
internal static string cmd_get_hotkeys {
get {
return ResourceManager.GetString("cmd_get_hotkeys", resourceCulture);
}
}
internal static string get_hotkeys_no_hotkeys {
get {
return ResourceManager.GetString("get_hotkeys_no_hotkeys", resourceCulture);
}
}
internal static string get_hotkeys_header {
get {
return ResourceManager.GetString("get_hotkeys_header", resourceCulture);
}
}
internal static string get_hotkeys_instruction {
get {
return ResourceManager.GetString("get_hotkeys_instruction", resourceCulture);
}
}
internal static string editor_params_timeout {
get {
return ResourceManager.GetString("editor_params_timeout", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,233 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Base Command -->
<data name="error_fancyzones_not_running" xml:space="preserve">
<value>Error: FancyZones is not running. Start PowerToys (FancyZones) and retry.</value>
</data>
<!-- GetActiveLayoutCommand -->
<data name="cmd_get_active_layout" xml:space="preserve">
<value>Show currently active layout</value>
</data>
<data name="get_active_layout_no_monitor_info" xml:space="preserve">
<value>Could not get current monitor information.</value>
</data>
<data name="get_active_layout_no_layouts" xml:space="preserve">
<value>No layouts configured.</value>
</data>
<data name="get_active_layout_header" xml:space="preserve">
<value>=== Active FancyZones Layout(s) ===</value>
</data>
<data name="get_active_layout_no_layout" xml:space="preserve">
<value> No layout applied</value>
</data>
<!-- GetLayoutsCommand -->
<data name="cmd_get_layouts" xml:space="preserve">
<value>List available layouts</value>
</data>
<data name="get_layouts_templates_header" xml:space="preserve">
<value>=== Built-in Template Layouts ({0} total) ===</value>
</data>
<data name="get_layouts_custom_header" xml:space="preserve">
<value>=== Custom Layouts ({0} total) ===</value>
</data>
<data name="get_layouts_canvas_note" xml:space="preserve">
<value>Note: Canvas layout preview is approximate.</value>
</data>
<data name="get_layouts_canvas_detail" xml:space="preserve">
<value>Open FancyZones Editor for precise zone boundaries.</value>
</data>
<data name="get_layouts_usage" xml:space="preserve">
<value>Use 'FancyZonesCLI.exe set-layout &lt;UUID&gt;' to apply a layout.</value>
</data>
<!-- GetMonitorsCommand -->
<data name="cmd_get_monitors" xml:space="preserve">
<value>List monitors and FancyZones metadata</value>
</data>
<data name="get_monitors_error" xml:space="preserve">
<value>Failed to read monitor information. {0}
Note: Ensure FancyZones is running to get current monitor information.</value>
</data>
<data name="get_monitors_no_monitors" xml:space="preserve">
<value>No monitors found.</value>
</data>
<data name="get_monitors_header" xml:space="preserve">
<value>=== Monitors ({0} total) ===</value>
</data>
<!-- SetLayoutCommand -->
<data name="cmd_set_layout" xml:space="preserve">
<value>Set layout by UUID or template name</value>
</data>
<data name="set_layout_arg_layout" xml:space="preserve">
<value>Layout UUID or template type (e.g. focus, columns)</value>
</data>
<data name="set_layout_opt_monitor" xml:space="preserve">
<value>Apply to monitor N (1-based)</value>
</data>
<data name="set_layout_opt_all" xml:space="preserve">
<value>Apply to all monitors</value>
</data>
<data name="set_layout_error_monitor_index" xml:space="preserve">
<value>Monitor index must be &gt;= 1.</value>
</data>
<data name="set_layout_error_both_options" xml:space="preserve">
<value>Cannot specify both --monitor and --all.</value>
</data>
<data name="set_layout_error_not_found" xml:space="preserve">
<value>Layout '{0}' not found
Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')
For custom layouts, use the UUID from 'get-layouts'</value>
</data>
<data name="set_layout_error_monitor_not_found" xml:space="preserve">
<value>Monitor {0} not found. Available monitors: 1-{1}</value>
</data>
<data name="set_layout_error_no_monitors" xml:space="preserve">
<value>Internal error - no monitors to update.</value>
</data>
<data name="set_layout_error_unsupported_type" xml:space="preserve">
<value>Unsupported custom layout type '{0}'.</value>
</data>
<data name="set_layout_error_no_layout" xml:space="preserve">
<value>Internal error - no layout selected.</value>
</data>
<data name="set_layout_success_all" xml:space="preserve">
<value>Layout '{0}' applied to all monitors.</value>
</data>
<data name="set_layout_success_monitor" xml:space="preserve">
<value>Layout '{0}' applied to monitor {1}.</value>
</data>
<data name="set_layout_success_default" xml:space="preserve">
<value>Layout '{0}' applied to monitor 1.</value>
</data>
<!-- OpenEditorCommand -->
<data name="cmd_open_editor" xml:space="preserve">
<value>Launch FancyZones layout editor</value>
</data>
<data name="open_editor_error" xml:space="preserve">
<value>Failed to request FancyZones Editor launch. {0}</value>
</data>
<!-- OpenSettingsCommand -->
<data name="cmd_open_settings" xml:space="preserve">
<value>Open FancyZones settings page</value>
</data>
<data name="open_settings_error_not_started" xml:space="preserve">
<value>PowerToys.exe failed to start.</value>
</data>
<data name="open_settings_error" xml:space="preserve">
<value>Failed to open FancyZones Settings. {0}</value>
</data>
<!-- SetHotkeyCommand -->
<data name="cmd_set_hotkey" xml:space="preserve">
<value>Assign hotkey (0-9) to a custom layout</value>
</data>
<data name="set_hotkey_arg_key" xml:space="preserve">
<value>Hotkey index (0-9)</value>
</data>
<data name="set_hotkey_arg_layout" xml:space="preserve">
<value>Custom layout UUID</value>
</data>
<data name="set_hotkey_error_invalid_key" xml:space="preserve">
<value>Key must be between 0 and 9.</value>
</data>
<data name="set_hotkey_error_not_custom" xml:space="preserve">
<value>Layout '{0}' is not a custom layout UUID.</value>
</data>
<!-- RemoveHotkeyCommand -->
<data name="cmd_remove_hotkey" xml:space="preserve">
<value>Remove hotkey assignment</value>
</data>
<data name="remove_hotkey_arg_key" xml:space="preserve">
<value>Hotkey index (0-9)</value>
</data>
<data name="remove_hotkey_no_hotkeys" xml:space="preserve">
<value>No hotkeys configured.</value>
</data>
<data name="remove_hotkey_not_found" xml:space="preserve">
<value>No hotkey assigned to key {0}</value>
</data>
<!-- GetHotkeysCommand -->
<data name="cmd_get_hotkeys" xml:space="preserve">
<value>List all layout hotkeys</value>
</data>
<data name="get_hotkeys_no_hotkeys" xml:space="preserve">
<value>No hotkeys configured.</value>
</data>
<data name="get_hotkeys_header" xml:space="preserve">
<value>=== Layout Hotkeys ===</value>
</data>
<data name="get_hotkeys_instruction" xml:space="preserve">
<value>Press Win + Ctrl + Alt + &lt;number&gt; to switch layouts:</value>
</data>
<!-- EditorParametersRefresh -->
<data name="editor_params_timeout" xml:space="preserve">
<value>Could not get current monitor information (timed out after {0}ms waiting for '{1}').</value>
</data>
</root>

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace FancyZonesCLI.Telemetry
{
/// <summary>
/// Telemetry event for FancyZones CLI command execution.
/// </summary>
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class FancyZonesCLICommandEvent : EventBase, IEvent
{
public FancyZonesCLICommandEvent()
{
EventName = "FancyZones_CLICommand";
}
/// <summary>
/// Gets or sets the name of the CLI command that was executed.
/// </summary>
public string CommandName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the command executed successfully.
/// </summary>
public bool Successful { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading;
@@ -60,7 +61,7 @@ internal static class EditorParametersRefresh
var finalParams = FancyZonesDataIO.ReadEditorParameters();
if (finalParams.Monitors == null || finalParams.Monitors.Count == 0)
{
throw new InvalidOperationException($"Could not get current monitor information (timed out after {maxWaitMilliseconds}ms waiting for '{Path.GetFileName(filePath)}').");
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.editor_params_timeout, maxWaitMilliseconds, Path.GetFileName(filePath)));
}
return finalParams;

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.ImageResizerCLI</AssemblyTitle>
<AssemblyDescription>PowerToys Image Resizer Command Line Interface</AssemblyDescription>
<Description>PowerToys Image Resizer CLI</Description>
<OutputType>Exe</OutputType>
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
<AssemblyName>PowerToys.ImageResizerCLI</AssemblyName>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
</ItemGroup>
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
<ItemGroup>
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Text;
using ImageResizer.Cli;
using ManagedCommon;
namespace ImageResizerCLI;
internal static class Program
{
private static int Main(string[] args)
{
try
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
}
}
catch (CultureNotFoundException)
{
// Ignore invalid culture and fall back to default.
}
Console.InputEncoding = Encoding.Unicode;
// Initialize logger to file (same as other modules)
CliLogger.Initialize("\\ImageResizer\\Logs");
CliLogger.Info($"ImageResizerCLI started with {args.Length} argument(s)");
try
{
var executor = new ImageResizerCliExecutor();
return executor.Run(args);
}
catch (Exception ex)
{
CliLogger.Error($"Unhandled exception: {ex.Message}");
CliLogger.Error($"Stack trace: {ex.StackTrace}");
Console.Error.WriteLine($"Fatal error: {ex.Message}");
return 1;
}
}
}

View File

@@ -0,0 +1,320 @@
// 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 ImageResizer.Cli;
using ImageResizer.Models;
using ImageResizer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Tests.Cli
{
[TestClass]
public class CliSettingsApplierTests
{
private Settings CreateDefaultSettings()
{
var settings = new Settings();
settings.Sizes.Add(new ResizeSize(0, "Small", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel));
settings.Sizes.Add(new ResizeSize(1, "Medium", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel));
settings.Sizes.Add(new ResizeSize(2, "Large", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel));
return settings;
}
[TestMethod]
public void Apply_WithCustomWidth_SetsCustomSizeWidth()
{
var options = new CliOptions { Width = 800 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithCustomHeight_SetsCustomSizeHeight()
{
var options = new CliOptions { Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithCustomSize_SelectsCustomSizeIndex()
{
var options = new CliOptions { Width = 800, Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
// Custom size index should be settings.Sizes.Count
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithZeroWidth_SetsZeroForAutoCalculation()
{
var options = new CliOptions { Width = 0, Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(0.0, settings.CustomSize.Width);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithZeroHeight_SetsZeroForAutoCalculation()
{
var options = new CliOptions { Width = 800, Height = 0 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
Assert.AreEqual(0.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithNullWidthAndHeight_DoesNotModifyCustomSize()
{
var options = new CliOptions { Width = null, Height = null };
var settings = CreateDefaultSettings();
var originalWidth = settings.CustomSize.Width;
var originalHeight = settings.CustomSize.Height;
CliSettingsApplier.Apply(options, settings);
// When both null, should not modify CustomSize (keeps default 1024x640)
Assert.AreEqual(originalWidth, settings.CustomSize.Width);
Assert.AreEqual(originalHeight, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithUnit_SetsCustomSizeUnit()
{
var options = new CliOptions { Width = 100, Unit = ResizeUnit.Percent };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(ResizeUnit.Percent, settings.CustomSize.Unit);
}
[TestMethod]
public void Apply_WithFit_SetsCustomSizeFit()
{
var options = new CliOptions { Width = 800, Fit = ResizeFit.Fill };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(ResizeFit.Fill, settings.CustomSize.Fit);
}
[TestMethod]
public void Apply_WithValidSizeIndex_SetsSelectedSizeIndex()
{
var options = new CliOptions { SizeIndex = 1 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(1, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithInvalidSizeIndex_DoesNotChangeSelection()
{
var options = new CliOptions { SizeIndex = 99 };
var settings = CreateDefaultSettings();
var originalIndex = settings.SelectedSizeIndex;
CliSettingsApplier.Apply(options, settings);
// Should remain unchanged when invalid
Assert.AreEqual(originalIndex, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithNegativeSizeIndex_DoesNotChangeSelection()
{
var options = new CliOptions { SizeIndex = -1 };
var settings = CreateDefaultSettings();
var originalIndex = settings.SelectedSizeIndex;
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(originalIndex, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithShrinkOnly_SetsShrinkOnly()
{
var options = new CliOptions { ShrinkOnly = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.ShrinkOnly);
}
[TestMethod]
public void Apply_WithReplace_SetsReplace()
{
var options = new CliOptions { Replace = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.Replace);
}
[TestMethod]
public void Apply_WithIgnoreOrientation_SetsIgnoreOrientation()
{
var options = new CliOptions { IgnoreOrientation = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.IgnoreOrientation);
}
[TestMethod]
public void Apply_WithRemoveMetadata_SetsRemoveMetadata()
{
var options = new CliOptions { RemoveMetadata = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.RemoveMetadata);
}
[TestMethod]
public void Apply_WithJpegQualityLevel_SetsJpegQualityLevel()
{
var options = new CliOptions { JpegQualityLevel = 85 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(85, settings.JpegQualityLevel);
}
[TestMethod]
public void Apply_WithKeepDateModified_SetsKeepDateModified()
{
var options = new CliOptions { KeepDateModified = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.KeepDateModified);
}
[TestMethod]
public void Apply_WithFileName_SetsFileName()
{
var options = new CliOptions { FileName = "%1 (%2)" };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual("%1 (%2)", settings.FileName);
}
[TestMethod]
public void Apply_WithEmptyFileName_DoesNotChangeFileName()
{
var options = new CliOptions { FileName = string.Empty };
var settings = CreateDefaultSettings();
var originalFileName = settings.FileName;
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(originalFileName, settings.FileName);
}
[TestMethod]
public void Apply_WithMultipleOptions_AppliesAllOptions()
{
var options = new CliOptions
{
Width = 800,
Height = 600,
Unit = ResizeUnit.Percent,
Fit = ResizeFit.Fill,
ShrinkOnly = true,
Replace = true,
IgnoreOrientation = true,
RemoveMetadata = true,
JpegQualityLevel = 90,
KeepDateModified = true,
FileName = "test_%2",
};
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
Assert.AreEqual(600.0, settings.CustomSize.Height);
Assert.AreEqual(ResizeUnit.Percent, settings.CustomSize.Unit);
Assert.AreEqual(ResizeFit.Fill, settings.CustomSize.Fit);
Assert.IsTrue(settings.ShrinkOnly);
Assert.IsTrue(settings.Replace);
Assert.IsTrue(settings.IgnoreOrientation);
Assert.IsTrue(settings.RemoveMetadata);
Assert.AreEqual(90, settings.JpegQualityLevel);
Assert.IsTrue(settings.KeepDateModified);
Assert.AreEqual("test_%2", settings.FileName);
}
[TestMethod]
public void Apply_CustomSizeTakesPrecedence_OverSizeIndex()
{
var options = new CliOptions
{
Width = 800,
Height = 600,
SizeIndex = 1, // Should be ignored when Width/Height specified
};
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
// Custom size should be selected, not preset
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithOnlyWidth_StillSelectsCustomSize()
{
var options = new CliOptions { Width = 800 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithOnlyHeight_StillSelectsCustomSize()
{
var options = new CliOptions { Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
}
}

View File

@@ -0,0 +1,268 @@
// 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.Linq;
using ImageResizer.Cli.Commands;
using ImageResizer.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Tests.Models
{
[TestClass]
public class CliOptionsTests
{
private static readonly string[] _multiFileArgs = new[] { "test1.jpg", "test2.jpg", "test3.jpg" };
private static readonly string[] _mixedOptionsArgs = new[] { "--width", "800", "test1.jpg", "--height", "600", "test2.jpg" };
[TestMethod]
public void Parse_WithValidWidth_SetsWidth()
{
var args = new[] { "--width", "800", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
}
[TestMethod]
public void Parse_WithValidHeight_SetsHeight()
{
var args = new[] { "--height", "600", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(600.0, options.Height);
}
[TestMethod]
public void Parse_WithShortWidthAlias_WorksIdentically()
{
var longFormArgs = new[] { "--width", "800", "test.jpg" };
var shortFormArgs = new[] { "-w", "800", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.Width, shortForm.Width);
}
[TestMethod]
public void Parse_WithShortHeightAlias_WorksIdentically()
{
var longFormArgs = new[] { "--height", "600", "test.jpg" };
var shortFormArgs = new[] { "-h", "600", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.Height, shortForm.Height);
}
[TestMethod]
public void Parse_WithValidUnit_SetsUnit()
{
var args = new[] { "--unit", "Percent", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeUnit.Percent, options.Unit);
}
[TestMethod]
public void Parse_WithValidFit_SetsFit()
{
var args = new[] { "--fit", "Fill", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeFit.Fill, options.Fit);
}
[TestMethod]
public void Parse_WithSizeIndex_SetsSizeIndex()
{
var args = new[] { "--size", "2", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(2, options.SizeIndex);
}
[TestMethod]
public void Parse_WithShrinkOnly_SetsShrinkOnly()
{
var args = new[] { "--shrink-only", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ShrinkOnly);
}
[TestMethod]
public void Parse_WithReplace_SetsReplace()
{
var args = new[] { "--replace", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.Replace);
}
[TestMethod]
public void Parse_WithIgnoreOrientation_SetsIgnoreOrientation()
{
var args = new[] { "--ignore-orientation", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.IgnoreOrientation);
}
[TestMethod]
public void Parse_WithRemoveMetadata_SetsRemoveMetadata()
{
var args = new[] { "--remove-metadata", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.RemoveMetadata);
}
[TestMethod]
public void Parse_WithValidQuality_SetsQuality()
{
var args = new[] { "--quality", "85", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(85, options.JpegQualityLevel);
}
[TestMethod]
public void Parse_WithKeepDateModified_SetsKeepDateModified()
{
var args = new[] { "--keep-date-modified", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.KeepDateModified);
}
[TestMethod]
public void Parse_WithFileName_SetsFileName()
{
var args = new[] { "--filename", "%1 (%2)", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual("%1 (%2)", options.FileName);
}
[TestMethod]
public void Parse_WithDestination_SetsDestinationDirectory()
{
var args = new[] { "--destination", "C:\\Output", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual("C:\\Output", options.DestinationDirectory);
}
[TestMethod]
public void Parse_WithShortDestinationAlias_WorksIdentically()
{
var longFormArgs = new[] { "--destination", "C:\\Output", "test.jpg" };
var shortFormArgs = new[] { "-d", "C:\\Output", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.DestinationDirectory, shortForm.DestinationDirectory);
}
[TestMethod]
public void Parse_WithProgressLines_SetsProgressLines()
{
var args = new[] { "--progress-lines", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ProgressLines);
}
[TestMethod]
public void Parse_WithAccessibleAlias_SetsProgressLines()
{
var args = new[] { "--accessible", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ProgressLines);
}
[TestMethod]
public void Parse_WithMultipleFiles_AddsAllFiles()
{
var args = _multiFileArgs;
var options = CliOptions.Parse(args);
Assert.AreEqual(3, options.Files.Count);
CollectionAssert.Contains(options.Files.ToList(), "test1.jpg");
CollectionAssert.Contains(options.Files.ToList(), "test2.jpg");
CollectionAssert.Contains(options.Files.ToList(), "test3.jpg");
}
[TestMethod]
public void Parse_WithMixedOptionsAndFiles_ParsesCorrectly()
{
var args = _mixedOptionsArgs;
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
Assert.AreEqual(600.0, options.Height);
Assert.AreEqual(2, options.Files.Count);
}
[TestMethod]
public void Parse_WithHelp_SetsShowHelp()
{
var args = new[] { "--help" };
var options = CliOptions.Parse(args);
Assert.IsTrue(options.ShowHelp);
}
[TestMethod]
public void Parse_WithShowConfig_SetsShowConfig()
{
var args = new[] { "--show-config" };
var options = CliOptions.Parse(args);
Assert.IsTrue(options.ShowConfig);
}
[TestMethod]
public void Parse_WithNoArguments_ReturnsEmptyOptions()
{
var args = Array.Empty<string>();
var options = CliOptions.Parse(args);
Assert.IsNotNull(options);
Assert.AreEqual(0, options.Files.Count);
}
[TestMethod]
public void Parse_WithZeroWidth_AllowsZeroValue()
{
var args = new[] { "--width", "0", "--height", "600", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(0.0, options.Width);
Assert.AreEqual(600.0, options.Height);
}
[TestMethod]
public void Parse_WithZeroHeight_AllowsZeroValue()
{
var args = new[] { "--width", "800", "--height", "0", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
Assert.AreEqual(0.0, options.Height);
}
[TestMethod]
public void Parse_CaseInsensitiveEnums_ParsesCorrectly()
{
var args = new[] { "--unit", "pixel", "--fit", "fit", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeUnit.Pixel, options.Unit);
Assert.AreEqual(ResizeFit.Fit, options.Fit);
}
}
}

View File

@@ -25,20 +25,27 @@ namespace ImageResizer.Models
[TestMethod]
public void FromCommandLineWorks()
{
// Use actual test files that exist in the test directory
var testDir = Path.GetDirectoryName(typeof(ResizeBatchTests).Assembly.Location);
var file1 = Path.Combine(testDir, "Test.jpg");
var file2 = Path.Combine(testDir, "Test.png");
var file3 = Path.Combine(testDir, "Test.gif");
var standardInput =
"Image1.jpg" + EOL +
"Image2.jpg";
file1 + EOL +
file2;
var args = new[]
{
"/d", "OutputDir",
"Image3.jpg",
file3,
};
var result = ResizeBatch.FromCommandLine(
new StringReader(standardInput),
args);
CollectionAssert.AreEquivalent(new List<string> { "Image1.jpg", "Image2.jpg", "Image3.jpg" }, result.Files.ToArray());
var files = result.Files.Select(Path.GetFileName).ToArray();
CollectionAssert.AreEquivalent(new List<string> { "Test.jpg", "Test.png", "Test.gif" }, files);
Assert.AreEqual("OutputDir", result.DestinationDirectory);
}

View File

@@ -0,0 +1,28 @@
// 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 ManagedCommon;
namespace ImageResizer.Cli
{
public static class CliLogger
{
private static bool _initialized;
public static void Initialize(string logSubFolder)
{
if (!_initialized)
{
Logger.InitializeLogger(logSubFolder);
_initialized = true;
}
}
public static void Info(string message) => Logger.LogInfo(message);
public static void Warn(string message) => Logger.LogWarning(message);
public static void Error(string message) => Logger.LogError(message);
}
}

View File

@@ -0,0 +1,122 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.Cli
{
/// <summary>
/// Applies CLI options to Settings object.
/// Separated from executor logic for Single Responsibility Principle.
/// </summary>
public static class CliSettingsApplier
{
/// <summary>
/// Applies CLI options to the settings, overriding default values.
/// </summary>
/// <param name="cliOptions">The CLI options to apply.</param>
/// <param name="settings">The settings to modify.</param>
public static void Apply(CliOptions cliOptions, Settings settings)
{
// Handle complex size options first
ApplySizeOptions(cliOptions, settings);
// Apply simple property mappings
ApplySimpleOptions(cliOptions, settings);
}
private static void ApplySizeOptions(CliOptions cliOptions, Settings settings)
{
if (cliOptions.Width.HasValue || cliOptions.Height.HasValue)
{
ApplyCustomSizeOptions(cliOptions, settings);
}
else if (cliOptions.SizeIndex.HasValue)
{
ApplyPresetSizeOption(cliOptions, settings);
}
}
private static void ApplyCustomSizeOptions(CliOptions cliOptions, Settings settings)
{
// Set dimensions (0 = auto-calculate for aspect ratio preservation)
// Implementation: ResizeSize.ConvertToPixels() returns double.PositiveInfinity for 0 in Fit mode,
// causing Math.Min(scaleX, scaleY) to preserve aspect ratio by selecting the non-zero scale.
// For Fill/Stretch modes, 0 uses the original dimension instead.
settings.CustomSize.Width = cliOptions.Width ?? 0;
settings.CustomSize.Height = cliOptions.Height ?? 0;
// Apply optional properties
if (cliOptions.Unit.HasValue)
{
settings.CustomSize.Unit = cliOptions.Unit.Value;
}
if (cliOptions.Fit.HasValue)
{
settings.CustomSize.Fit = cliOptions.Fit.Value;
}
// Select custom size (index = Sizes.Count)
settings.SelectedSizeIndex = settings.Sizes.Count;
}
private static void ApplyPresetSizeOption(CliOptions cliOptions, Settings settings)
{
var index = cliOptions.SizeIndex.Value;
if (index >= 0 && index < settings.Sizes.Count)
{
settings.SelectedSizeIndex = index;
}
else
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_WarningInvalidSizeIndex, index));
CliLogger.Warn($"Invalid size index: {index}");
}
}
private static void ApplySimpleOptions(CliOptions cliOptions, Settings settings)
{
if (cliOptions.ShrinkOnly.HasValue)
{
settings.ShrinkOnly = cliOptions.ShrinkOnly.Value;
}
if (cliOptions.Replace.HasValue)
{
settings.Replace = cliOptions.Replace.Value;
}
if (cliOptions.IgnoreOrientation.HasValue)
{
settings.IgnoreOrientation = cliOptions.IgnoreOrientation.Value;
}
if (cliOptions.RemoveMetadata.HasValue)
{
settings.RemoveMetadata = cliOptions.RemoveMetadata.Value;
}
if (cliOptions.JpegQualityLevel.HasValue)
{
settings.JpegQualityLevel = cliOptions.JpegQualityLevel.Value;
}
if (cliOptions.KeepDateModified.HasValue)
{
settings.KeepDateModified = cliOptions.KeepDateModified.Value;
}
if (!string.IsNullOrEmpty(cliOptions.FileName))
{
settings.FileName = cliOptions.FileName;
}
}
}
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
using ImageResizer.Cli.Options;
namespace ImageResizer.Cli.Commands
{
/// <summary>
/// Root command for the ImageResizer CLI.
/// </summary>
public sealed class ImageResizerRootCommand : RootCommand
{
public ImageResizerRootCommand()
: base("PowerToys Image Resizer - Resize images from command line")
{
HelpOption = new HelpOption();
ShowConfigOption = new ShowConfigOption();
DestinationOption = new DestinationOption();
WidthOption = new WidthOption();
HeightOption = new HeightOption();
UnitOption = new UnitOption();
FitOption = new FitOption();
SizeOption = new SizeOption();
ShrinkOnlyOption = new ShrinkOnlyOption();
ReplaceOption = new ReplaceOption();
IgnoreOrientationOption = new IgnoreOrientationOption();
RemoveMetadataOption = new RemoveMetadataOption();
QualityOption = new QualityOption();
KeepDateModifiedOption = new KeepDateModifiedOption();
FileNameOption = new FileNameOption();
ProgressLinesOption = new ProgressLinesOption();
FilesArgument = new FilesArgument();
AddOption(HelpOption);
AddOption(ShowConfigOption);
AddOption(DestinationOption);
AddOption(WidthOption);
AddOption(HeightOption);
AddOption(UnitOption);
AddOption(FitOption);
AddOption(SizeOption);
AddOption(ShrinkOnlyOption);
AddOption(ReplaceOption);
AddOption(IgnoreOrientationOption);
AddOption(RemoveMetadataOption);
AddOption(QualityOption);
AddOption(KeepDateModifiedOption);
AddOption(FileNameOption);
AddOption(ProgressLinesOption);
AddArgument(FilesArgument);
}
public HelpOption HelpOption { get; }
public ShowConfigOption ShowConfigOption { get; }
public DestinationOption DestinationOption { get; }
public WidthOption WidthOption { get; }
public HeightOption HeightOption { get; }
public UnitOption UnitOption { get; }
public FitOption FitOption { get; }
public SizeOption SizeOption { get; }
public ShrinkOnlyOption ShrinkOnlyOption { get; }
public ReplaceOption ReplaceOption { get; }
public IgnoreOrientationOption IgnoreOrientationOption { get; }
public RemoveMetadataOption RemoveMetadataOption { get; }
public QualityOption QualityOption { get; }
public KeepDateModifiedOption KeepDateModifiedOption { get; }
public FileNameOption FileNameOption { get; }
public ProgressLinesOption ProgressLinesOption { get; }
public FilesArgument FilesArgument { get; }
}
}

View File

@@ -0,0 +1,124 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.Cli
{
/// <summary>
/// Executes Image Resizer CLI operations.
/// Instance-based design for better testability and Single Responsibility Principle.
/// </summary>
public class ImageResizerCliExecutor
{
/// <summary>
/// Runs the CLI executor with the provided command-line arguments.
/// </summary>
/// <param name="args">Command-line arguments.</param>
/// <returns>Exit code.</returns>
public int Run(string[] args)
{
var cliOptions = CliOptions.Parse(args);
if (cliOptions.ParseErrors.Count > 0)
{
foreach (var error in cliOptions.ParseErrors)
{
Console.Error.WriteLine(error);
CliLogger.Error($"Parse error: {error}");
}
CliOptions.PrintUsage();
return 1;
}
if (cliOptions.ShowHelp)
{
CliOptions.PrintUsage();
return 0;
}
if (cliOptions.ShowConfig)
{
CliOptions.PrintConfig(Settings.Default);
return 0;
}
if (cliOptions.Files.Count == 0 && string.IsNullOrEmpty(cliOptions.PipeName))
{
Console.WriteLine(Resources.CLI_NoInputFiles);
CliOptions.PrintUsage();
return 1;
}
return RunSilentMode(cliOptions);
}
private int RunSilentMode(CliOptions cliOptions)
{
var batch = ResizeBatch.FromCliOptions(Console.In, cliOptions);
var settings = Settings.Default;
CliSettingsApplier.Apply(cliOptions, settings);
CliLogger.Info($"CLI mode: processing {batch.Files.Count} files");
// Use accessible line-based progress if requested or detected
bool useLineBasedProgress = cliOptions.ProgressLines ?? false;
int lastReportedMilestone = -1;
var errors = batch.Process(
(completed, total) =>
{
var progress = (int)((completed / total) * 100);
if (useLineBasedProgress)
{
// Milestone-based progress (0%, 25%, 50%, 75%, 100%)
int milestone = (progress / 25) * 25;
if (milestone > lastReportedMilestone || completed == (int)total)
{
lastReportedMilestone = milestone;
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_ProgressFormat, progress, completed, (int)total));
}
}
else
{
// Traditional carriage return mode
Console.Write(string.Format(CultureInfo.InvariantCulture, "\r{0}", string.Format(CultureInfo.InvariantCulture, Resources.CLI_ProgressFormat, progress, completed, (int)total)));
}
},
settings,
CancellationToken.None);
if (!useLineBasedProgress)
{
Console.WriteLine();
}
var errorList = errors.ToList();
if (errorList.Count > 0)
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_CompletedWithErrors, errorList.Count));
CliLogger.Error($"Processing completed with {errorList.Count} error(s)");
foreach (var error in errorList)
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, " {0}: {1}", error.File, error.Error));
CliLogger.Error($" {error.File}: {error.Error}");
}
return 1;
}
CliLogger.Info("CLI batch completed successfully");
Console.WriteLine(Resources.CLI_AllFilesProcessed);
return 0;
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class DestinationOption : Option<string>
{
private static readonly string[] _aliases = ["--destination", "-d", "/d"];
public DestinationOption()
: base(_aliases, Properties.Resources.CLI_Option_Destination)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FileNameOption : Option<string>
{
private static readonly string[] _aliases = ["--filename", "-n"];
public FileNameOption()
: base(_aliases, Properties.Resources.CLI_Option_FileName)
{
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FilesArgument : Argument<string[]>
{
public FilesArgument()
: base("files", Properties.Resources.CLI_Option_Files)
{
Arity = ArgumentArity.ZeroOrMore;
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FitOption : Option<ImageResizer.Models.ResizeFit?>
{
private static readonly string[] _aliases = ["--fit", "-f"];
public FitOption()
: base(_aliases, Properties.Resources.CLI_Option_Fit)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HeightOption : Option<double?>
{
private static readonly string[] _aliases = ["--height", "-h"];
public HeightOption()
: base(_aliases, Properties.Resources.CLI_Option_Height)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HelpOption : Option<bool>
{
private static readonly string[] _aliases = ["--help", "-?", "/?"];
public HelpOption()
: base(_aliases, Properties.Resources.CLI_Option_Help)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class IgnoreOrientationOption : Option<bool>
{
private static readonly string[] _aliases = ["--ignore-orientation"];
public IgnoreOrientationOption()
: base(_aliases, Properties.Resources.CLI_Option_IgnoreOrientation)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class KeepDateModifiedOption : Option<bool>
{
private static readonly string[] _aliases = ["--keep-date-modified"];
public KeepDateModifiedOption()
: base(_aliases, Properties.Resources.CLI_Option_KeepDateModified)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ProgressLinesOption : Option<bool>
{
private static readonly string[] _aliases = ["--progress-lines", "--accessible"];
public ProgressLinesOption()
: base(_aliases, "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
{
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class QualityOption : Option<int?>
{
private static readonly string[] _aliases = ["--quality", "-q"];
public QualityOption()
: base(_aliases, Properties.Resources.CLI_Option_Quality)
{
AddValidator(result =>
{
var value = result.GetValueOrDefault<int?>();
if (value.HasValue && (value.Value < 1 || value.Value > 100))
{
result.ErrorMessage = "JPEG quality must be between 1 and 100.";
}
});
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class RemoveMetadataOption : Option<bool>
{
private static readonly string[] _aliases = ["--remove-metadata"];
public RemoveMetadataOption()
: base(_aliases, Properties.Resources.CLI_Option_RemoveMetadata)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ReplaceOption : Option<bool>
{
private static readonly string[] _aliases = ["--replace", "-r"];
public ReplaceOption()
: base(_aliases, Properties.Resources.CLI_Option_Replace)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShowConfigOption : Option<bool>
{
private static readonly string[] _aliases = ["--show-config", "--config"];
public ShowConfigOption()
: base(_aliases, Properties.Resources.CLI_Option_ShowConfig)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShrinkOnlyOption : Option<bool>
{
private static readonly string[] _aliases = ["--shrink-only"];
public ShrinkOnlyOption()
: base(_aliases, Properties.Resources.CLI_Option_ShrinkOnly)
{
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class SizeOption : Option<int?>
{
private static readonly string[] _aliases = ["--size"];
public SizeOption()
: base(_aliases, Properties.Resources.CLI_Option_Size)
{
AddValidator(result =>
{
var value = result.GetValueOrDefault<int?>();
if (value.HasValue && value.Value < 0)
{
result.ErrorMessage = "Size index must be a non-negative integer.";
}
});
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class UnitOption : Option<ImageResizer.Models.ResizeUnit?>
{
private static readonly string[] _aliases = ["--unit", "-u"];
public UnitOption()
: base(_aliases, Properties.Resources.CLI_Option_Unit)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class WidthOption : Option<double?>
{
private static readonly string[] _aliases = ["--width", "-w"];
public WidthOption()
: base(_aliases, Properties.Resources.CLI_Option_Width)
{
}
}
}

View File

@@ -20,6 +20,7 @@
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>CA1863</NoWarn>
</PropertyGroup>
<PropertyGroup>
@@ -51,6 +52,7 @@
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="WPF-UI" />
</ItemGroup>

View File

@@ -0,0 +1,261 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.CommandLine.Parsing;
using System.Globalization;
using ImageResizer.Cli.Commands;
#pragma warning disable SA1649 // File name should match first type name
#pragma warning disable SA1402 // File may only contain a single type
namespace ImageResizer.Models
{
/// <summary>
/// Represents the command-line options for ImageResizer CLI mode.
/// </summary>
public class CliOptions
{
/// <summary>
/// Gets or sets a value indicating whether to show help information.
/// </summary>
public bool ShowHelp { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show current configuration.
/// </summary>
public bool ShowConfig { get; set; }
/// <summary>
/// Gets or sets the destination directory for resized images.
/// </summary>
public string DestinationDirectory { get; set; }
/// <summary>
/// Gets or sets the width of the resized image.
/// </summary>
public double? Width { get; set; }
/// <summary>
/// Gets or sets the height of the resized image.
/// </summary>
public double? Height { get; set; }
/// <summary>
/// Gets or sets the resize unit (Pixel, Percent, Inch, Centimeter).
/// </summary>
public ResizeUnit? Unit { get; set; }
/// <summary>
/// Gets or sets the resize fit mode (Fill, Fit, Stretch).
/// </summary>
public ResizeFit? Fit { get; set; }
/// <summary>
/// Gets or sets the index of the preset size to use.
/// </summary>
public int? SizeIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to only shrink images (not enlarge).
/// </summary>
public bool? ShrinkOnly { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to replace the original file.
/// </summary>
public bool? Replace { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to ignore orientation when resizing.
/// </summary>
public bool? IgnoreOrientation { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to remove metadata from the resized image.
/// </summary>
public bool? RemoveMetadata { get; set; }
/// <summary>
/// Gets or sets the JPEG quality level (1-100).
/// </summary>
public int? JpegQualityLevel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to keep the date modified.
/// </summary>
public bool? KeepDateModified { get; set; }
/// <summary>
/// Gets or sets the output filename format.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use line-based progress output for screen reader accessibility.
/// </summary>
public bool? ProgressLines { get; set; }
/// <summary>
/// Gets the list of files to process.
/// </summary>
public ICollection<string> Files { get; } = new List<string>();
/// <summary>
/// Gets or sets the pipe name for receiving file list.
/// </summary>
public string PipeName { get; set; }
/// <summary>
/// Gets parse/validation errors produced by System.CommandLine.
/// </summary>
public IReadOnlyList<string> ParseErrors { get; private set; } = Array.Empty<string>();
/// <summary>
/// Converts a boolean value to nullable bool (true -> true, false -> null).
/// </summary>
private static bool? ToBoolOrNull(bool value) => value ? true : null;
/// <summary>
/// Parses command-line arguments into CliOptions using System.CommandLine.
/// </summary>
/// <param name="args">The command-line arguments.</param>
/// <returns>A CliOptions instance with parsed values.</returns>
public static CliOptions Parse(string[] args)
{
var options = new CliOptions();
var cmd = new ImageResizerRootCommand();
// Parse using System.CommandLine
var parseResult = new Parser(cmd).Parse(args);
if (parseResult.Errors.Count > 0)
{
var errors = new List<string>(parseResult.Errors.Count);
foreach (var error in parseResult.Errors)
{
errors.Add(error.Message);
}
options.ParseErrors = new ReadOnlyCollection<string>(errors);
}
// Extract values from parse result using strongly typed options
options.ShowHelp = parseResult.GetValueForOption(cmd.HelpOption);
options.ShowConfig = parseResult.GetValueForOption(cmd.ShowConfigOption);
options.DestinationDirectory = parseResult.GetValueForOption(cmd.DestinationOption);
options.Width = parseResult.GetValueForOption(cmd.WidthOption);
options.Height = parseResult.GetValueForOption(cmd.HeightOption);
options.Unit = parseResult.GetValueForOption(cmd.UnitOption);
options.Fit = parseResult.GetValueForOption(cmd.FitOption);
options.SizeIndex = parseResult.GetValueForOption(cmd.SizeOption);
// Convert bool to nullable bool (true -> true, false -> null)
options.ShrinkOnly = ToBoolOrNull(parseResult.GetValueForOption(cmd.ShrinkOnlyOption));
options.Replace = ToBoolOrNull(parseResult.GetValueForOption(cmd.ReplaceOption));
options.IgnoreOrientation = ToBoolOrNull(parseResult.GetValueForOption(cmd.IgnoreOrientationOption));
options.RemoveMetadata = ToBoolOrNull(parseResult.GetValueForOption(cmd.RemoveMetadataOption));
options.KeepDateModified = ToBoolOrNull(parseResult.GetValueForOption(cmd.KeepDateModifiedOption));
options.ProgressLines = ToBoolOrNull(parseResult.GetValueForOption(cmd.ProgressLinesOption));
options.JpegQualityLevel = parseResult.GetValueForOption(cmd.QualityOption);
options.FileName = parseResult.GetValueForOption(cmd.FileNameOption);
// Get files from arguments
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
if (files != null)
{
const string pipeNamePrefix = "\\\\.\\pipe\\";
foreach (var file in files)
{
// Check for pipe name (must be at the start of the path)
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
{
options.PipeName = file.Substring(pipeNamePrefix.Length);
}
else
{
options.Files.Add(file);
}
}
}
return options;
}
/// <summary>
/// Prints current configuration to the console.
/// </summary>
/// <param name="settings">The settings to display.</param>
public static void PrintConfig(ImageResizer.Properties.Settings settings)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_ConfigTitle);
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigGeneralSettings);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigShrinkOnly, settings.ShrinkOnly));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigReplaceOriginal, settings.Replace));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigIgnoreOrientation, settings.IgnoreOrientation));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigRemoveMetadata, settings.RemoveMetadata));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigKeepDateModified, settings.KeepDateModified));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigJpegQuality, settings.JpegQualityLevel));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPngInterlace, settings.PngInterlaceOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigTiffCompress, settings.TiffCompressOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFilenameFormat, settings.FileName));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigCustomSize);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigWidth, settings.CustomSize.Width, settings.CustomSize.Unit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigHeight, settings.CustomSize.Height, settings.CustomSize.Unit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFitMode, settings.CustomSize.Fit));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigPresetSizes);
for (int i = 0; i < settings.Sizes.Count; i++)
{
var size = settings.Sizes[i];
var selected = i == settings.SelectedSizeIndex ? "*" : " ";
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPresetSizeFormat, i, selected, size.Name, size.Width, size.Height, size.Unit, size.Fit));
}
if (settings.SelectedSizeIndex >= settings.Sizes.Count)
{
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigCustomSelected, settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
}
}
/// <summary>
/// Prints usage information to the console.
/// </summary>
public static void PrintUsage()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_UsageTitle);
Console.WriteLine();
var cmd = new ImageResizerRootCommand();
// Print usage line
Console.WriteLine(Properties.Resources.CLI_UsageLine);
Console.WriteLine();
// Print options from the command definition
Console.WriteLine(Properties.Resources.CLI_UsageOptions);
foreach (var option in cmd.Options)
{
var aliases = string.Join(", ", option.Aliases);
var description = option.Description ?? string.Empty;
Console.WriteLine($" {aliases,-30} {description}");
}
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_UsageExamples);
Console.WriteLine(Properties.Resources.CLI_UsageExampleHelp);
Console.WriteLine(Properties.Resources.CLI_UsageExampleDimensions);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePercent);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePreset);
}
}
}

View File

@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -39,44 +40,78 @@ namespace ImageResizer.Models
_aiSuperResolutionService = null;
}
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
/// <summary>
/// Validates if a file path is a supported image format.
/// </summary>
/// <param name="path">The file path to validate.</param>
/// <returns>True if the path is valid and points to a supported image file.</returns>
private static bool IsValidImagePath(string path)
{
var batch = new ResizeBatch();
const string pipeNamePrefix = "\\\\.\\pipe\\";
string pipeName = null;
for (var i = 0; i < args?.Length; i++)
if (string.IsNullOrWhiteSpace(path))
{
if (args[i] == "/d")
{
batch.DestinationDirectory = args[++i];
continue;
}
else if (args[i].Contains(pipeNamePrefix))
{
pipeName = args[i].Substring(pipeNamePrefix.Length);
continue;
}
batch.Files.Add(args[i]);
return false;
}
if (string.IsNullOrEmpty(pipeName))
if (!File.Exists(path))
{
return false;
}
var ext = Path.GetExtension(path)?.ToLowerInvariant();
var validExtensions = new[]
{
".bmp", ".dib", ".gif", ".jfif", ".jpe", ".jpeg", ".jpg",
".jxr", ".png", ".rle", ".tif", ".tiff", ".wdp",
};
return validExtensions.Contains(ext);
}
/// <summary>
/// Creates a ResizeBatch from CliOptions.
/// </summary>
/// <param name="standardInput">Standard input stream for reading additional file paths.</param>
/// <param name="options">The parsed CLI options.</param>
/// <returns>A ResizeBatch instance.</returns>
public static ResizeBatch FromCliOptions(TextReader standardInput, CliOptions options)
{
var batch = new ResizeBatch
{
DestinationDirectory = options.DestinationDirectory,
};
foreach (var file in options.Files)
{
// Convert relative paths to absolute paths
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
if (IsValidImagePath(absolutePath))
{
batch.Files.Add(absolutePath);
}
}
if (string.IsNullOrEmpty(options.PipeName))
{
// NB: We read these from stdin since there are limits on the number of args you can have
// Only read from stdin if it's redirected (piped input), not from interactive terminal
string file;
if (standardInput != null)
if (standardInput != null && (Console.IsInputRedirected || !ReferenceEquals(standardInput, Console.In)))
{
while ((file = standardInput.ReadLine()) != null)
{
batch.Files.Add(file);
// Convert relative paths to absolute paths
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
if (IsValidImagePath(absolutePath))
{
batch.Files.Add(absolutePath);
}
}
}
}
else
{
using (NamedPipeClientStream pipeClient =
new NamedPipeClientStream(".", pipeName, PipeDirection.In))
new NamedPipeClientStream(".", options.PipeName, PipeDirection.In))
{
// Connect to the pipe or wait until the pipe is available.
pipeClient.Connect();
@@ -88,7 +123,10 @@ namespace ImageResizer.Models
// Display the read text to the console
while ((file = sr.ReadLine()) != null)
{
batch.Files.Add(file);
if (IsValidImagePath(file))
{
batch.Files.Add(file);
}
}
}
}
@@ -97,17 +135,26 @@ namespace ImageResizer.Models
return batch;
}
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
{
var options = CliOptions.Parse(args);
return FromCliOptions(standardInput, options);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken)
{
// NOTE: Settings.Default is captured once before parallel processing.
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
return Process(reportProgress, Settings.Default, cancellationToken);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken)
{
double total = Files.Count;
int completed = 0;
var errors = new ConcurrentBag<ResizeError>();
// NOTE: Settings.Default is captured once before parallel processing.
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
var settings = Settings.Default;
// TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async
// APIs and a custom SynchronizationContext
Parallel.ForEach(

View File

@@ -716,5 +716,437 @@ namespace ImageResizer.Properties {
return ResourceManager.GetString("Width", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Processing {0} files....
/// </summary>
public static string CLI_ProcessingFiles {
get {
return ResourceManager.GetString("CLI_ProcessingFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to [{0}%] {1}/{2} completed.
/// </summary>
public static string CLI_ProgressFormat {
get {
return ResourceManager.GetString("CLI_ProgressFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Completed with {0} error(s)..
/// </summary>
public static string CLI_CompletedWithErrors {
get {
return ResourceManager.GetString("CLI_CompletedWithErrors", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to All files processed successfully!.
/// </summary>
public static string CLI_AllFilesProcessed {
get {
return ResourceManager.GetString("CLI_AllFilesProcessed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No input files or pipe specified. Showing usage..
/// </summary>
public static string CLI_NoInputFiles {
get {
return ResourceManager.GetString("CLI_NoInputFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Warning: Size index {0} is invalid. Using custom size..
/// </summary>
public static string CLI_WarningInvalidSizeIndex {
get {
return ResourceManager.GetString("CLI_WarningInvalidSizeIndex", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Current Configuration:.
/// </summary>
public static string CLI_ConfigTitle {
get {
return ResourceManager.GetString("CLI_ConfigTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to General Settings:.
/// </summary>
public static string CLI_ConfigGeneralSettings {
get {
return ResourceManager.GetString("CLI_ConfigGeneralSettings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shrink only: {0}.
/// </summary>
public static string CLI_ConfigShrinkOnly {
get {
return ResourceManager.GetString("CLI_ConfigShrinkOnly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Replace original: {0}.
/// </summary>
public static string CLI_ConfigReplaceOriginal {
get {
return ResourceManager.GetString("CLI_ConfigReplaceOriginal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ignore orientation: {0}.
/// </summary>
public static string CLI_ConfigIgnoreOrientation {
get {
return ResourceManager.GetString("CLI_ConfigIgnoreOrientation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove metadata: {0}.
/// </summary>
public static string CLI_ConfigRemoveMetadata {
get {
return ResourceManager.GetString("CLI_ConfigRemoveMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Keep date modified: {0}.
/// </summary>
public static string CLI_ConfigKeepDateModified {
get {
return ResourceManager.GetString("CLI_ConfigKeepDateModified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to JPEG quality: {0}.
/// </summary>
public static string CLI_ConfigJpegQuality {
get {
return ResourceManager.GetString("CLI_ConfigJpegQuality", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PNG interlace: {0}.
/// </summary>
public static string CLI_ConfigPngInterlace {
get {
return ResourceManager.GetString("CLI_ConfigPngInterlace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TIFF compress: {0}.
/// </summary>
public static string CLI_ConfigTiffCompress {
get {
return ResourceManager.GetString("CLI_ConfigTiffCompress", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Filename format: {0}.
/// </summary>
public static string CLI_ConfigFilenameFormat {
get {
return ResourceManager.GetString("CLI_ConfigFilenameFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom Size:.
/// </summary>
public static string CLI_ConfigCustomSize {
get {
return ResourceManager.GetString("CLI_ConfigCustomSize", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Width: {0}.
/// </summary>
public static string CLI_ConfigWidth {
get {
return ResourceManager.GetString("CLI_ConfigWidth", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height: {0}.
/// </summary>
public static string CLI_ConfigHeight {
get {
return ResourceManager.GetString("CLI_ConfigHeight", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Fit mode: {0}.
/// </summary>
public static string CLI_ConfigFitMode {
get {
return ResourceManager.GetString("CLI_ConfigFitMode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preset Sizes: (* = currently selected).
/// </summary>
public static string CLI_ConfigPresetSizes {
get {
return ResourceManager.GetString("CLI_ConfigPresetSizes", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: {1} x {2} ({3}).
/// </summary>
public static string CLI_ConfigPresetSizeFormat {
get {
return ResourceManager.GetString("CLI_ConfigPresetSizeFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to → Custom size selected.
/// </summary>
public static string CLI_ConfigCustomSelected {
get {
return ResourceManager.GetString("CLI_ConfigCustomSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image Resizer CLI.
/// </summary>
public static string CLI_UsageTitle {
get {
return ResourceManager.GetString("CLI_UsageTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Usage: PowerToys.ImageResizer.exe [options] &lt;files&gt;.
/// </summary>
public static string CLI_UsageLine {
get {
return ResourceManager.GetString("CLI_UsageLine", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Options:.
/// </summary>
public static string CLI_UsageOptions {
get {
return ResourceManager.GetString("CLI_UsageOptions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Examples:.
/// </summary>
public static string CLI_UsageExamples {
get {
return ResourceManager.GetString("CLI_UsageExamples", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --help.
/// </summary>
public static string CLI_UsageExampleHelp {
get {
return ResourceManager.GetString("CLI_UsageExampleHelp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --width 800 --height 600 image.jpg.
/// </summary>
public static string CLI_UsageExampleDimensions {
get {
return ResourceManager.GetString("CLI_UsageExampleDimensions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --size 50 --unit percent *.jpg.
/// </summary>
public static string CLI_UsageExamplePercent {
get {
return ResourceManager.GetString("CLI_UsageExamplePercent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --size 2 image1.png image2.png.
/// </summary>
public static string CLI_UsageExamplePreset {
get {
return ResourceManager.GetString("CLI_UsageExamplePreset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Destination directory for resized images.
/// </summary>
public static string CLI_Option_Destination {
get {
return ResourceManager.GetString("CLI_Option_Destination", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output filename format (e.g., %1 (%2)).
/// </summary>
public static string CLI_Option_FileName {
get {
return ResourceManager.GetString("CLI_Option_FileName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image files to resize.
/// </summary>
public static string CLI_Option_Files {
get {
return ResourceManager.GetString("CLI_Option_Files", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to How to fit image: fill, fit, stretch.
/// </summary>
public static string CLI_Option_Fit {
get {
return ResourceManager.GetString("CLI_Option_Fit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height of the resized image in pixels.
/// </summary>
public static string CLI_Option_Height {
get {
return ResourceManager.GetString("CLI_Option_Height", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Display this help message.
/// </summary>
public static string CLI_Option_Help {
get {
return ResourceManager.GetString("CLI_Option_Help", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ignore image orientation metadata.
/// </summary>
public static string CLI_Option_IgnoreOrientation {
get {
return ResourceManager.GetString("CLI_Option_IgnoreOrientation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preserve the original file modification date.
/// </summary>
public static string CLI_Option_KeepDateModified {
get {
return ResourceManager.GetString("CLI_Option_KeepDateModified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Set JPEG quality level (1-100).
/// </summary>
public static string CLI_Option_Quality {
get {
return ResourceManager.GetString("CLI_Option_Quality", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove image metadata during resizing.
/// </summary>
public static string CLI_Option_RemoveMetadata {
get {
return ResourceManager.GetString("CLI_Option_RemoveMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Replace the original image file.
/// </summary>
public static string CLI_Option_Replace {
get {
return ResourceManager.GetString("CLI_Option_Replace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Display current configuration.
/// </summary>
public static string CLI_Option_ShowConfig {
get {
return ResourceManager.GetString("CLI_Option_ShowConfig", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only shrink images, do not enlarge.
/// </summary>
public static string CLI_Option_ShrinkOnly {
get {
return ResourceManager.GetString("CLI_Option_ShrinkOnly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use preset size by index (0-based).
/// </summary>
public static string CLI_Option_Size {
get {
return ResourceManager.GetString("CLI_Option_Size", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unit of measurement: pixel, percent, cm, inch.
/// </summary>
public static string CLI_Option_Unit {
get {
return ResourceManager.GetString("CLI_Option_Unit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Width of the resized image in pixels.
/// </summary>
public static string CLI_Option_Width {
get {
return ResourceManager.GetString("CLI_Option_Width", resourceCulture);
}
}
}
}

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