Compare commits

..

8 Commits

Author SHA1 Message Date
Shawn Yuan (from Dev Box)
f04a59d0c8 init 2025-12-29 12:04:25 +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
Kai Tao
d87dde132d Cmdpal extension: Powertoys extension for cmdpal (#44006)
<!-- 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
Installer built, and every command works as expected,
Now use sparse app deployment, so we don't need an extra msix

---------

Co-authored-by: kaitao-ms <kaitao1105@gmail.com>
2025-12-23 21:07:44 +08:00
283 changed files with 12197 additions and 1851 deletions

View File

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

View File

@@ -216,6 +216,7 @@ CImage
cla
CLASSDC
CLASSNOTAVAILABLE
CLEARTYPE
clickable
clickonce
CLIENTEDGE
@@ -253,6 +254,7 @@ colorhistory
colorhistorylimit
COLORKEY
colorref
Convs
comctl
comdlg
comexp
@@ -529,9 +531,12 @@ eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FARPROC
fdw
fdx
FErase
fesf
FFFF
FInc
Figma
FILEEXPLORER
fileexploreraddons
@@ -573,6 +578,7 @@ formatetc
FORPARSING
foundrylocal
FRAMECHANGED
FRestore
frm
FROMTOUCH
fsanitize
@@ -607,6 +613,7 @@ GETSCREENSAVERRUNNING
GETSECKEY
GETSTICKYKEYS
GETTEXTLENGTH
gfx
GHND
gitmodules
GMEM
@@ -657,6 +664,7 @@ hdwwiz
Helpline
helptext
HGFE
hgdiobj
hglobal
hhk
HHmmssfff
@@ -704,7 +712,7 @@ hotlight
hotspot
HPAINTBUFFER
HRAWINPUT
HREDRAW
hredraw
hres
hresult
hrgn
@@ -882,7 +890,7 @@ LINKOVERLAY
LINQTo
listview
LIVEDRAW
LIVEZOOM
livezoom
LLKH
llkhf
LMEM
@@ -911,6 +919,7 @@ LPBITMAPINFOHEADER
LPCFHOOKPROC
LPCITEMIDLIST
LPCLSID
lpch
lpcmi
LPCMINVOKECOMMANDINFO
LPCREATESTRUCT
@@ -935,6 +944,7 @@ lptpm
LPTR
LPTSTR
lpv
LPrivate
LPW
lpwcx
lpwndpl
@@ -1349,6 +1359,7 @@ ppv
ppwsz
prc
Prefixer
Premul
prependpath
prepopulate
prevhost
@@ -1904,7 +1915,7 @@ valuegenerator
variantassignment
VARTYPE
vcamp
VCENTER
vcenter
vcgtq
VCINSTALLDIR
Vcpkg
@@ -1936,7 +1947,7 @@ vorrq
VOS
vpaddlq
vqsubq
VREDRAW
vredraw
vreinterpretq
VSC
VSCBD

2
.gitignore vendored
View File

@@ -358,4 +358,4 @@ src/common/Telemetry/*.etl
/src/settings-ui/Settings.UI/Assets/Settings/search.index.json
# PowerToysInstaller Build Temp Files
installer/*/*.wxs.bk
installer/*/*.wxs.bk

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",
@@ -235,6 +237,14 @@
"PowerToys.CmdPalModuleInterface.dll",
"CmdPalKeyboardService.dll",
"PowerToys.ModuleContracts.dll",
"Awake.ModuleServices.dll",
"ColorPicker.ModuleServices.dll",
"Workspaces.ModuleServices.dll",
"Microsoft.CommandPalette.Extensions.dll",
"Microsoft.CommandPalette.Extensions.Toolkit.dll",
"Microsoft.CmdPal.Ext.PowerToys.dll",
"Microsoft.CmdPal.Ext.PowerToys.exe",
"*Microsoft.CmdPal.UI_*.msix",
"PowerToys.DSC.dll",
@@ -358,9 +368,13 @@
"boost_regex-vc143-mt-x32-1_87.dll",
"boost_regex-vc143-mt-x64-1_87.dll",
"Microsoft.ML.OnnxRuntime.dll",
"UnitsNet.dll",
"UtfUnknown.dll",
"Wpf.Ui.dll"
"Wpf.Ui.dll",
"Shmuelie.WinRTServer.dll",
"ToolGood.Words.Pinyin.dll"
],
"SigningInfo": {
"Operations": [

View File

@@ -624,4 +624,4 @@ jobs:
- publish: $(JobOutputDirectory)
artifact: $(JobOutputArtifactName)-failure-$(System.JobAttempt)
displayName: Publish failure logs
condition: or(failed(), canceled())
condition: or(failed(), canceled())

View File

@@ -104,4 +104,4 @@ if ($totalFailure -gt 0) {
exit 1
}
exit 0
exit 0

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

@@ -37,6 +37,7 @@
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
<PackageVersion Include="MessagePack" Version="3.1.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
@@ -94,6 +95,7 @@
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
<PackageVersion Include="SharpCompress" Version="0.37.2" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
<!-- Don't update SkiaSharp.Views.WinUI to version 3.* branch as this brakes the HexBox control in Registry Preview. -->
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="2.88.9" />
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
@@ -145,4 +147,4 @@
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1560,6 +1560,7 @@ SOFTWARE.
- ReverseMarkdown
- ScipBe.Common.Office.OneNote
- SharpCompress
- Shmuelie.WinRTServer
- SkiaSharp.Views.WinUI
- StreamJsonRpc
- StyleCop.Analyzers

View File

@@ -44,6 +44,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/common/SettingsAPI/SettingsAPI.vcxproj" Id="6955446d-23f7-4023-9bb3-8657f904af99" />
<Project Path="src/common/Themes/Themes.vcxproj" Id="98537082-0fdb-40de-abd8-0dc5a4269bab" />
<Project Path="src/common/UITestAutomation/UITestAutomation.csproj">
@@ -156,6 +160,10 @@
<Project Path="src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.vcxproj" Id="48a0a19e-a0be-4256-acf8-cc3b80291af9" />
</Folder>
<Folder Name="/modules/awake/">
<Project Path="src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/awake/Awake/Awake.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -166,6 +174,10 @@
<Project Path="src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj" Id="0014d652-901f-4456-8d65-06fc5f997fb0" />
</Folder>
<Folder Name="/modules/colorpicker/">
<Project Path="src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/colorPicker/ColorPicker/ColorPicker.vcxproj" Id="655c9af2-18d3-4da6-80e4-85504a7722ba">
<BuildDependency Project="src/common/logger/logger.vcxproj" />
</Project>
@@ -206,6 +218,11 @@
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -442,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">
@@ -932,6 +953,10 @@
<Project Path="src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj" Id="2d604c07-51fc-46bb-9eb7-75aecc7f5e81" />
</Folder>
<Folder Name="/modules/Workspaces/">
<Project Path="src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

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

@@ -7,11 +7,18 @@
<Fragment>
<DirectoryRef Id="INSTALLFOLDER">
<Component Id="Microsoft_CommandPalette_Extensions_winmd" Guid="304AD25A-A986-4058-940E-61DB79EBD78C" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Microsoft_CommandPalette_Extensions_winmd" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="Microsoft.CommandPalette.Extensions.winmd" Source="$(var.BinDir)Microsoft.CommandPalette.Extensions.winmd" />
</Component>
<!-- Generated by generateFileComponents.ps1 -->
<!--BaseApplicationsFiles_Component_Def-->
</DirectoryRef>
<ComponentGroup Id="BaseApplicationsComponentGroup">
<ComponentRef Id="Microsoft_CommandPalette_Extensions_winmd" />
</ComponentGroup>
</Fragment>

View File

@@ -173,4 +173,4 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
</ItemGroup>
</Target>
<Target Name="Restore" />
</Project>
</Project>

View File

@@ -10,7 +10,8 @@
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10"
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai">
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai com">
<Identity
Name="Microsoft.PowerToys.SparseApp"
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
@@ -30,6 +31,7 @@
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.26226.0" />
</Dependencies>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
<systemai:Capability Name="systemAIModels"/>
<rescap:Capability Name="unvirtualizedResources"/>
@@ -66,5 +68,42 @@
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.CmdPalExtension" Executable="Microsoft.CmdPal.Ext.PowerToys.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.CommandPaletteExtension"
Description="PowerToys Command Palette Extension"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
<Extensions>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Microsoft.CmdPal.Ext.PowerToys.exe" Arguments="-RegisterProcessAsComServer" DisplayName="PowerToys Command Palette Extension">
<com:Class Id="7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17" DisplayName="PowerToys Command Palette Extension" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
<uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.commandpalette"
Id="PowerToys"
PublicFolder="Public"
DisplayName="PowerToys"
Description="Surface PowerToys commands inside Command Palette">
<uap3:Properties>
<CmdPalProvider>
<Activation>
<CreateInstance ClassId="7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17" />
</Activation>
<SupportedInterfaces>
<Commands/>
</SupportedInterfaces>
</CmdPalProvider>
</uap3:Properties>
</uap3:AppExtension>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
</Package>
</Package>

View File

@@ -2,8 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using ManagedCommon;
namespace Common.UI
{
@@ -120,28 +122,33 @@ namespace Common.UI
}
}
public static void OpenSettings(SettingsWindow window, bool mainExecutableIsOnTheParentFolder)
// What about debug build? Should also consider debug build, maybe tray window message?
public static void OpenSettings(SettingsWindow window)
{
try
{
var directoryPath = System.AppContext.BaseDirectory;
if (mainExecutableIsOnTheParentFolder)
var exePath = Path.Combine(
PowerToysPathResolver.GetPowerToysInstallPath(),
"PowerToys.exe");
if (exePath == null || !File.Exists(exePath))
{
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
directoryPath = Path.Combine(directoryPath, "..");
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
else
{
// PowerToys.exe is in the same path as the application.
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
Logger.LogError($"Failed to find powertoys exe path, {exePath}");
return;
}
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
var args = "--open-settings=" + SettingsWindowNameToString(window);
Process.Start(new ProcessStartInfo
{
FileName = exePath,
Arguments = args,
UseShellExecute = false,
});
}
catch
catch (Exception ex)
{
// TODO(stefan): Log exception once unified logging is implemented
Logger.LogError(ex.Message);
}
}
}

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

@@ -0,0 +1,168 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Versioning;
using Microsoft.Win32;
namespace ManagedCommon
{
[SupportedOSPlatform("windows")]
public class PowerToysPathResolver
{
private const string PowerToysRegistryKey = @"Software\Classes\powertoys";
private const string PowerToysExe = "PowerToys.exe";
/// <summary>
/// Gets the PowerToys installation path by checking registry entries
/// </summary>
/// <returns>The path to PowerToys installation directory, or null if not found</returns>
public static string GetPowerToysInstallPath()
{
#if DEBUG
// In debug builds, resolve directly from the running process (no installer/registry involved).
return GetPathFromCurrentProcess();
#else
// Try to get path from Per-User installation first
string path = GetPathFromRegistry(RegistryHive.CurrentUser);
if (!string.IsNullOrEmpty(path))
{
return path;
}
// Fall back to Per-Machine installation
path = GetPathFromRegistry(RegistryHive.LocalMachine);
if (!string.IsNullOrEmpty(path))
{
return path;
}
return null;
#endif
}
private static string GetPathFromRegistry(RegistryHive hive)
{
try
{
using var baseKey = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64);
// First try to get path from the powertoys protocol registration
string path = GetPathFromProtocolRegistration(baseKey);
if (!string.IsNullOrEmpty(path))
{
return path;
}
}
catch (Exception)
{
// Ignore registry access errors
}
return null;
}
private static string GetPathFromProtocolRegistration(RegistryKey baseKey)
{
try
{
using var key = baseKey.OpenSubKey($@"{PowerToysRegistryKey}\shell\open\command");
if (key != null)
{
string command = key.GetValue(string.Empty)?.ToString();
if (!string.IsNullOrEmpty(command))
{
// Parse command like: "C:\Program Files\PowerToys\PowerToys.exe" "%1"
return ExtractPathFromCommand(command);
}
}
}
catch (Exception)
{
// Ignore registry access errors
}
return null;
}
private static string GetPathFromCurrentProcess()
{
try
{
// If we're running inside PowerToys.exe (dev/debug builds), use the executable location.
var processPath = Process.GetCurrentProcess().MainModule?.FileName;
if (!string.IsNullOrEmpty(processPath))
{
var processDir = Path.GetDirectoryName(processPath);
if (!string.IsNullOrEmpty(processDir) && File.Exists(Path.Combine(processDir, PowerToysExe)))
{
return processDir;
}
}
// As a fallback, walk up from AppContext.BaseDirectory to find PowerToys.exe.
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory != null)
{
var candidate = Path.Combine(directory.FullName, PowerToysExe);
if (File.Exists(candidate))
{
return directory.FullName;
}
directory = directory.Parent;
}
}
catch
{
// Ignore reflection/process permission errors; caller will see null and handle accordingly.
}
return null;
}
private static string ExtractPathFromCommand(string command)
{
if (string.IsNullOrEmpty(command))
{
return null;
}
try
{
// Handle quoted paths: "C:\Program Files\PowerToys\PowerToys.exe" "%1"
if (command.StartsWith('\"'))
{
int endQuote = command.IndexOf('\"', 1);
if (endQuote > 1)
{
string exePath = command.Substring(1, endQuote - 1);
if (File.Exists(exePath))
{
return Path.GetDirectoryName(exePath);
}
}
}
else
{
// Handle unquoted paths (less common)
string[] parts = command.Split(' ');
if (parts.Length > 0 && File.Exists(parts[0]))
{
return Path.GetDirectoryName(parts[0]);
}
}
}
catch (Exception)
{
// Ignore path parsing errors
}
return null;
}
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Common.UI;
namespace PowerToys.ModuleContracts;
/// <summary>
/// Base contract for PowerToys modules exposed to the Command Palette.
/// </summary>
public interface IModuleService
{
/// <summary>
/// Gets module identifier (e.g., Workspaces, Awake).
/// </summary>
string Key { get; }
Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default);
Task<OperationResult> OpenSettingsAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// Helper base to reduce duplication for simple modules.
/// </summary>
public abstract class ModuleServiceBase : IModuleService
{
public abstract string Key { get; }
protected abstract SettingsDeepLink.SettingsWindow SettingsWindow { get; }
public abstract Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default);
public virtual Task<OperationResult> OpenSettingsAsync(CancellationToken cancellationToken = default)
{
try
{
SettingsDeepLink.OpenSettings(SettingsWindow);
return Task.FromResult(OperationResult.Ok());
}
catch (Exception ex)
{
return Task.FromResult(OperationResult.Fail($"Failed to open settings for {Key}: {ex.Message}"));
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerToys.ModuleContracts;
/// <summary>
/// Lightweight result type for module operations.
/// </summary>
public readonly record struct OperationResult(bool Success, string? Error = null)
{
public static OperationResult Ok() => new(true, null);
public static OperationResult Fail(string error) => new(false, error);
}
/// <summary>
/// Result type with a payload.
/// </summary>
public readonly record struct OperationResult<T>(bool Success, T? Value, string? Error = null);
/// <summary>
/// Factory helpers for creating operation results.
/// </summary>
public static class OperationResults
{
public static OperationResult<T> Ok<T>(T value) => new(true, value, null);
public static OperationResult<T> Fail<T>(string error) => new(false, default, error);
}

View File

@@ -0,0 +1,16 @@
<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.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common.UI\Common.UI.csproj" />
</ItemGroup>
</Project>

View File

@@ -75,10 +75,62 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE;
}
hstring Constants::AdvancedPasteShowUIEvent()
{
return CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT;
}
hstring Constants::AdvancedPasteTerminateAppMessage()
{
return CommonSharedConstants::ADVANCED_PASTE_TERMINATE_APP_MESSAGE;
}
hstring Constants::AlwaysOnTopPinEvent()
{
return CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT;
}
hstring Constants::FindMyMouseTriggerEvent()
{
return CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT;
}
hstring Constants::MouseHighlighterTriggerEvent()
{
return CommonSharedConstants::MOUSE_HIGHLIGHTER_TRIGGER_EVENT;
}
hstring Constants::MouseCrosshairsTriggerEvent()
{
return CommonSharedConstants::MOUSE_CROSSHAIRS_TRIGGER_EVENT;
}
hstring Constants::CursorWrapTriggerEvent()
{
return CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT;
}
hstring Constants::LightSwitchToggleEvent()
{
return CommonSharedConstants::LIGHTSWITCH_TOGGLE_EVENT;
}
hstring Constants::ZoomItZoomEvent()
{
return CommonSharedConstants::ZOOMIT_ZOOM_EVENT;
}
hstring Constants::ZoomItDrawEvent()
{
return CommonSharedConstants::ZOOMIT_DRAW_EVENT;
}
hstring Constants::ZoomItBreakEvent()
{
return CommonSharedConstants::ZOOMIT_BREAK_EVENT;
}
hstring Constants::ZoomItLiveZoomEvent()
{
return CommonSharedConstants::ZOOMIT_LIVEZOOM_EVENT;
}
hstring Constants::ZoomItSnipEvent()
{
return CommonSharedConstants::ZOOMIT_SNIP_EVENT;
}
hstring Constants::ZoomItRecordEvent()
{
return CommonSharedConstants::ZOOMIT_RECORD_EVENT;
}
hstring Constants::ShowPowerOCRSharedEvent()
{
return CommonSharedConstants::SHOW_POWEROCR_SHARED_EVENT;

View File

@@ -23,6 +23,20 @@ namespace winrt::PowerToys::Interop::implementation
static hstring AdvancedPasteAdditionalActionMessage();
static hstring AdvancedPasteCustomActionMessage();
static hstring AdvancedPasteTerminateAppMessage();
static hstring AdvancedPasteShowUIEvent();
static hstring AlwaysOnTopPinEvent();
static hstring MeasureToolTriggerEvent();
static hstring FindMyMouseTriggerEvent();
static hstring MouseHighlighterTriggerEvent();
static hstring MouseCrosshairsTriggerEvent();
static hstring CursorWrapTriggerEvent();
static hstring LightSwitchToggleEvent();
static hstring ZoomItZoomEvent();
static hstring ZoomItDrawEvent();
static hstring ZoomItBreakEvent();
static hstring ZoomItLiveZoomEvent();
static hstring ZoomItSnipEvent();
static hstring ZoomItRecordEvent();
static hstring ShowPowerOCRSharedEvent();
static hstring TerminatePowerOCRSharedEvent();
static hstring MouseJumpShowPreviewEvent();
@@ -33,7 +47,6 @@ namespace winrt::PowerToys::Interop::implementation
static hstring PowerAccentExitEvent();
static hstring ShortcutGuideTriggerEvent();
static hstring RegistryPreviewTriggerEvent();
static hstring MeasureToolTriggerEvent();
static hstring GcodePreviewResizeEvent();
static hstring BgcodePreviewResizeEvent();
static hstring QoiPreviewResizeEvent();

View File

@@ -20,6 +20,19 @@ namespace PowerToys
static String AdvancedPasteAdditionalActionMessage();
static String AdvancedPasteCustomActionMessage();
static String AdvancedPasteTerminateAppMessage();
static String AdvancedPasteShowUIEvent();
static String AlwaysOnTopPinEvent();
static String FindMyMouseTriggerEvent();
static String MouseHighlighterTriggerEvent();
static String MouseCrosshairsTriggerEvent();
static String CursorWrapTriggerEvent();
static String LightSwitchToggleEvent();
static String ZoomItZoomEvent();
static String ZoomItDrawEvent();
static String ZoomItBreakEvent();
static String ZoomItLiveZoomEvent();
static String ZoomItSnipEvent();
static String ZoomItRecordEvent();
static String ShowPowerOCRSharedEvent();
static String TerminatePowerOCRSharedEvent();
static String MouseJumpShowPreviewEvent();
@@ -51,4 +64,4 @@ namespace PowerToys
static String ShowCmdPalEvent();
}
}
}
}

View File

@@ -40,6 +40,8 @@ namespace CommonSharedConstants
const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction";
const wchar_t ADVANCED_PASTE_TERMINATE_APP_MESSAGE[] = L"TerminateApp";
const wchar_t ADVANCED_PASTE_SHOW_UI_EVENT[] = L"Local\\PowerToys_AdvancedPaste_ShowUI";
// Path to the event used to show Color Picker
const wchar_t SHOW_COLOR_PICKER_SHARED_EVENT[] = L"Local\\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525";
@@ -83,12 +85,21 @@ namespace CommonSharedConstants
const wchar_t TERMINATE_MOUSE_JUMP_SHARED_EVENT[] = L"Local\\TerminateMouseJumpEvent-252fa337-317f-4c37-a61f-99464c3f9728";
// Paths to the events used by other Mouse Utilities
const wchar_t FIND_MY_MOUSE_TRIGGER_EVENT[] = L"Local\\FindMyMouseTriggerEvent-5a9dc5f4-1c74-4f2f-a66f-1b9b6a2f9b23";
const wchar_t MOUSE_HIGHLIGHTER_TRIGGER_EVENT[] = L"Local\\MouseHighlighterTriggerEvent-1e3c9c3d-3fdf-4f9a-9a52-31c9b3c3a8f4";
const wchar_t MOUSE_CROSSHAIRS_TRIGGER_EVENT[] = L"Local\\MouseCrosshairsTriggerEvent-0d4c7f92-0a5c-4f5c-b64b-8a2a2f7e0b21";
const wchar_t CURSOR_WRAP_TRIGGER_EVENT[] = L"Local\\CursorWrapTriggerEvent-1f8452b5-4e6e-45b3-8b09-13f14a5900c9";
// Path to the event used by RegistryPreview
const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687";
// Path to the event used by MeasureTool
const wchar_t MEASURE_TOOL_TRIGGER_EVENT[] = L"Local\\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199";
// Path to the event used by LightSwitch
const wchar_t LIGHTSWITCH_TOGGLE_EVENT[] = L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a";
// Path to the event used by GcodePreviewHandler
const wchar_t GCODE_PREVIEW_RESIZE_EVENT[] = L"Local\\PowerToysGcodePreviewResizeEvent-6ff1f9bd-ccbd-4b24-a79f-40a34fb0317d";
@@ -130,6 +141,12 @@ namespace CommonSharedConstants
// Path to the events used by ZoomIt
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
const wchar_t ZOOMIT_ZOOM_EVENT[] = L"Local\\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393";
const wchar_t ZOOMIT_DRAW_EVENT[] = L"Local\\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975";
const wchar_t ZOOMIT_BREAK_EVENT[] = L"Local\\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b";
const wchar_t ZOOMIT_LIVEZOOM_EVENT[] = L"Local\\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d";
const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30";
const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";

View File

@@ -3,78 +3,128 @@
#include <functional>
#include <thread>
#include <string>
#include <atomic>
#include <windows.h>
/// <summary>
/// A reusable utility class that listens for a named Windows event and invokes a callback when triggered.
/// Provides RAII-based resource management for event handles and the listener thread.
/// The thread is properly joined on destruction to ensure clean shutdown.
/// </summary>
class EventWaiter
{
public:
EventWaiter() {}
EventWaiter(const std::wstring& name, std::function<void(DWORD)> callback)
EventWaiter() = default;
EventWaiter(const EventWaiter&) = delete;
EventWaiter& operator=(const EventWaiter&) = delete;
EventWaiter(EventWaiter&&) = delete;
EventWaiter& operator=(EventWaiter&&) = delete;
~EventWaiter()
{
// Create localExitThreadEvent and localWaitingEvent for capturing. We cannot capture 'this' as we implement move constructor.
auto localExitThreadEvent = exitThreadEvent = CreateEvent(nullptr, false, false, nullptr);
HANDLE localWaitingEvent = waitingEvent = CreateEvent(nullptr, false, false, name.c_str());
std::thread([=]() {
HANDLE events[2] = { localWaitingEvent, localExitThreadEvent };
while (true)
stop();
}
/// <summary>
/// Starts listening for the specified named event. When the event is signaled, the callback is invoked.
/// </summary>
/// <param name="name">The name of the Windows event to listen for.</param>
/// <param name="callback">The callback function to invoke when the event is triggered. Receives ERROR_SUCCESS on success.</param>
/// <returns>true if listening started successfully, false otherwise.</returns>
bool start(const std::wstring& name, std::function<void(DWORD)> callback)
{
if (m_listening)
{
return false;
}
m_exitThreadEvent = CreateEventW(nullptr, false, false, nullptr);
m_waitingEvent = CreateEventW(nullptr, false, false, name.c_str());
if (!m_exitThreadEvent || !m_waitingEvent)
{
cleanup();
return false;
}
m_listening = true;
m_eventThread = std::thread([this, cb = std::move(callback)]() {
HANDLE events[2] = { m_waitingEvent, m_exitThreadEvent };
while (m_listening)
{
auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE);
if (!m_listening)
{
break;
}
if (waitResult == WAIT_OBJECT_0 + 1)
{
// Exit event signaled
break;
}
if (waitResult == WAIT_FAILED)
{
callback(GetLastError());
cb(GetLastError());
continue;
}
if (waitResult == WAIT_OBJECT_0)
{
callback(ERROR_SUCCESS);
cb(ERROR_SUCCESS);
}
}
}).detach();
});
return true;
}
EventWaiter(EventWaiter&) = delete;
EventWaiter& operator=(EventWaiter&) = delete;
EventWaiter(EventWaiter&& a) noexcept
/// <summary>
/// Stops listening for the event and cleans up resources.
/// Waits for the listener thread to finish before returning.
/// Safe to call multiple times.
/// </summary>
void stop()
{
this->exitThreadEvent = a.exitThreadEvent;
this->waitingEvent = a.waitingEvent;
a.exitThreadEvent = nullptr;
a.waitingEvent = nullptr;
}
EventWaiter& operator=(EventWaiter&& a) noexcept
{
this->exitThreadEvent = a.exitThreadEvent;
this->waitingEvent = a.waitingEvent;
a.exitThreadEvent = nullptr;
a.waitingEvent = nullptr;
return *this;
}
~EventWaiter()
{
if (exitThreadEvent)
m_listening = false;
if (m_exitThreadEvent)
{
SetEvent(exitThreadEvent);
CloseHandle(exitThreadEvent);
SetEvent(m_exitThreadEvent);
}
if (waitingEvent)
if (m_eventThread.joinable())
{
CloseHandle(waitingEvent);
m_eventThread.join();
}
cleanup();
}
/// <summary>
/// Returns whether the listener is currently active.
/// </summary>
bool is_listening() const
{
return m_listening;
}
private:
HANDLE exitThreadEvent = nullptr;
HANDLE waitingEvent = nullptr;
void cleanup()
{
if (m_exitThreadEvent)
{
CloseHandle(m_exitThreadEvent);
m_exitThreadEvent = nullptr;
}
if (m_waitingEvent)
{
CloseHandle(m_waitingEvent);
m_waitingEvent = nullptr;
}
}
HANDLE m_exitThreadEvent = nullptr;
HANDLE m_waitingEvent = nullptr;
std::thread m_eventThread;
std::atomic_bool m_listening{ false };
};

View File

@@ -45,6 +45,7 @@
<Target Name="GenerateDscResourceJsonFiles" AfterTargets="Build" Condition="'$(CIBuild)' != 'true'">
<Message Text="Generating DSC resource JSON files to DSCModules subfolder..." Importance="high" />
<MakeDir Directories="$(TargetDir)DSCModules" />
<Exec Command="dotnet &quot;$(TargetPath)&quot; manifest --resource settings --outputDir &quot;$(TargetDir)DSCModules&quot;" />
</Target>
</Project>

View File

@@ -335,7 +335,6 @@
<converters:CountToVisibilityConverter x:Key="CountToVisibilityConverter" />
<converters:CountToInvertedVisibilityConverter x:Key="CountToInvertedVisibilityConverter" />
<converters:ServiceTypeToIconConverter x:Key="ServiceTypeToIconConverter" />
<converters:PasteAIUsageToStringConverter x:Key="PasteAIUsageToStringConverter" />
</ResourceDictionary>
</UserControl.Resources>
<Grid x:Name="PromptBoxGrid" Loaded="Grid_Loaded">
@@ -431,52 +430,12 @@
Grid.Row="1"
MinHeight="104"
MaxHeight="320">
<StackPanel>
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.CustomFormatResult, Mode=OneWay}"
TextWrapping="Wrap"
Visibility="{x:Bind ViewModel.HasCustomFormatText, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<Image
HorizontalAlignment="Left"
Source="{x:Bind ViewModel.CustomFormatImageResult, Mode=OneWay}"
Stretch="Uniform"
Visibility="{x:Bind ViewModel.HasCustomFormatImage, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<Grid Visibility="{x:Bind ViewModel.HasCustomFormatAudio, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{x:Bind ViewModel.AudioFileName, Mode=OneWay}" HorizontalAlignment="Left" Margin="0,0,0,8" />
<Grid Grid.Row="1" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{x:Bind ViewModel.AudioPositionString, Mode=OneWay}" VerticalAlignment="Center" Margin="0,0,8,0" />
<Slider Grid.Column="1" Minimum="0" Maximum="{x:Bind ViewModel.AudioDuration, Mode=OneWay}" Value="{x:Bind ViewModel.AudioPosition, Mode=TwoWay}" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Text="{x:Bind ViewModel.AudioDurationString, Mode=OneWay}" VerticalAlignment="Center" Margin="8,0,0,0" />
</Grid>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Command="{x:Bind ViewModel.PlayPauseAudioCommand}">
<FontIcon Glyph="{x:Bind ViewModel.AudioPlayPauseGlyph, Mode=OneWay}" />
</Button>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<Button Command="{x:Bind ViewModel.SaveAudioCommand}" Content="Save" />
<Button Command="{x:Bind ViewModel.DeleteAudioCommand}" Content="Delete" />
</StackPanel>
</Grid>
</Grid>
</StackPanel>
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.CustomFormatResult, Mode=OneWay}"
TextWrapping="Wrap" />
</ScrollViewer>
</Grid>
<Rectangle
@@ -643,37 +602,20 @@
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ServiceType, Mode=OneWay}" />
</StackPanel>
<StackPanel
<Border
Grid.Column="2"
Padding="2,0,2,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="4">
<Border
Padding="2,0,2,0"
VerticalAlignment="Center"
BorderBrush="{ThemeResource ControlStrokeColorSecondary}"
BorderThickness="1"
CornerRadius="{StaticResource ControlCornerRadius}">
<TextBlock
AutomationProperties.AccessibilityView="Raw"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Usage, Mode=OneWay, Converter={StaticResource PasteAIUsageToStringConverter}}" />
</Border>
<Border
Padding="2,0,2,0"
VerticalAlignment="Center"
BorderBrush="{ThemeResource ControlStrokeColorSecondary}"
BorderThickness="1"
CornerRadius="{StaticResource ControlCornerRadius}"
Visibility="{x:Bind IsLocalModel, Mode=OneWay}">
<TextBlock
x:Uid="LocalModelBadge"
AutomationProperties.AccessibilityView="Raw"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</Border>
</StackPanel>
BorderBrush="{ThemeResource ControlStrokeColorSecondary}"
BorderThickness="1"
CornerRadius="{StaticResource ControlCornerRadius}"
Visibility="{x:Bind IsLocalModel, Mode=OneWay}">
<TextBlock
x:Uid="LocalModelBadge"
AutomationProperties.AccessibilityView="Raw"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</Border>
<!--<Border
Grid.Column="2"
Padding="2,0,2,0"

View File

@@ -164,7 +164,7 @@ namespace AdvancedPaste.Controls
return;
}
var flyout = AIProviderButton.Flyout;
var flyout = FlyoutBase.GetAttachedFlyout(AIProviderButton);
if (AIProviderListView.SelectedItem is not PasteAIProviderDefinition provider)
{
@@ -180,6 +180,7 @@ namespace AdvancedPaste.Controls
if (ViewModel.SetActiveProviderCommand.CanExecute(provider))
{
await ViewModel.SetActiveProviderCommand.ExecuteAsync(provider);
SyncProviderSelection();
}
flyout?.Hide();

View File

@@ -1,30 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using AdvancedPaste.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml.Data;
namespace AdvancedPaste.Converters;
public sealed partial class PasteAIUsageToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var usage = value switch
{
string s => PasteAIUsageExtensions.FromConfigString(s),
PasteAIUsage u => u,
_ => PasteAIUsage.ChatCompletion,
};
return ResourceLoaderInstance.ResourceLoader.GetString($"PasteAIUsage_{usage}_Label");
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -46,13 +46,6 @@ internal static class DataPackageHelpers
return dataPackage;
}
internal static DataPackage CreateFromImage(RandomAccessStreamReference imageStreamRef)
{
DataPackage dataPackage = new();
dataPackage.SetBitmap(imageStreamRef);
return dataPackage;
}
internal static async Task<DataPackage> CreateFromFileAsync(string fileName)
{
var storageFile = await StorageFile.GetFileFromPathAsync(fileName);
@@ -250,29 +243,6 @@ internal static class DataPackageHelpers
return memoryStream.ToArray();
}
internal static async Task<(byte[] Data, string MimeType)> GetAudioBytesAsync(this DataPackageView dataPackageView)
{
if (dataPackageView.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await dataPackageView.GetStorageItemsAsync();
var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null;
if (file != null)
{
var supportedAudioTypes = SupportedFileTypes.Value.FirstOrDefault(x => x.Format == ClipboardFormat.Audio).FileTypes;
if (supportedAudioTypes != null && supportedAudioTypes.Contains(file.FileType))
{
using var stream = await file.OpenStreamForReadAsync();
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
return (memoryStream.ToArray(), file.ContentType);
}
}
}
return (null, null);
}
internal static async Task<SoftwareBitmap> GetImageContentAsync(this DataPackageView dataPackageView)
{
using var stream = await dataPackageView.GetImageStreamAsync();
@@ -309,11 +279,7 @@ internal static class DataPackageHelpers
var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null;
if (file != null)
{
var supportedImageTypes = SupportedFileTypes.Value.FirstOrDefault(x => x.Format == ClipboardFormat.Image).FileTypes;
if (supportedImageTypes != null && supportedImageTypes.Contains(file.FileType))
{
return await file.OpenReadAsync();
}
return await file.OpenReadAsync();
}
}

View File

@@ -118,8 +118,8 @@ public enum PasteFormats
IconGlyph = "\uE945",
RequiresAIService = true,
CanPreview = true,
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Image | ClipboardFormat.Audio,
KernelFunctionDescription = "Takes user instructions and applies them to the current clipboard content (text, image or audio). Use this function for image analysis, description, or transformation tasks beyond simple OCR.",
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Image,
KernelFunctionDescription = "Takes user instructions and applies them to the current clipboard content (text or image). Use this function for image analysis, description, or transformation tasks beyond simple OCR.",
RequiresPrompt = true)]
CustomTextTransformation,
}

View File

@@ -40,15 +40,15 @@ namespace AdvancedPaste.Services.CustomActions
this.userSettings = userSettings;
}
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, byte[] audioBytes, string audioMimeType, CancellationToken cancellationToken, IProgress<double> progress)
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress)
{
var pasteConfig = userSettings?.PasteAIConfiguration;
var providerConfig = BuildProviderConfig(pasteConfig);
return await TransformAsync(prompt, inputText, imageBytes, audioBytes, audioMimeType, providerConfig, cancellationToken, progress);
return await TransformAsync(prompt, inputText, imageBytes, providerConfig, cancellationToken, progress);
}
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, byte[] audioBytes, string audioMimeType, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
{
ArgumentNullException.ThrowIfNull(providerConfig);
@@ -57,7 +57,7 @@ namespace AdvancedPaste.Services.CustomActions
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
}
if (string.IsNullOrWhiteSpace(inputText) && imageBytes is null && audioBytes is null)
if (string.IsNullOrWhiteSpace(inputText) && imageBytes is null)
{
Logger.LogWarning("Clipboard has no usable data");
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
@@ -82,8 +82,6 @@ namespace AdvancedPaste.Services.CustomActions
InputText = inputText,
ImageBytes = imageBytes,
ImageMimeType = imageBytes != null ? "image/png" : null,
AudioBytes = audioBytes,
AudioMimeType = audioMimeType,
SystemPrompt = systemPrompt,
};
@@ -170,10 +168,6 @@ namespace AdvancedPaste.Services.CustomActions
ModelPath = provider.ModelPath,
SystemPrompt = systemPrompt,
ModerationEnabled = provider.ModerationEnabled,
Usage = provider.UsageKind,
ImageWidth = provider.ImageWidth,
ImageHeight = provider.ImageHeight,
Voice = provider.Voice,
};
return providerConfig;

View File

@@ -12,6 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
{
public interface ICustomActionTransformService
{
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, byte[] audioBytes, string audioMimeType, CancellationToken cancellationToken, IProgress<double> progress);
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress);
}
}

View File

@@ -28,13 +28,5 @@ namespace AdvancedPaste.Services.CustomActions
public string SystemPrompt { get; set; }
public bool ModerationEnabled { get; set; }
public PasteAIUsage Usage { get; set; }
public string Voice { get; set; }
public int ImageWidth { get; set; }
public int ImageHeight { get; set; }
}
}

View File

@@ -16,10 +16,6 @@ namespace AdvancedPaste.Services.CustomActions
public string ImageMimeType { get; init; }
public byte[] AudioBytes { get; init; }
public string AudioMimeType { get; init; }
public string SystemPrompt { get; init; }
public AIServiceUsage Usage { get; set; } = AIServiceUsage.None;

View File

@@ -4,23 +4,18 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AudioToText;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.AzureAIInference;
using Microsoft.SemanticKernel.Connectors.Google;
using Microsoft.SemanticKernel.Connectors.MistralAI;
using Microsoft.SemanticKernel.Connectors.Ollama;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.TextToAudio;
using Microsoft.SemanticKernel.TextToImage;
namespace AdvancedPaste.Services.CustomActions
{
@@ -70,129 +65,14 @@ namespace AdvancedPaste.Services.CustomActions
var prompt = request.Prompt;
var inputText = request.InputText;
var imageBytes = request.ImageBytes;
var audioBytes = request.AudioBytes;
if (string.IsNullOrWhiteSpace(prompt) || (string.IsNullOrWhiteSpace(inputText) && imageBytes is null && audioBytes is null))
if (string.IsNullOrWhiteSpace(prompt) || (string.IsNullOrWhiteSpace(inputText) && imageBytes is null))
{
throw new ArgumentException("Prompt and input content must be provided", nameof(request));
}
var executionSettings = CreateExecutionSettings();
var kernel = CreateKernel();
switch (_config.Usage)
{
case PasteAIUsage.TextToImage:
var imageDescription = string.IsNullOrWhiteSpace(prompt) ? inputText : $"{inputText}. {prompt}";
return await ProcessTextToImageAsync(kernel, imageDescription, cancellationToken);
case PasteAIUsage.TextToAudio:
var textToAudioInput = string.IsNullOrWhiteSpace(prompt) ? inputText : $"{inputText}. {prompt}";
return await ProcessTextToAudioAsync(kernel, textToAudioInput, cancellationToken);
case PasteAIUsage.AudioToText:
return await ProcessAudioToTextAsync(kernel, request, cancellationToken);
case PasteAIUsage.ChatCompletion:
default:
var userMessageContent = $"""
User instructions:
{prompt}
Clipboard Content:
{inputText}
Output:
""";
return await ProcessChatCompletionAsync(kernel, request, userMessageContent, systemPrompt, cancellationToken);
}
}
private async Task<string> ProcessTextToImageAsync(Kernel kernel, string userMessageContent, CancellationToken cancellationToken)
{
#pragma warning disable SKEXP0001
var imageService = kernel.GetRequiredService<ITextToImageService>();
var width = _config.ImageWidth > 0 ? _config.ImageWidth : 1024;
var height = _config.ImageHeight > 0 ? _config.ImageHeight : 1024;
var settings = new OpenAITextToImageExecutionSettings
{
Size = (width, height),
};
var generatedImages = await imageService.GetImageContentsAsync(new TextContent(userMessageContent), settings, cancellationToken: cancellationToken);
if (generatedImages.Count == 0)
{
throw new InvalidOperationException("No image generated.");
}
var imageContent = generatedImages[0];
if (imageContent.Data.HasValue)
{
var base64 = Convert.ToBase64String(imageContent.Data.Value.ToArray());
return $"data:{imageContent.MimeType ?? "image/png"};base64,{base64}";
}
else if (imageContent.Uri != null)
{
using var client = new HttpClient();
var imageBytes = await client.GetByteArrayAsync(imageContent.Uri, cancellationToken);
var base64 = Convert.ToBase64String(imageBytes);
return $"data:image/png;base64,{base64}";
}
else
{
throw new InvalidOperationException("Generated image contains no data.");
}
#pragma warning restore SKEXP0001
}
private async Task<string> ProcessTextToAudioAsync(Kernel kernel, string text, CancellationToken cancellationToken)
{
#pragma warning disable SKEXP0001
var audioService = kernel.GetRequiredService<ITextToAudioService>();
var settings = new OpenAITextToAudioExecutionSettings
{
Voice = _config.Voice,
ResponseFormat = "mp3",
};
var audioContent = await audioService.GetAudioContentAsync(text, settings, cancellationToken: cancellationToken);
if (audioContent.Data.HasValue)
{
var tempPath = Path.GetTempPath();
var fileName = $"AdvancedPaste_Audio_{DateTime.Now:yyyyMMddHHmmss}.mp3";
var filePath = Path.Combine(tempPath, fileName);
await File.WriteAllBytesAsync(filePath, audioContent.Data.Value.ToArray(), cancellationToken);
return filePath;
}
else
{
throw new InvalidOperationException("Generated audio contains no data.");
}
#pragma warning restore SKEXP0001
}
private async Task<string> ProcessAudioToTextAsync(Kernel kernel, PasteAIRequest request, CancellationToken cancellationToken)
{
#pragma warning disable SKEXP0001
var audioService = kernel.GetRequiredService<IAudioToTextService>();
if (request.AudioBytes == null || request.AudioBytes.Length == 0)
{
throw new ArgumentException("Audio content must be provided", nameof(request));
}
var audioContent = new AudioContent(request.AudioBytes, request.AudioMimeType);
var textContent = await audioService.GetTextContentAsync(audioContent, null, cancellationToken: cancellationToken);
return textContent.Text;
#pragma warning restore SKEXP0001
}
private async Task<string> ProcessChatCompletionAsync(Kernel kernel, PasteAIRequest request, string userMessageContent, string systemPrompt, CancellationToken cancellationToken)
{
var executionSettings = CreateExecutionSettings();
var modelId = _config.Model;
IChatCompletionService chatService;
@@ -215,20 +95,29 @@ namespace AdvancedPaste.Services.CustomActions
var chatHistory = new ChatHistory();
chatHistory.AddSystemMessage(systemPrompt);
if (request.ImageBytes != null)
if (imageBytes != null)
{
var collection = new ChatMessageContentItemCollection();
if (!string.IsNullOrWhiteSpace(request.InputText))
if (!string.IsNullOrWhiteSpace(inputText))
{
collection.Add(new TextContent($"Clipboard Content:\n{request.InputText}"));
collection.Add(new TextContent($"Clipboard Content:\n{inputText}"));
}
collection.Add(new ImageContent(request.ImageBytes, request.ImageMimeType ?? "image/png"));
collection.Add(new TextContent($"User instructions:\n{request.Prompt}\n\nOutput:"));
collection.Add(new ImageContent(imageBytes, request.ImageMimeType ?? "image/png"));
collection.Add(new TextContent($"User instructions:\n{prompt}\n\nOutput:"));
chatHistory.AddUserMessage(collection);
}
else
{
var userMessageContent = $"""
User instructions:
{prompt}
Clipboard Content:
{inputText}
Output:
""";
chatHistory.AddUserMessage(userMessageContent);
}
@@ -253,55 +142,11 @@ namespace AdvancedPaste.Services.CustomActions
switch (_serviceType)
{
case AIServiceType.OpenAI:
if (_config.Usage == PasteAIUsage.TextToImage)
{
#pragma warning disable SKEXP0010
kernelBuilder.AddOpenAITextToImage(apiKey, modelId: _config.Model);
#pragma warning restore SKEXP0010
}
else if (_config.Usage == PasteAIUsage.TextToAudio)
{
#pragma warning disable SKEXP0010
kernelBuilder.AddOpenAITextToAudio(_config.Model, apiKey);
#pragma warning restore SKEXP0010
}
else if (_config.Usage == PasteAIUsage.AudioToText)
{
#pragma warning disable SKEXP0010
kernelBuilder.AddOpenAIAudioToText(_config.Model, apiKey);
#pragma warning restore SKEXP0010
}
else
{
kernelBuilder.AddOpenAIChatCompletion(_config.Model, apiKey, serviceId: _config.Model);
}
kernelBuilder.AddOpenAIChatCompletion(_config.Model, apiKey, serviceId: _config.Model);
break;
case AIServiceType.AzureOpenAI:
var deploymentName = string.IsNullOrWhiteSpace(_config.DeploymentName) ? _config.Model : _config.DeploymentName;
if (_config.Usage == PasteAIUsage.TextToImage)
{
#pragma warning disable SKEXP0010
kernelBuilder.AddAzureOpenAITextToImage(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey);
#pragma warning restore SKEXP0010
}
else if (_config.Usage == PasteAIUsage.TextToAudio)
{
#pragma warning disable SKEXP0010
kernelBuilder.AddAzureOpenAITextToAudio(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey);
#pragma warning restore SKEXP0010
}
else if (_config.Usage == PasteAIUsage.AudioToText)
{
#pragma warning disable SKEXP0010
kernelBuilder.AddAzureOpenAIAudioToText(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey);
#pragma warning restore SKEXP0010
}
else
{
kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey, serviceId: _config.Model);
}
kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey, serviceId: _config.Model);
break;
case AIServiceType.Mistral:
kernelBuilder.AddMistralChatCompletion(_config.Model, apiKey: apiKey);

View File

@@ -341,16 +341,15 @@ public abstract class KernelServiceBase(
async dataPackageView =>
{
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
var audio = await dataPackageView.GetAudioBytesAsync();
var input = await dataPackageView.GetTextOrHtmlTextAsync();
if (string.IsNullOrEmpty(input) && imageBytes == null && audio.Data == null)
if (string.IsNullOrEmpty(input) && imageBytes == null)
{
// If we have no text and no image, try to get text via OCR or throw if nothing exists
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
}
var result = await _customActionTransformService.TransformAsync(fixedPrompt, input, imageBytes, audio.Data, audio.MimeType, kernel.GetCancellationToken(), kernel.GetProgress());
var result = await _customActionTransformService.TransformAsync(fixedPrompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty);
});
@@ -361,22 +360,21 @@ public abstract class KernelServiceBase(
async dataPackageView =>
{
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
var audio = await dataPackageView.GetAudioBytesAsync();
var input = await dataPackageView.GetTextOrHtmlTextAsync();
if (string.IsNullOrEmpty(input) && imageBytes == null && audio.Data == null)
if (string.IsNullOrEmpty(input) && imageBytes == null)
{
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
}
string output = await GetPromptBasedOutput(format, prompt, input, imageBytes, audio.Data, audio.MimeType, kernel.GetCancellationToken(), kernel.GetProgress());
string output = await GetPromptBasedOutput(format, prompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
return DataPackageHelpers.CreateFromText(output);
});
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, byte[] imageBytes, byte[] audioBytes, string audioMimeType, CancellationToken cancellationToken, IProgress<double> progress) =>
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress) =>
format switch
{
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformAsync(prompt, input, imageBytes, audioBytes, audioMimeType, cancellationToken, progress))?.Content ?? string.Empty,
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformAsync(prompt, input, imageBytes, cancellationToken, progress))?.Content ?? string.Empty,
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
};

View File

@@ -34,26 +34,12 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
// Run on thread-pool; although we use Async routines consistently, some actions still occasionally take a long time without yielding.
return await Task.Run(async () =>
{
if (pasteFormat.Format == PasteFormats.CustomTextTransformation)
{
var audio = await clipboardData.GetAudioBytesAsync();
return DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(
pasteFormat.Prompt,
await clipboardData.GetTextOrHtmlTextAsync(),
await clipboardData.GetImageAsPngBytesAsync(),
audio.Data,
audio.MimeType,
cancellationToken,
progress))?.Content ?? string.Empty);
}
return pasteFormat.Format switch
pasteFormat.Format switch
{
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress))?.Content ?? string.Empty),
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
};
});
});
}
private static void WriteTelemetry(PasteFormats format, PasteActionSource source)

View File

@@ -372,16 +372,4 @@
<value>Unable to load Foundry Local model: {0}</value>
<comment>{0} is the model identifier. Do not translate {0}.</comment>
</data>
<data name="PasteAIUsage_ChatCompletion_Label" xml:space="preserve">
<value>Chat completion</value>
</data>
<data name="PasteAIUsage_TextToImage_Label" xml:space="preserve">
<value>Text to image</value>
</data>
<data name="PasteAIUsage_TextToAudio_Label" xml:space="preserve">
<value>Text to audio</value>
</data>
<data name="PasteAIUsage_AudioToText_Label" xml:space="preserve">
<value>Audio to text</value>
</data>
</root>

View File

@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Runtime.InteropServices;
@@ -28,8 +27,6 @@ using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.System;
using WinUIEx;
@@ -274,60 +271,6 @@ namespace AdvancedPaste.ViewModels
OnPropertyChanged(nameof(CurrentIndexDisplay));
};
PlayPauseAudioCommand = new RelayCommand(PlayPauseAudio);
SaveAudioCommand = new RelayCommand(SaveAudio);
DeleteAudioCommand = new RelayCommand(DeleteAudio);
_audioTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
_audioTimer.Tick += (s, e) =>
{
// Notify property change to update UI, but avoid triggering the setter logic
// The setter logic checks for significant difference, so it should be fine,
// but to be safe we are just notifying here.
OnPropertyChanged(nameof(AudioPosition));
OnPropertyChanged(nameof(AudioPositionString));
};
_audioPlayer = new MediaPlayer();
_audioPlayer.MediaOpened += (s, e) =>
{
_ = _dispatcherQueue.TryEnqueue(() =>
{
OnPropertyChanged(nameof(AudioDuration));
OnPropertyChanged(nameof(AudioDurationString));
});
};
_audioPlayer.PlaybackSession.PlaybackStateChanged += (s, e) =>
{
_ = _dispatcherQueue.TryEnqueue(() =>
{
OnPropertyChanged(nameof(IsAudioPlaying));
OnPropertyChanged(nameof(AudioPlayPauseGlyph));
if (s.PlaybackState == MediaPlaybackState.Playing)
{
_audioTimer.Start();
}
else
{
_audioTimer.Stop();
}
});
};
_audioPlayer.MediaEnded += (s, e) =>
{
_ = _dispatcherQueue.TryEnqueue(() =>
{
s.Position = TimeSpan.Zero;
// s.PlaybackState = MediaPlaybackState.Paused; // Read-only
_audioPlayer.Pause();
OnPropertyChanged(nameof(AudioPosition));
OnPropertyChanged(nameof(AudioPositionString));
OnPropertyChanged(nameof(IsAudioPlaying));
OnPropertyChanged(nameof(AudioPlayPauseGlyph));
});
};
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
UpdateOpenAIKey();
_clipboardTimer = new() { Interval = TimeSpan.FromSeconds(1) };
@@ -481,27 +424,7 @@ namespace AdvancedPaste.ViewModels
public void Dispose()
{
_clipboardTimer.Stop();
_userSettings.Changed -= UserSettings_Changed;
_pasteActionCancellationTokenSource?.Dispose();
_audioPlayer?.Dispose();
_audioTimer?.Stop();
// Cleanup any temporary audio files
foreach (var response in GeneratedResponses)
{
if (response.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase) && File.Exists(response))
{
try
{
File.Delete(response);
}
catch (Exception ex)
{
Logger.LogError($"Failed to delete temporary audio file: {response}", ex);
}
}
}
GC.SuppressFinalize(this);
}
@@ -635,23 +558,6 @@ namespace AdvancedPaste.ViewModels
}
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
// Cleanup any temporary audio files from previous session
foreach (var response in GeneratedResponses)
{
if (response.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase) && File.Exists(response))
{
try
{
File.Delete(response);
}
catch (Exception ex)
{
Logger.LogError($"Failed to delete temporary audio file: {response}", ex);
}
}
}
GeneratedResponses.Clear();
}
@@ -708,101 +614,8 @@ namespace AdvancedPaste.ViewModels
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasCustomFormatImage))]
[NotifyPropertyChangedFor(nameof(HasCustomFormatText))]
[NotifyPropertyChangedFor(nameof(CustomFormatImageResult))]
private string _customFormatResult;
public bool HasCustomFormatImage => CustomFormatResult?.StartsWith("data:image", StringComparison.OrdinalIgnoreCase) ?? false;
public bool HasCustomFormatAudio => CustomFormatResult?.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase) ?? false;
public bool HasCustomFormatText => !HasCustomFormatImage && !HasCustomFormatAudio;
public ImageSource CustomFormatImageResult
{
get
{
if (HasCustomFormatImage && !string.IsNullOrEmpty(CustomFormatResult))
{
try
{
var base64Data = CustomFormatResult.Split(',')[1];
var bytes = Convert.FromBase64String(base64Data);
var stream = new System.IO.MemoryStream(bytes);
var image = new BitmapImage();
image.SetSource(stream.AsRandomAccessStream());
return image;
}
catch (Exception ex)
{
Logger.LogError("Failed to create image source from data URI", ex);
}
}
return null;
}
}
private MediaPlayer _audioPlayer;
private DispatcherTimer _audioTimer;
public string AudioFileName => HasCustomFormatAudio ? Path.GetFileName(CustomFormatResult) : string.Empty;
public double AudioDuration => _audioPlayer?.PlaybackSession.NaturalDuration.TotalSeconds ?? 0;
public double AudioPosition
{
get => _audioPlayer?.PlaybackSession.Position.TotalSeconds ?? 0;
set
{
if (_audioPlayer != null)
{
if (Math.Abs(_audioPlayer.PlaybackSession.Position.TotalSeconds - value) > 0.5)
{
_audioPlayer.PlaybackSession.Position = TimeSpan.FromSeconds(value);
OnPropertyChanged(nameof(AudioPosition)); // Only notify if we actually changed the position
}
OnPropertyChanged(nameof(AudioPositionString));
}
}
}
public string AudioDurationString => TimeSpan.FromSeconds(AudioDuration).ToString(@"mm\:ss", CultureInfo.InvariantCulture);
public string AudioPositionString => TimeSpan.FromSeconds(AudioPosition).ToString(@"mm\:ss", CultureInfo.InvariantCulture);
public bool IsAudioPlaying => _audioPlayer?.PlaybackSession.PlaybackState == MediaPlaybackState.Playing;
public string AudioPlayPauseGlyph => IsAudioPlaying ? "\uE769" : "\uE768";
public IRelayCommand PlayPauseAudioCommand { get; }
public IRelayCommand SaveAudioCommand { get; }
public IRelayCommand DeleteAudioCommand { get; }
public MediaSource CustomFormatAudioResult
{
get
{
if (HasCustomFormatAudio && !string.IsNullOrEmpty(CustomFormatResult))
{
try
{
return MediaSource.CreateFromUri(new Uri(CustomFormatResult));
}
catch (Exception ex)
{
Logger.LogError("Failed to create audio source from file path", ex);
}
}
return null;
}
}
[RelayCommand]
public async Task PasteCustomAsync()
{
@@ -810,25 +623,7 @@ namespace AdvancedPaste.ViewModels
if (!string.IsNullOrEmpty(text))
{
if (text.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
{
try
{
var base64Data = text.Split(',')[1];
var bytes = Convert.FromBase64String(base64Data);
var stream = new System.IO.MemoryStream(bytes);
var dataPackage = DataPackageHelpers.CreateFromImage(Windows.Storage.Streams.RandomAccessStreamReference.CreateFromStream(stream.AsRandomAccessStream()));
await CopyPasteAndHideAsync(dataPackage);
}
catch (Exception ex)
{
Logger.LogError("Failed to paste image from data URI", ex);
}
}
else
{
await CopyPasteAndHideAsync(DataPackageHelpers.CreateFromText(text));
}
await CopyPasteAndHideAsync(DataPackageHelpers.CreateFromText(text));
}
}
@@ -866,7 +661,7 @@ namespace AdvancedPaste.ViewModels
[RelayCommand]
public void OpenSettings()
{
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste, true);
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste);
GetMainWindow()?.Close();
}
@@ -1100,6 +895,11 @@ namespace AdvancedPaste.ViewModels
Logger.LogError("Failed to activate AI provider", ex);
return;
}
UpdateAIProviderActiveFlags();
OnPropertyChanged(nameof(AIProviders));
NotifyActiveProviderChanged();
EnqueueRefreshPasteFormats();
}
public async Task CancelPasteActionAsync()
@@ -1122,119 +922,5 @@ namespace AdvancedPaste.ViewModels
TransformProgress = value;
});
}
partial void OnCustomFormatResultChanged(string value)
{
OnPropertyChanged(nameof(HasCustomFormatAudio));
OnPropertyChanged(nameof(CustomFormatAudioResult));
OnPropertyChanged(nameof(AudioFileName));
if (HasCustomFormatAudio)
{
try
{
if (_audioPlayer != null)
{
// Ensure we are on the UI thread if needed, though OnCustomFormatResultChanged is likely called on UI thread.
// Reset player state
_audioPlayer.Pause();
_audioPlayer.Source = MediaSource.CreateFromUri(new Uri(value));
}
}
catch (Exception ex)
{
Logger.LogError("Failed to set audio source", ex);
}
}
else
{
if (_audioPlayer != null)
{
_audioPlayer.Pause();
_audioPlayer.Source = null;
}
}
}
private void PlayPauseAudio()
{
if (_audioPlayer == null)
{
return;
}
if (_audioPlayer.PlaybackSession.PlaybackState == MediaPlaybackState.Playing)
{
_audioPlayer.Pause();
}
else
{
_audioPlayer.Play();
}
}
private async void SaveAudio()
{
if (!HasCustomFormatAudio || string.IsNullOrEmpty(CustomFormatResult))
{
return;
}
var mainWindow = GetMainWindow();
if (mainWindow == null)
{
return;
}
var savePicker = new Windows.Storage.Pickers.FileSavePicker();
savePicker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.Downloads;
savePicker.FileTypeChoices.Add("Audio", new List<string>() { ".mp3" });
savePicker.SuggestedFileName = Path.GetFileName(CustomFormatResult);
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(mainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(savePicker, hwnd);
var file = await savePicker.PickSaveFileAsync();
if (file != null)
{
try
{
File.Copy(CustomFormatResult, file.Path, true);
}
catch (Exception ex)
{
Logger.LogError("Failed to save audio file", ex);
}
}
}
private void DeleteAudio()
{
if (HasCustomFormatAudio && !string.IsNullOrEmpty(CustomFormatResult))
{
try
{
if (File.Exists(CustomFormatResult))
{
File.Delete(CustomFormatResult);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to delete audio file", ex);
}
GeneratedResponses.Remove(CustomFormatResult);
if (GeneratedResponses.Count > 0)
{
CurrentResponseIndex = Math.Max(0, CurrentResponseIndex - 1);
}
else
{
CustomFormatResult = null;
PreviewRequested?.Invoke(this, EventArgs.Empty);
}
}
}
}
}

View File

@@ -15,9 +15,11 @@
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
#include <common/utils/gpo.h>
#include <common/utils/EventWaiter.h>
#include <algorithm>
#include <cwctype>
#include <thread>
#include <vector>
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
@@ -101,6 +103,9 @@ private:
bool m_is_advanced_ai_enabled = false;
bool m_preview_custom_format_output = true;
// Event listening for external triggers (e.g., from CmdPal extension)
EventWaiter m_triggerEventWaiter;
Hotkey parse_single_hotkey(const wchar_t* keyName, const winrt::Windows::Data::Json::JsonObject& settingsObject)
{
try
@@ -779,6 +784,17 @@ public:
Trace::AdvancedPaste_Enable(true);
m_enabled = true;
m_process_manager.start();
// Start listening for external trigger event so we can invoke the same logic as the hotkey.
// Note: Use start() directly instead of constructor + move assignment to avoid dangling this pointer in the thread.
m_triggerEventWaiter.start(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT, [this](DWORD) {
// Same logic as hotkeyId == 1 (m_advanced_paste_ui_hotkey)
Logger::trace(L"AdvancedPaste ShowUI event triggered");
m_process_manager.start();
m_process_manager.bring_to_front();
m_process_manager.send_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE);
Trace::AdvancedPaste_Invoked(L"AdvancedPasteUIEvent");
});
};
void Disable(bool traceEvent)
@@ -787,6 +803,9 @@ public:
{
m_process_manager.stop();
// Stop event listening
m_triggerEventWaiter.stop();
if (traceEvent)
{
Trace::AdvancedPaste_Enable(false);

View File

@@ -146,7 +146,7 @@ public:
}
}
m_showEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT, [&](int err) {
m_showEventWaiter.start(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT, [&](DWORD err) {
if (m_enabled && err == ERROR_SUCCESS)
{
Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT);
@@ -164,7 +164,7 @@ public:
}
});
m_showAdminEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT, [&](int err) {
m_showAdminEventWaiter.start(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT, [&](DWORD err) {
if (m_enabled && err == ERROR_SUCCESS)
{
Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT);

View File

@@ -67,7 +67,7 @@ namespace Hosts
services.AddSingleton<IElevationHelper, ElevationHelper>();
services.AddSingleton<OpenSettingsFunction>(() =>
{
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts, true);
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts);
});
services.AddSingleton<MainViewModel, MainViewModel>();

View File

@@ -155,7 +155,7 @@ public:
}
}
m_showEventWaiter = EventWaiter(CommonSharedConstants::SHOW_HOSTS_EVENT, [&](int err)
m_showEventWaiter.start(CommonSharedConstants::SHOW_HOSTS_EVENT, [&](DWORD err)
{
if (m_enabled && err == ERROR_SUCCESS)
{
@@ -174,7 +174,7 @@ public:
}
});
m_showAdminEventWaiter = EventWaiter(CommonSharedConstants::SHOW_HOSTS_ADMIN_EVENT, [&](int err)
m_showAdminEventWaiter.start(CommonSharedConstants::SHOW_HOSTS_ADMIN_EVENT, [&](DWORD err)
{
if (m_enabled && err == ERROR_SUCCESS)
{

View File

@@ -168,9 +168,6 @@
<ClCompile>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
@@ -222,4 +219,4 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>
</Project>

View File

@@ -8,6 +8,8 @@
#include <codecvt>
#include <common/utils/logger_helper.h>
#include "ThemeHelper.h"
#include <thread>
#include <atomic>
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -103,12 +105,18 @@ private:
HANDLE m_force_light_event_handle;
HANDLE m_force_dark_event_handle;
HANDLE m_manual_override_event_handle;
HANDLE m_toggle_event_handle{ nullptr };
std::thread m_toggle_thread;
std::atomic<bool> m_toggle_thread_running{ false };
static const constexpr int NUM_DEFAULT_HOTKEYS = 4;
Hotkey m_toggle_theme_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'D' };
void init_settings();
void ToggleTheme();
void StartToggleListener();
void StopToggleListener();
public:
LightSwitchInterface()
@@ -118,6 +126,7 @@ public:
m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT");
m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK");
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
m_toggle_event_handle = CreateDefaultEvent(L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a");
init_settings();
};
@@ -130,6 +139,8 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
// Ensure worker threads/process handles are cleaned up before destruction
disable();
delete this;
}
@@ -444,6 +455,8 @@ public:
Logger::info(L"Light Switch process launched successfully (PID: {}).", pi.dwProcessId);
m_process = pi.hProcess;
CloseHandle(pi.hThread);
StartToggleListener();
}
// Disable the powertoy
@@ -469,6 +482,8 @@ public:
CloseHandle(m_process);
m_process = nullptr;
}
StopToggleListener();
}
// Returns if the powertoys is enabled
@@ -530,31 +545,8 @@ public:
}
else if (hotkeyId == 0)
{
// get current will return true if in light mode; otherwise false
Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme");
if (g_settings.m_changeSystem)
{
SetSystemTheme(!GetCurrentSystemTheme());
}
if (g_settings.m_changeApps)
{
SetAppsTheme(!GetCurrentAppsTheme());
}
if (!m_manual_override_event_handle)
{
m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
if (!m_manual_override_event_handle)
{
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
}
}
if (m_manual_override_event_handle)
{
SetEvent(m_manual_override_event_handle);
Logger::debug(L"[Light Switch] Manual override event set");
}
ToggleTheme();
}
return true;
@@ -567,8 +559,80 @@ public:
{
return WaitForSingleObject(m_process, 0) == WAIT_TIMEOUT;
}
};
void LightSwitchInterface::ToggleTheme()
{
if (g_settings.m_changeSystem)
{
SetSystemTheme(!GetCurrentSystemTheme());
}
if (g_settings.m_changeApps)
{
SetAppsTheme(!GetCurrentAppsTheme());
}
if (!m_manual_override_event_handle)
{
m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
if (!m_manual_override_event_handle)
{
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
}
}
if (m_manual_override_event_handle)
{
SetEvent(m_manual_override_event_handle);
Logger::debug(L"[Light Switch] Manual override event set");
}
}
void LightSwitchInterface::StartToggleListener()
{
if (m_toggle_thread_running || !m_toggle_event_handle)
{
return;
}
m_toggle_thread_running = true;
m_toggle_thread = std::thread([this]() {
while (m_toggle_thread_running)
{
const DWORD wait_result = WaitForSingleObject(m_toggle_event_handle, 500);
if (!m_toggle_thread_running)
{
break;
}
if (wait_result == WAIT_OBJECT_0)
{
ToggleTheme();
ResetEvent(m_toggle_event_handle);
}
}
});
}
void LightSwitchInterface::StopToggleListener()
{
if (!m_toggle_thread_running)
{
return;
}
m_toggle_thread_running = false;
if (m_toggle_event_handle)
{
SetEvent(m_toggle_event_handle);
}
if (m_toggle_thread.joinable())
{
m_toggle_thread.join();
}
}
std::wstring utf8_to_wstring(const std::string& str)
{
if (str.empty())
@@ -646,4 +710,4 @@ void LightSwitchInterface::init_settings()
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new LightSwitchInterface();
}
}

View File

@@ -149,7 +149,7 @@ public:
init_settings();
triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT);
triggerEventWaiter = EventWaiter(CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT, [this](int) {
triggerEventWaiter.start(CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT, [this](DWORD) {
on_hotkey(0);
});
}

View File

@@ -6,6 +6,7 @@
#include "../../../common/utils/resources.h"
#include "../../../common/logger/logger.h"
#include "../../../common/utils/logger_helper.h"
#include "../../../common/interop/shared_constants.h"
#include <atomic>
#include <thread>
#include <vector>
@@ -108,6 +109,12 @@ private:
// Hotkey
Hotkey m_activationHotkey{};
// Event-driven trigger support (for CmdPal/automation)
HANDLE m_triggerEventHandle = nullptr;
HANDLE m_terminateEventHandle = nullptr;
std::thread m_eventThread;
std::atomic_bool m_listening{ false };
public:
// Constructor
CursorWrap()
@@ -121,7 +128,8 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
StopMouseHook();
// Ensure hooks/threads/handles are torn down before deletion
disable();
g_cursorWrapInstance = nullptr; // Clear global instance pointer
delete this;
}
@@ -195,11 +203,54 @@ public:
{
m_enabled = true;
Trace::EnableCursorWrap(true);
// Always start the mouse hook when the module is enabled
// This ensures cursor wrapping is active immediately after enabling
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started");
// Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
m_triggerEventHandle = CreateEventW(nullptr, false, false, CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT);
m_terminateEventHandle = CreateEventW(nullptr, false, false, nullptr);
if (m_triggerEventHandle && m_terminateEventHandle)
{
m_listening = true;
m_eventThread = std::thread([this]() {
HANDLE handles[2] = { m_triggerEventHandle, m_terminateEventHandle };
// WH_MOUSE_LL callbacks are delivered to the thread that installed the hook.
// Ensure this thread has a message queue and pumps messages while the hook is active.
MSG msg;
PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started");
while (m_listening)
{
auto res = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT);
if (!m_listening)
{
break;
}
if (res == WAIT_OBJECT_0)
{
ToggleMouseHook();
}
else if (res == WAIT_OBJECT_0 + 1)
{
break;
}
else
{
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
StopMouseHook();
Logger::info("CursorWrap event listener stopped");
});
}
}
// Disable the powertoy
@@ -207,8 +258,26 @@ public:
{
m_enabled = false;
Trace::EnableCursorWrap(false);
StopMouseHook();
Logger::info("CursorWrap disabled - mouse hook stopped");
m_listening = false;
if (m_terminateEventHandle)
{
SetEvent(m_terminateEventHandle);
}
if (m_eventThread.joinable())
{
m_eventThread.join();
}
if (m_triggerEventHandle)
{
CloseHandle(m_triggerEventHandle);
m_triggerEventHandle = nullptr;
}
if (m_terminateEventHandle)
{
CloseHandle(m_terminateEventHandle);
m_terminateEventHandle = nullptr;
}
}
// Returns if the powertoys is enabled
@@ -240,7 +309,19 @@ public:
return false;
}
// Toggle cursor wrapping
// Toggle on the thread that owns the WH_MOUSE_LL hook (the event listener thread).
if (m_triggerEventHandle)
{
return SetEvent(m_triggerEventHandle);
}
return false;
}
private:
void ToggleMouseHook()
{
// Toggle cursor wrapping.
if (m_hookActive)
{
StopMouseHook();
@@ -253,11 +334,8 @@ public:
RunComprehensiveTests();
#endif
}
return true;
}
private:
// Load the settings file.
void init_settings()
{

View File

@@ -8,6 +8,8 @@
#include <common/utils/logger_helper.h>
#include <common/utils/color.h>
#include <common/utils/string_utils.h>
#include <common/utils/EventWaiter.h>
#include <common/interop/shared_constants.h>
namespace
{
@@ -69,6 +71,9 @@ private:
// Find My Mouse specific settings
FindMyMouseSettings m_findMyMouseSettings;
// Event-driven trigger support
EventWaiter m_triggerEventWaiter;
// Load initial settings from the persisted values.
void init_settings();
@@ -86,6 +91,8 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
// Ensure threads/handles are cleaned up before destruction
disable();
delete this;
}
@@ -150,6 +157,11 @@ public:
m_enabled = true;
Trace::EnableFindMyMouse(true);
std::thread([=]() { FindMyMouseMain(m_hModule, m_findMyMouseSettings); }).detach();
// Start listening for external trigger event so we can invoke the same logic as the hotkey.
m_triggerEventWaiter.start(CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT, [this](DWORD) {
OnHotkeyEx();
});
}
// Disable the powertoy
@@ -158,6 +170,8 @@ public:
m_enabled = false;
Trace::EnableFindMyMouse(false);
FindMyMouseDisable();
m_triggerEventWaiter.stop();
}
// Returns if the powertoys is enabled
@@ -216,7 +230,7 @@ inline static uint8_t LegacyOpacityToAlpha(int overlayOpacityPercent)
overlayOpacityPercent = 100;
}
// Round to nearest integer (0<EFBFBD>255)
// Round to nearest integer (0255)
return static_cast<uint8_t>((overlayOpacityPercent * 255 + 50) / 100);
}
@@ -532,4 +546,4 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new FindMyMouse();
}
}

View File

@@ -4,6 +4,8 @@
#include "trace.h"
#include "MouseHighlighter.h"
#include "common/utils/color.h"
#include <common/utils/EventWaiter.h>
#include <common/interop/shared_constants.h>
namespace
{
@@ -61,6 +63,9 @@ private:
// Mouse Highlighter specific settings
MouseHighlighterSettings m_highlightSettings;
// Event-driven trigger support
EventWaiter m_triggerEventWaiter;
public:
// Constructor
MouseHighlighter()
@@ -72,6 +77,8 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
// Tear down threads/handles before deletion to avoid abort() on joinable threads during shutdown
disable();
delete this;
}
@@ -132,6 +139,11 @@ public:
m_enabled = true;
Trace::EnableMouseHighlighter(true);
std::thread([=]() { MouseHighlighterMain(m_hModule, m_highlightSettings); }).detach();
// Start listening for external trigger event so we can invoke the same logic as the hotkey.
m_triggerEventWaiter.start(CommonSharedConstants::MOUSE_HIGHLIGHTER_TRIGGER_EVENT, [this](DWORD) {
OnHotkeyEx();
});
}
// Disable the powertoy
@@ -140,6 +152,8 @@ public:
m_enabled = false;
Trace::EnableMouseHighlighter(false);
MouseHighlighterDisable();
m_triggerEventWaiter.stop();
}
// Returns if the powertoys is enabled

View File

@@ -4,7 +4,8 @@
#include "trace.h"
#include "InclusiveCrosshairs.h"
#include "common/utils/color.h"
#include <atomic>
#include <common/utils/EventWaiter.h>
#include <common/interop/shared_constants.h>
#include <thread>
#include <chrono>
#include <memory>
@@ -124,6 +125,9 @@ private:
// Mouse Pointer Crosshairs specific settings
InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings;
// Event-driven trigger support
EventWaiter m_triggerEventWaiter;
public:
// Constructor
MousePointerCrosshairs()
@@ -137,11 +141,9 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
UninstallKeyboardHook();
StopXTimer();
StopYTimer();
// Ensure all background threads/handles are torn down before destruction to avoid std::terminate/abort on joinable threads
disable();
g_instance.store(nullptr, std::memory_order_release);
// Release shared state so worker threads (if any) exit when weak_ptr lock fails
m_state.reset();
delete this;
}
@@ -203,6 +205,11 @@ public:
m_enabled = true;
Trace::EnableMousePointerCrosshairs(true);
std::thread([=]() { InclusiveCrosshairsMain(m_hModule, m_inclusiveCrosshairsSettings); }).detach();
// Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
m_triggerEventWaiter.start(CommonSharedConstants::MOUSE_CROSSHAIRS_TRIGGER_EVENT, [this](DWORD) {
on_hotkey(0); // activation hotkey
});
}
// Disable the powertoy
@@ -215,6 +222,8 @@ public:
StopYTimer();
m_glideState = 0;
InclusiveCrosshairsDisable();
m_triggerEventWaiter.stop();
}
// Returns if the powertoys is enabled
@@ -901,4 +910,4 @@ private:
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new MousePointerCrosshairs();
}
}

View File

@@ -426,7 +426,7 @@ public partial class OCROverlay : Window
private void SettingsMenuItem_Click(object sender, RoutedEventArgs e)
{
WindowUtilities.CloseAllOCROverlays();
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerOCR, false);
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerOCR);
}
private static bool CheckIfCheckingOrUnchecking(object? sender)

View File

@@ -121,7 +121,7 @@ int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInst
else
{
auto mainThreadId = GetCurrentThreadId();
exitEventWaiter = EventWaiter(CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, [mainThreadId, &window](int err) {
exitEventWaiter.start(CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, [mainThreadId, &window](DWORD err) {
if (err != ERROR_SUCCESS)
{
Logger::error(L"Failed to wait for {} event. {}", CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, get_last_error_or_default(err));

View File

@@ -37,7 +37,7 @@ public:
}
triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT);
triggerEventWaiter = EventWaiter(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, [this](int) {
triggerEventWaiter.start(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, [this](DWORD) {
OnHotkeyEx();
});

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using PowerToys.ModuleContracts;
using WorkspacesCsharpLibrary.Data;
namespace Workspaces.ModuleServices;
/// <summary>
/// Workspaces-specific operations.
/// </summary>
public interface IWorkspaceService : IModuleService
{
Task<OperationResult> LaunchWorkspaceAsync(string workspaceId, CancellationToken cancellationToken = default);
Task<OperationResult> LaunchEditorAsync(CancellationToken cancellationToken = default);
Task<OperationResult> SnapshotAsync(string? targetPath = null, CancellationToken cancellationToken = default);
Task<OperationResult<IReadOnlyList<ProjectWrapper>>> GetWorkspacesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.IO;
using Common.UI;
using ManagedCommon;
using PowerToys.Interop;
using PowerToys.ModuleContracts;
using WorkspacesCsharpLibrary.Data;
namespace Workspaces.ModuleServices;
/// <summary>
/// Implementation of workspace actions for reuse across hosts.
/// </summary>
public sealed class WorkspaceService : ModuleServiceBase, IWorkspaceService
{
public static WorkspaceService Instance { get; } = new();
public override string Key => SettingsDeepLink.SettingsWindow.Workspaces.ToString();
protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.Workspaces;
public override Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default)
{
// Treat launch as invoking the Workspaces editor.
return LaunchEditorAsync(cancellationToken);
}
public Task<OperationResult> LaunchEditorAsync(CancellationToken cancellationToken = default)
{
try
{
using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.WorkspacesLaunchEditorEvent());
eventHandle.Set();
return Task.FromResult(OperationResult.Ok());
}
catch (Exception ex)
{
return Task.FromResult(OperationResult.Fail($"Failed to launch Workspaces editor: {ex.Message}"));
}
}
public Task<OperationResult> LaunchWorkspaceAsync(string workspaceId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(workspaceId))
{
return Task.FromResult(OperationResult.Fail("Workspace id is required."));
}
try
{
var powertoysBaseDir = PowerToysPathResolver.GetPowerToysInstallPath();
if (string.IsNullOrEmpty(powertoysBaseDir))
{
return Task.FromResult(OperationResult.Fail("PowerToys installation path not found."));
}
var launcherPath = Path.Combine(powertoysBaseDir, "PowerToys.WorkspacesLauncher.exe");
var startInfo = new ProcessStartInfo(launcherPath)
{
Arguments = workspaceId,
UseShellExecute = true,
};
Process.Start(startInfo);
return Task.FromResult(OperationResult.Ok());
}
catch (Exception ex)
{
return Task.FromResult(OperationResult.Fail($"Failed to launch workspace: {ex.Message}"));
}
}
public Task<OperationResult> SnapshotAsync(string? targetPath = null, CancellationToken cancellationToken = default)
{
// Snapshot orchestration is not yet exposed via events; provide a clear failure for now.
return Task.FromResult(OperationResult.Fail("Snapshot is not implemented for Workspaces."));
}
public Task<OperationResult<IReadOnlyList<ProjectWrapper>>> GetWorkspacesAsync(CancellationToken cancellationToken = default)
{
try
{
var items = WorkspacesStorage.Load();
return Task.FromResult(OperationResults.Ok<IReadOnlyList<ProjectWrapper>>(items));
}
catch (Exception ex)
{
return Task.FromResult(OperationResults.Fail<IReadOnlyList<ProjectWrapper>>($"Failed to read workspaces: {ex.Message}"));
}
}
}

View File

@@ -0,0 +1,20 @@
<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.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\PowerToys.ModuleContracts\PowerToys.ModuleContracts.csproj" />
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,70 @@
// 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.Text.Json.Serialization;
namespace WorkspacesCsharpLibrary.Data;
public struct ApplicationWrapper
{
public struct WindowPositionWrapper
{
[JsonPropertyName("x")]
public int X { get; set; }
[JsonPropertyName("y")]
public int Y { get; set; }
[JsonPropertyName("width")]
public int Width { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
}
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("application")]
public string Application { get; set; }
[JsonPropertyName("application-path")]
public string ApplicationPath { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("package-full-name")]
public string PackageFullName { get; set; }
[JsonPropertyName("app-user-model-id")]
public string AppUserModelId { get; set; }
[JsonPropertyName("pwa-app-id")]
public string PwaAppId { get; set; }
[JsonPropertyName("command-line-arguments")]
public string CommandLineArguments { get; set; }
[JsonPropertyName("is-elevated")]
public bool IsElevated { get; set; }
[JsonPropertyName("can-launch-elevated")]
public bool CanLaunchElevated { get; set; }
[JsonPropertyName("minimized")]
public bool Minimized { get; set; }
[JsonPropertyName("maximized")]
public bool Maximized { get; set; }
[JsonPropertyName("position")]
public WindowPositionWrapper Position { get; set; }
[JsonPropertyName("monitor")]
public int Monitor { get; set; }
[JsonPropertyName("version")]
public string Version { get; set; }
}

View File

@@ -2,13 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace WorkspacesEditor.Data
namespace WorkspacesCsharpLibrary.Data;
public enum InvokePoint
{
/* sync with workspaces-common */
public enum InvokePoint
{
EditorButton = 0,
Shortcut,
LaunchAndEdit,
}
EditorButton = 0,
Shortcut,
LaunchAndEdit,
CommandPaletteExtension,
}

View File

@@ -0,0 +1,43 @@
// 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.Text.Json.Serialization;
namespace WorkspacesCsharpLibrary.Data;
public struct MonitorConfigurationWrapper
{
public struct MonitorRectWrapper
{
[JsonPropertyName("top")]
public int Top { get; set; }
[JsonPropertyName("left")]
public int Left { get; set; }
[JsonPropertyName("width")]
public int Width { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
}
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("instance-id")]
public string InstanceId { get; set; }
[JsonPropertyName("monitor-number")]
public int MonitorNumber { get; set; }
[JsonPropertyName("dpi")]
public int Dpi { get; set; }
[JsonPropertyName("monitor-rect-dpi-aware")]
public MonitorRectWrapper MonitorRectDpiAware { get; set; }
[JsonPropertyName("monitor-rect-dpi-unaware")]
public MonitorRectWrapper MonitorRectDpiUnaware { get; set; }
}

View File

@@ -2,13 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.PowerToys.Settings.UI.Library
using WorkspacesCsharpLibrary.Data;
namespace WorkspacesCsharpLibrary.Data;
public class ProjectData : WorkspacesEditorData<ProjectWrapper>
{
public enum PasteAIUsage
{
ChatCompletion,
TextToImage,
TextToAudio,
AudioToText,
}
}

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.Collections.Generic;
namespace WorkspacesCsharpLibrary.Data;
public struct ProjectWrapper
{
public string Id { get; set; }
public string Name { get; set; }
public long CreationTime { get; set; }
public long LastLaunchedTime { get; set; }
public bool IsShortcutNeeded { get; set; }
public bool MoveExistingWindows { get; set; }
public List<MonitorConfigurationWrapper> MonitorConfiguration { get; set; }
public List<ApplicationWrapper> Applications { get; set; }
}

View File

@@ -2,9 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using WorkspacesEditor.Utils;
using WorkspacesCsharpLibrary.Data;
using WorkspacesCsharpLibrary.Utils;
namespace WorkspacesEditor.Data
namespace WorkspacesCsharpLibrary.Data
{
public class TempProjectData : ProjectData
{

View File

@@ -0,0 +1,27 @@
// 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.Collections.Generic;
using WorkspacesCsharpLibrary.Utils;
using static WorkspacesCsharpLibrary.Data.WorkspacesData;
namespace WorkspacesCsharpLibrary.Data;
public class WorkspacesData : WorkspacesEditorData<WorkspacesListWrapper>
{
public string File => FolderUtils.DataFolder() + "\\workspaces.json";
public struct WorkspacesListWrapper
{
public List<ProjectWrapper> Workspaces { get; set; }
}
public enum OrderBy
{
LastViewed = 0,
Created = 1,
Name = 2,
Unknown = 3,
}
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using WorkspacesCsharpLibrary.Utils;
namespace WorkspacesCsharpLibrary.Data;
/// <summary>
/// Shared JSON serializer helper for Workspaces payloads.
/// </summary>
public class WorkspacesEditorData<T>
{
[RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")]
[RequiresDynamicCode("JSON serialization uses reflection-based serializer.")]
public T Read(string file)
{
IOUtils ioUtils = new();
string data = ioUtils.ReadFile(file);
return JsonSerializer.Deserialize<T>(data, WorkspacesJsonOptions.EditorOptions)!;
}
[RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")]
[RequiresDynamicCode("JSON serialization uses reflection-based serializer.")]
public string Serialize(T data)
{
return JsonSerializer.Serialize(data, WorkspacesJsonOptions.EditorOptions);
}
[RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")]
[RequiresDynamicCode("JSON serialization uses reflection-based serializer.")]
public T Deserialize(string json)
{
return JsonSerializer.Deserialize<T>(json, WorkspacesJsonOptions.EditorOptions)!;
}
}

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.Text.Json;
using WorkspacesCsharpLibrary.Utils;
namespace WorkspacesCsharpLibrary.Data;
internal static class WorkspacesJsonOptions
{
internal static readonly JsonSerializerOptions EditorOptions = new()
{
PropertyNamingPolicy = new DashCaseNamingPolicy(),
WriteIndented = true,
};
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace WorkspacesCsharpLibrary.Data;
/// <summary>
/// Lightweight reader for persisted workspaces.
/// </summary>
public static class WorkspacesStorage
{
public static IReadOnlyList<ProjectWrapper> Load()
{
var filePath = GetDefaultFilePath();
if (!File.Exists(filePath))
{
return [];
}
try
{
var json = File.ReadAllText(filePath);
var data = JsonSerializer.Deserialize(json, WorkspacesStorageJsonContext.Default.WorkspacesFile);
if (data?.Workspaces == null)
{
return [];
}
return data.Workspaces
.Where(ws => !string.IsNullOrWhiteSpace(ws.Id) && !string.IsNullOrWhiteSpace(ws.Name))
.Select(ws => new ProjectWrapper
{
Id = ws.Id!,
Name = ws.Name!,
Applications = ws.Applications ?? new List<ApplicationWrapper>(),
CreationTime = ws.CreationTime,
LastLaunchedTime = ws.LastLaunchedTime,
IsShortcutNeeded = ws.IsShortcutNeeded,
MoveExistingWindows = ws.MoveExistingWindows,
MonitorConfiguration = ws.MonitorConfiguration ?? new List<MonitorConfigurationWrapper>(),
})
.ToList()
.AsReadOnly();
}
catch
{
return Array.Empty<ProjectWrapper>();
}
}
public static string GetDefaultFilePath()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(localAppData, "Microsoft", "PowerToys", "Workspaces", "workspaces.json");
}
internal sealed class WorkspacesFile
{
public List<WorkspaceProject> Workspaces { get; set; } = new();
}
internal sealed class WorkspaceProject
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("applications")]
public List<ApplicationWrapper> Applications { get; set; } = new();
[JsonPropertyName("monitor-configuration")]
public List<MonitorConfigurationWrapper> MonitorConfiguration { get; set; } = new();
[JsonPropertyName("creation-time")]
public long CreationTime { get; set; }
[JsonPropertyName("last-launched-time")]
public long LastLaunchedTime { get; set; }
[JsonPropertyName("is-shortcut-needed")]
public bool IsShortcutNeeded { get; set; }
[JsonPropertyName("move-existing-windows")]
public bool MoveExistingWindows { get; set; }
}
}

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.Text.Json.Serialization;
namespace WorkspacesCsharpLibrary.Data;
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(WorkspacesStorage.WorkspacesFile))]
[JsonSerializable(typeof(WorkspacesStorage.WorkspaceProject))]
[JsonSerializable(typeof(ApplicationWrapper))]
[JsonSerializable(typeof(ApplicationWrapper.WindowPositionWrapper))]
[JsonSerializable(typeof(MonitorConfigurationWrapper))]
[JsonSerializable(typeof(MonitorConfigurationWrapper.MonitorRectWrapper))]
internal sealed partial class WorkspacesStorageJsonContext : JsonSerializerContext
{
}

View File

@@ -16,7 +16,7 @@ using Windows.Management.Deployment;
namespace WorkspacesCsharpLibrary.Models
{
public class BaseApplication : INotifyPropertyChanged, IDisposable
public partial class BaseApplication : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;

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.Text.Json;
using WorkspacesCsharpLibrary.Utils;
namespace WorkspacesCsharpLibrary.Utils;
public class DashCaseNamingPolicy : JsonNamingPolicy
{
public static DashCaseNamingPolicy Instance { get; } = new DashCaseNamingPolicy();
public override string ConvertName(string name)
{
return name.UpperCamelCaseToDashCase();
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
namespace WorkspacesCsharpLibrary.Utils;
public class FolderUtils
{
public static string Desktop()
{
return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
}
public static string Temp()
{
return Path.GetTempPath();
}
// Note: the same path should be used in SnapshotTool and Launcher
public static string DataFolder()
{
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.IO.Abstractions;
using System.Threading.Tasks;
namespace WorkspacesCsharpLibrary.Utils;
public class IOUtils
{
private readonly IFileSystem _fileSystem = new FileSystem();
public void WriteFile(string fileName, string data)
{
_fileSystem.File.WriteAllText(fileName, data);
}
public string ReadFile(string fileName)
{
if (_fileSystem.File.Exists(fileName))
{
int attempts = 0;
while (attempts < 10)
{
try
{
using FileSystemStream inputStream = _fileSystem.File.Open(fileName, FileMode.Open);
using StreamReader reader = new(inputStream);
string data = reader.ReadToEnd();
inputStream.Close();
return data;
}
catch (Exception)
{
Task.Delay(10).Wait();
}
attempts++;
}
}
return string.Empty;
}
}

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.Linq;
namespace WorkspacesCsharpLibrary.Utils;
public static class StringUtils
{
public static string UpperCamelCaseToDashCase(this string str)
{
// If it's a single letter variable, leave it as it is
return str.Length == 1
? str
: string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x : x.ToString())).ToLowerInvariant();
}
}

View File

@@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.WorkspacesCsharpLibrary</AssemblyTitle>
<AssemblyDescription>PowerToys Workspaces Csharp Library</AssemblyDescription>
<Description>PowerToys Workspaces Csharp Library</Description>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@@ -15,4 +17,8 @@
<AssemblyName>PowerToys.WorkspacesCsharpLibrary</AssemblyName>
</PropertyGroup>
</Project>
<ItemGroup>
<PackageReference Include="System.IO.Abstractions" />
</ItemGroup>
</Project>

View File

@@ -1,101 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using static WorkspacesEditor.Data.ProjectData;
namespace WorkspacesEditor.Data
{
public class ProjectData : WorkspacesEditorData<ProjectWrapper>
{
public struct ApplicationWrapper
{
public struct WindowPositionWrapper
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
public string Id { get; set; }
public string Application { get; set; }
public string ApplicationPath { get; set; }
public string Title { get; set; }
public string PackageFullName { get; set; }
public string AppUserModelId { get; set; }
public string PwaAppId { get; set; }
public string CommandLineArguments { get; set; }
public bool IsElevated { get; set; }
public bool CanLaunchElevated { get; set; }
public bool Minimized { get; set; }
public bool Maximized { get; set; }
public WindowPositionWrapper Position { get; set; }
public int Monitor { get; set; }
public string Version { get; set; }
}
public struct MonitorConfigurationWrapper
{
public struct MonitorRectWrapper
{
public int Top { get; set; }
public int Left { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
public string Id { get; set; }
public string InstanceId { get; set; }
public int MonitorNumber { get; set; }
public int Dpi { get; set; }
public MonitorRectWrapper MonitorRectDpiAware { get; set; }
public MonitorRectWrapper MonitorRectDpiUnaware { get; set; }
}
public struct ProjectWrapper
{
public string Id { get; set; }
public string Name { get; set; }
public long CreationTime { get; set; }
public long LastLaunchedTime { get; set; }
public bool IsShortcutNeeded { get; set; }
public bool MoveExistingWindows { get; set; }
public List<MonitorConfigurationWrapper> MonitorConfiguration { get; set; }
public List<ApplicationWrapper> Applications { get; set; }
}
}
}

View File

@@ -1,30 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using WorkspacesEditor.Utils;
using static WorkspacesEditor.Data.ProjectData;
using static WorkspacesEditor.Data.WorkspacesData;
namespace WorkspacesEditor.Data
{
public class WorkspacesData : WorkspacesEditorData<WorkspacesListWrapper>
{
public string File => FolderUtils.DataFolder() + "\\workspaces.json";
public struct WorkspacesListWrapper
{
public List<ProjectWrapper> Workspaces { get; set; }
}
public enum OrderBy
{
LastViewed = 0,
Created = 1,
Name = 2,
Unknown = 3,
}
}
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json;
using WorkspacesEditor.Utils;
namespace WorkspacesEditor.Data
{
public class WorkspacesEditorData<T>
{
protected JsonSerializerOptions JsonOptions
{
get => new()
{
PropertyNamingPolicy = new DashCaseNamingPolicy(),
WriteIndented = true,
};
}
public T Read(string file)
{
IOUtils ioUtils = new();
string data = ioUtils.ReadFile(file);
return JsonSerializer.Deserialize<T>(data, JsonOptions);
}
public string Serialize(T data)
{
return JsonSerializer.Serialize(data, JsonOptions);
}
}
}

View File

@@ -13,7 +13,7 @@ using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using ManagedCommon;
using WorkspacesEditor.Data;
using WorkspacesCsharpLibrary.Data;
using WorkspacesEditor.Utils;
namespace WorkspacesEditor.Models
@@ -226,7 +226,7 @@ namespace WorkspacesEditor.Models
}
}
public Project(ProjectData.ProjectWrapper project)
public Project(ProjectWrapper project)
{
Id = project.Id;
Name = project.Name;
@@ -237,7 +237,7 @@ namespace WorkspacesEditor.Models
Monitors = [];
Applications = [];
foreach (ProjectData.ApplicationWrapper app in project.Applications)
foreach (ApplicationWrapper app in project.Applications)
{
Models.Application newApp = new()
{
@@ -269,7 +269,7 @@ namespace WorkspacesEditor.Models
Applications.Add(newApp);
}
foreach (ProjectData.MonitorConfigurationWrapper monitor in project.MonitorConfiguration)
foreach (MonitorConfigurationWrapper monitor in project.MonitorConfiguration)
{
System.Windows.Rect dpiAware = new(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height);
System.Windows.Rect dpiUnaware = new(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height);

View File

@@ -1,18 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json;
namespace WorkspacesEditor.Utils
{
public class DashCaseNamingPolicy : JsonNamingPolicy
{
public static DashCaseNamingPolicy Instance { get; } = new DashCaseNamingPolicy();
public override string ConvertName(string name)
{
return name.UpperCamelCaseToDashCase();
}
}
}

View File

@@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
namespace WorkspacesEditor.Utils
{
public class FolderUtils
{
public static string Desktop()
{
return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
}
public static string Temp()
{
return Path.GetTempPath();
}
// Note: the same path should be used in SnapshotTool and Launcher
public static string DataFolder()
{
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
}
}
}

View File

@@ -1,52 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.IO.Abstractions;
using System.Threading.Tasks;
namespace WorkspacesEditor.Utils
{
public class IOUtils
{
private readonly IFileSystem _fileSystem = new FileSystem();
public IOUtils()
{
}
public void WriteFile(string fileName, string data)
{
_fileSystem.File.WriteAllText(fileName, data);
}
public string ReadFile(string fileName)
{
if (_fileSystem.File.Exists(fileName))
{
int attempts = 0;
while (attempts < 10)
{
try
{
using FileSystemStream inputStream = _fileSystem.File.Open(fileName, FileMode.Open);
using StreamReader reader = new(inputStream);
string data = reader.ReadToEnd();
inputStream.Close();
return data;
}
catch (Exception)
{
Task.Delay(10).Wait();
}
attempts++;
}
}
return string.Empty;
}
}
}

View File

@@ -6,9 +6,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ManagedCommon;
using WorkspacesEditor.Data;
using WorkspacesCsharpLibrary.Data;
using WorkspacesCsharpLibrary.Utils;
using WorkspacesEditor.Models;
using WorkspacesEditor.ViewModels;
@@ -81,7 +81,7 @@ namespace WorkspacesEditor.Utils
foreach (Project project in workspaces)
{
ProjectData.ProjectWrapper wrapper = new()
ProjectWrapper wrapper = new()
{
Id = project.Id,
Name = project.Name,
@@ -95,7 +95,7 @@ namespace WorkspacesEditor.Utils
foreach (Application app in project.Applications.Where(x => x.IsIncluded))
{
wrapper.Applications.Add(new ProjectData.ApplicationWrapper
wrapper.Applications.Add(new ApplicationWrapper
{
Id = app.Id,
Application = app.AppName,
@@ -110,7 +110,7 @@ namespace WorkspacesEditor.Utils
Version = app.Version,
Maximized = app.Maximized,
Minimized = app.Minimized,
Position = new ProjectData.ApplicationWrapper.WindowPositionWrapper
Position = new ApplicationWrapper.WindowPositionWrapper
{
X = app.Position.X,
Y = app.Position.Y,
@@ -123,20 +123,20 @@ namespace WorkspacesEditor.Utils
foreach (MonitorSetup monitor in project.Monitors)
{
wrapper.MonitorConfiguration.Add(new ProjectData.MonitorConfigurationWrapper
wrapper.MonitorConfiguration.Add(new MonitorConfigurationWrapper
{
Id = monitor.MonitorName,
InstanceId = monitor.MonitorInstanceId,
MonitorNumber = monitor.MonitorNumber,
Dpi = monitor.Dpi,
MonitorRectDpiAware = new ProjectData.MonitorConfigurationWrapper.MonitorRectWrapper
MonitorRectDpiAware = new MonitorConfigurationWrapper.MonitorRectWrapper
{
Left = (int)monitor.MonitorDpiAwareBounds.Left,
Top = (int)monitor.MonitorDpiAwareBounds.Top,
Width = (int)monitor.MonitorDpiAwareBounds.Width,
Height = (int)monitor.MonitorDpiAwareBounds.Height,
},
MonitorRectDpiUnaware = new ProjectData.MonitorConfigurationWrapper.MonitorRectWrapper
MonitorRectDpiUnaware = new MonitorConfigurationWrapper.MonitorRectWrapper
{
Left = (int)monitor.MonitorDpiUnawareBounds.Left,
Top = (int)monitor.MonitorDpiUnawareBounds.Top,
@@ -163,7 +163,7 @@ namespace WorkspacesEditor.Utils
private bool AddWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
{
foreach (ProjectData.ProjectWrapper project in workspaces.Workspaces)
foreach (ProjectWrapper project in workspaces.Workspaces)
{
mainViewModel.Workspaces.Add(new Project(project));
}

View File

@@ -18,12 +18,12 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using WorkspacesCsharpLibrary;
using WorkspacesEditor.Data;
using WorkspacesCsharpLibrary.Data;
using WorkspacesCsharpLibrary.Utils;
using WorkspacesEditor.Models;
using WorkspacesEditor.Telemetry;
using WorkspacesEditor.Utils;
using static WorkspacesEditor.Data.WorkspacesData;
using static WorkspacesCsharpLibrary.Data.WorkspacesData;
namespace WorkspacesEditor.ViewModels
{

View File

@@ -78,6 +78,15 @@
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Data\WorkspacesData.cs" />
<Compile Remove="Data\ProjectData.cs" />
<Compile Remove="Data\WorkspacesEditorData`1.cs" />
<Compile Remove="Utils\IOUtils.cs" />
<Compile Remove="Utils\FolderUtils.cs" />
<Compile Remove="Utils\DashCaseNamingPolicy.cs" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
@@ -96,4 +105,4 @@
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
</Project>
</Project>

View File

@@ -9,7 +9,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using WorkspacesEditor.Data;
using WorkspacesCsharpLibrary.Data;
using WorkspacesEditor.Models;
using WorkspacesEditor.ViewModels;

View File

@@ -3,15 +3,12 @@
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Workspaces.Data;
using static WorkspacesLauncherUI.Data.AppLaunchData;
using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
namespace WorkspacesLauncherUI.Data
{
public class AppLaunchData : WorkspacesUIData<AppLaunchDataWrapper>
public class AppLaunchData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData<AppLaunchDataWrapper>
{
public struct AppLaunchDataWrapper
{

View File

@@ -3,14 +3,11 @@
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Workspaces.Data;
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
namespace WorkspacesLauncherUI.Data
{
public class AppLaunchInfoData : WorkspacesUIData<AppLaunchInfoWrapper>
public class AppLaunchInfoData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData<AppLaunchInfoWrapper>
{
public struct AppLaunchInfoWrapper
{

View File

@@ -4,15 +4,12 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Workspaces.Data;
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
namespace WorkspacesLauncherUI.Data
{
public class AppLaunchInfosData : WorkspacesUIData<AppLaunchInfoListWrapper>
public class AppLaunchInfosData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData<AppLaunchInfoListWrapper>
{
public struct AppLaunchInfoListWrapper
{

View File

@@ -1,35 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json;
using WorkspacesLauncherUI.Utils;
namespace Workspaces.Data
{
public class WorkspacesUIData<T>
{
protected JsonSerializerOptions JsonOptions
{
get
{
return new JsonSerializerOptions
{
PropertyNamingPolicy = new DashCaseNamingPolicy(),
WriteIndented = true,
};
}
}
public T Deserialize(string data)
{
return JsonSerializer.Deserialize<T>(data, JsonOptions);
}
public string Serialize(T data)
{
return JsonSerializer.Serialize(data, JsonOptions);
}
}
}

View File

@@ -6,13 +6,11 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using ManagedCommon;
using WorkspacesCsharpLibrary;
using WorkspacesLauncherUI.Data;
using WorkspacesLauncherUI.Models;
using WorkspacesLauncherUI.Utils;
namespace WorkspacesLauncherUI.ViewModels
{

View File

@@ -1,102 +1,102 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.WorkspacesLauncherUI</AssemblyTitle>
<AssemblyDescription>PowerToys Workspaces Editor</AssemblyDescription>
<Description>PowerToys Workspaces Editor</Description>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
</PropertyGroup>
<PropertyGroup>
<ProjectGuid>{9C53CC25-0623-4569-95BC-B05410675EE3}</ProjectGuid>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<ApplicationIcon>..\Assets\Workspaces\Workspaces.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AssemblyName>PowerToys.WorkspacesLauncherUI</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Content Include="..\Assets\**\*.*">
<Link>Assets\Workspaces\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<PropertyGroup>
<AssemblyTitle>PowerToys.WorkspacesLauncherUI</AssemblyTitle>
<AssemblyDescription>PowerToys Workspaces Launcher UI</AssemblyDescription>
<Description>PowerToys Workspaces Launcher UI</Description>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
</PropertyGroup>
<ItemGroup>
<COMReference Include="IWshRuntimeLibrary">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>f935dc20-1cf0-11d0-adb9-00c04fd58a0b</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
<COMReference Include="Shell32">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>50a7e9b0-70ef-11d1-b75a-00a0c90564fe</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="app.manifest" />
</ItemGroup>
<PropertyGroup>
<ProjectGuid>{9C53CC25-0623-4569-95BC-B05410675EE3}</ProjectGuid>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ControlzEx" />
<PackageReference Include="ModernWpfUI" />
<PackageReference Include="System.IO.Abstractions" />
</ItemGroup>
<PropertyGroup>
<ApplicationIcon>..\Assets\Workspaces\Workspaces.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AssemblyName>PowerToys.WorkspacesLauncherUI</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="..\Assets\**\*.*">
<Link>Assets\Workspaces\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<COMReference Include="IWshRuntimeLibrary">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>f935dc20-1cf0-11d0-adb9-00c04fd58a0b</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
<COMReference Include="Shell32">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>50a7e9b0-70ef-11d1-b75a-00a0c90564fe</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="app.manifest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ControlzEx" />
<PackageReference Include="ModernWpfUI" />
<PackageReference Include="System.IO.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
</Project>

View File

@@ -201,7 +201,7 @@ public:
Logger::error(message.value());
}
}
m_toggleEditorEventWaiter = EventWaiter(CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT, [&](int err) {
m_toggleEditorEventWaiter.start(CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT, [&](DWORD err) {
if (err == ERROR_SUCCESS)
{
Logger::trace(L"{} event was signaled", CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT);

View File

@@ -369,4 +369,4 @@
<Import Project="..\..\..\..\packages\robmikh.common.0.0.23-beta\build\native\robmikh.common.targets" Condition="Exists('..\..\..\..\packages\robmikh.common.0.0.23-beta\build\native\robmikh.common.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</Project>
</Project>

View File

@@ -28,6 +28,20 @@
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
#include <common/utils/gpo.h>
#include <array>
#include <vector>
#endif // __ZOOMIT_POWERTOYS__
#ifdef __ZOOMIT_POWERTOYS__
enum class ZoomItCommand
{
Zoom,
Draw,
Break,
LiveZoom,
Snip,
Record,
};
#endif // __ZOOMIT_POWERTOYS__
namespace winrt
@@ -172,7 +186,6 @@ std::wstring g_RecordingSaveLocationGIF;
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
std::shared_ptr<GifRecordingSession> g_GifRecordingSession = nullptr;
type_pGetMonitorInfo pGetMonitorInfo;
type_MonitorFromPoint pMonitorFromPoint;
type_pSHAutoComplete pSHAutoComplete;
@@ -7712,6 +7725,53 @@ HWND InitInstance( HINSTANCE hInstance, int nCmdShow )
}
// Dispatch commands coming from the PowerToys IPC channel.
#ifdef __ZOOMIT_POWERTOYS__
void ZoomIt_DispatchCommand(ZoomItCommand cmd)
{
auto post_hotkey = [](WPARAM id)
{
if (g_hWndMain != nullptr)
{
PostMessage(g_hWndMain, WM_HOTKEY, id, 0);
}
};
switch (cmd)
{
case ZoomItCommand::Zoom:
if (g_hWndMain != nullptr)
{
PostMessage(g_hWndMain, WM_COMMAND, IDC_ZOOM, 0);
}
Trace::ZoomItActivateZoom();
break;
case ZoomItCommand::Draw:
post_hotkey(DRAW_HOTKEY);
Trace::ZoomItActivateDraw();
break;
case ZoomItCommand::Break:
post_hotkey(BREAK_HOTKEY);
Trace::ZoomItActivateBreak();
break;
case ZoomItCommand::LiveZoom:
post_hotkey(LIVE_HOTKEY);
Trace::ZoomItActivateLiveZoom();
break;
case ZoomItCommand::Snip:
post_hotkey(SNIP_HOTKEY);
Trace::ZoomItActivateSnip();
break;
case ZoomItCommand::Record:
post_hotkey(RECORD_HOTKEY);
Trace::ZoomItActivateRecord();
break;
default:
break;
}
}
#endif
//----------------------------------------------------------------------------
//
// WinMain
@@ -7746,7 +7806,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
// Initialize logger
LoggerHelpers::init_logger(L"ZoomIt", L"", LogSettings::zoomItLoggerName);
ProcessWaiter::OnProcessTerminate(pid, [mainThreadId](int err) {
if (err != ERROR_SUCCESS)
{
@@ -7905,27 +7964,63 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
#ifdef __ZOOMIT_POWERTOYS__
HANDLE m_reload_settings_event_handle = NULL;
HANDLE m_exit_event_handle = NULL;
HANDLE m_zoom_event_handle = NULL;
HANDLE m_draw_event_handle = NULL;
HANDLE m_break_event_handle = NULL;
HANDLE m_live_zoom_event_handle = NULL;
HANDLE m_snip_event_handle = NULL;
HANDLE m_record_event_handle = NULL;
std::thread m_event_triggers_thread;
if( g_StartedByPowerToys ) {
// Start a thread to listen to PowerToys Events.
m_reload_settings_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_REFRESH_SETTINGS_EVENT);
m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_EXIT_EVENT);
if (!m_reload_settings_event_handle || !m_exit_event_handle)
m_zoom_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_ZOOM_EVENT);
m_draw_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_DRAW_EVENT);
m_break_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_BREAK_EVENT);
m_live_zoom_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_LIVEZOOM_EVENT);
m_snip_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_SNIP_EVENT);
m_record_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_RECORD_EVENT);
if (!m_reload_settings_event_handle || !m_exit_event_handle || !m_zoom_event_handle || !m_draw_event_handle || !m_break_event_handle || !m_live_zoom_event_handle || !m_snip_event_handle || !m_record_event_handle)
{
Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError()));
return 1;
}
m_event_triggers_thread = std::thread([&]() {
const std::array<HANDLE, 8> event_handles{
m_reload_settings_event_handle,
m_exit_event_handle,
m_zoom_event_handle,
m_draw_event_handle,
m_break_event_handle,
m_live_zoom_event_handle,
m_snip_event_handle,
m_record_event_handle,
};
const DWORD handle_count = static_cast<DWORD>(event_handles.size());
m_event_triggers_thread = std::thread([event_handles, handle_count]() {
MSG msg;
HANDLE event_handles[2] = {m_reload_settings_event_handle, m_exit_event_handle};
while (g_running)
{
DWORD dwEvt = MsgWaitForMultipleObjects(2, event_handles, false, INFINITE, QS_ALLINPUT);
DWORD dwEvt = MsgWaitForMultipleObjects(handle_count, event_handles.data(), false, INFINITE, QS_ALLINPUT);
if (dwEvt == WAIT_FAILED)
{
Logger::error(L"ZoomIt event wait failed. {}", get_last_error_or_default(GetLastError()));
break;
}
if (!g_running)
{
break;
}
if (dwEvt == WAIT_OBJECT_0 + handle_count)
{
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
continue;
}
switch (dwEvt)
{
case WAIT_OBJECT_0:
@@ -7938,19 +8033,28 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
case WAIT_OBJECT_0 + 1:
{
// Exit Event
Logger::trace(L"Received an exit event.");
PostMessage(g_hWndMain, WM_QUIT, 0, 0);
break;
}
case WAIT_OBJECT_0 + 2:
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
ZoomIt_DispatchCommand(ZoomItCommand::Zoom);
break;
default:
case WAIT_OBJECT_0 + 3:
ZoomIt_DispatchCommand(ZoomItCommand::Draw);
break;
case WAIT_OBJECT_0 + 4:
ZoomIt_DispatchCommand(ZoomItCommand::Break);
break;
case WAIT_OBJECT_0 + 5:
ZoomIt_DispatchCommand(ZoomItCommand::LiveZoom);
break;
case WAIT_OBJECT_0 + 6:
ZoomIt_DispatchCommand(ZoomItCommand::Snip);
break;
case WAIT_OBJECT_0 + 7:
ZoomIt_DispatchCommand(ZoomItCommand::Record);
break;
default: break;
}
}
});
@@ -7980,6 +8084,12 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
SetEvent(m_reload_settings_event_handle);
CloseHandle(m_reload_settings_event_handle);
CloseHandle(m_exit_event_handle);
CloseHandle(m_zoom_event_handle);
CloseHandle(m_draw_event_handle);
CloseHandle(m_break_event_handle);
CloseHandle(m_live_zoom_event_handle);
CloseHandle(m_snip_event_handle);
CloseHandle(m_record_event_handle);
m_event_triggers_thread.join();
}
#endif // __ZOOMIT_POWERTOYS__

View File

@@ -8,6 +8,7 @@
#include <common/utils/logger_helper.h>
#include <common/utils/resources.h>
#include <common/utils/winapi_error.h>
#include <common/interop/shared_constants.h>
#include <shellapi.h>
#include <common/interop/shared_constants.h>

View File

@@ -0,0 +1,20 @@
<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.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\PowerToys.ModuleContracts\PowerToys.ModuleContracts.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,151 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Text.Json;
using Common.UI;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.ModuleContracts;
namespace Awake.ModuleServices;
/// <summary>
/// Provides CLI-based Awake control for reuse across hosts.
/// </summary>
public sealed class AwakeService : ModuleServiceBase, IAwakeService
{
public static AwakeService Instance { get; } = new();
public override string Key => SettingsDeepLink.SettingsWindow.Awake.ToString();
protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.Awake;
public override Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default)
{
// Default launch -> indefinite, honoring Awake's own settings for display behavior.
return SetIndefiniteAsync(cancellationToken);
}
public AwakeState GetCurrentState()
{
var isRunning = IsAwakeProcessRunning();
var settings = ReadSettings();
if (settings is null)
{
return new AwakeState(isRunning, AwakeStateMode.Passive, false, null, null);
}
var mode = settings.Properties.Mode switch
{
AwakeMode.PASSIVE => AwakeStateMode.Passive,
AwakeMode.INDEFINITE => AwakeStateMode.Indefinite,
AwakeMode.TIMED => AwakeStateMode.Timed,
AwakeMode.EXPIRABLE => AwakeStateMode.Expirable,
_ => AwakeStateMode.Passive,
};
TimeSpan? duration = null;
DateTimeOffset? expiration = null;
switch (mode)
{
case AwakeStateMode.Timed:
duration = TimeSpan.FromHours(settings.Properties.IntervalHours) + TimeSpan.FromMinutes(settings.Properties.IntervalMinutes);
break;
case AwakeStateMode.Expirable:
expiration = settings.Properties.ExpirationDateTime;
break;
}
return new AwakeState(isRunning, mode, settings.Properties.KeepDisplayOn, duration, expiration);
}
public Task<OperationResult> SetIndefiniteAsync(CancellationToken cancellationToken = default)
{
return UpdateSettingsAsync(
settings =>
{
settings.Properties.Mode = AwakeMode.INDEFINITE;
},
cancellationToken);
}
public Task<OperationResult> SetTimedAsync(int minutes, CancellationToken cancellationToken = default)
{
if (minutes <= 0)
{
return Task.FromResult(OperationResult.Fail("Minutes must be greater than zero."));
}
return UpdateSettingsAsync(
settings =>
{
var totalMinutes = Math.Min(minutes, int.MaxValue);
settings.Properties.Mode = AwakeMode.TIMED;
settings.Properties.IntervalHours = (uint)(totalMinutes / 60);
settings.Properties.IntervalMinutes = (uint)(totalMinutes % 60);
},
cancellationToken);
}
public Task<OperationResult> SetOffAsync(CancellationToken cancellationToken = default)
{
return UpdateSettingsAsync(
settings =>
{
settings.Properties.Mode = AwakeMode.PASSIVE;
},
cancellationToken);
}
private static Task<OperationResult> UpdateSettingsAsync(Action<AwakeSettings> mutateSettings, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var settingsUtils = SettingsUtils.Default;
var settings = settingsUtils.GetSettingsOrDefault<AwakeSettings>(AwakeSettings.ModuleName);
mutateSettings(settings);
settingsUtils.SaveSettings(JsonSerializer.Serialize(settings, AwakeServiceJsonContext.Default.AwakeSettings), AwakeSettings.ModuleName);
return Task.FromResult(OperationResult.Ok());
}
catch (OperationCanceledException)
{
return Task.FromResult(OperationResult.Fail("Awake update was cancelled."));
}
catch (Exception ex)
{
return Task.FromResult(OperationResult.Fail($"Failed to update Awake settings: {ex.Message}"));
}
}
private static bool IsAwakeProcessRunning()
{
try
{
return Process.GetProcessesByName("PowerToys.Awake").Length > 0;
}
catch
{
return false;
}
}
private static AwakeSettings? ReadSettings()
{
try
{
var settingsUtils = SettingsUtils.Default;
return settingsUtils.GetSettingsOrDefault<AwakeSettings>(AwakeSettings.ModuleName);
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,13 @@
// 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.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Awake.ModuleServices;
[JsonSerializable(typeof(AwakeSettings))]
internal sealed partial class AwakeServiceJsonContext : JsonSerializerContext
{
}

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.
namespace Awake.ModuleServices;
/// <summary>
/// Represents the current state of the Awake module.
/// </summary>
/// <param name="IsRunning">Whether the Awake process is currently running.</param>
/// <param name="Mode">The current Awake mode (Passive, Indefinite, Timed, Expirable).</param>
/// <param name="KeepDisplayOn">Whether the display is kept on.</param>
/// <param name="Duration">For timed mode, the configured duration.</param>
/// <param name="Expiration">For expirable mode, the expiration date/time.</param>
public readonly record struct AwakeState(bool IsRunning, AwakeStateMode Mode, bool KeepDisplayOn, TimeSpan? Duration, DateTimeOffset? Expiration);
/// <summary>
/// The mode of the Awake module.
/// </summary>
public enum AwakeStateMode
{
Passive = 0,
Indefinite = 1,
Timed = 2,
Expirable = 3,
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using PowerToys.ModuleContracts;
namespace Awake.ModuleServices;
public interface IAwakeService : IModuleService
{
Task<OperationResult> SetIndefiniteAsync(CancellationToken cancellationToken = default);
Task<OperationResult> SetTimedAsync(int minutes, CancellationToken cancellationToken = default);
Task<OperationResult> SetOffAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets the current state of the Awake module.
/// </summary>
AwakeState GetCurrentState();
}

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