Compare commits

..

7 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
240 changed files with 3508 additions and 20363 deletions

View File

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

View File

@@ -11,7 +11,6 @@ ACCESSDENIED
ACCESSTOKEN
acfs
ACIE
ACR
AClient
AColumn
acrt
@@ -93,7 +92,6 @@ asf
Ashcraft
AShortcut
ASingle
ASUS
ASSOCCHANGED
ASSOCF
ASSOCSTR
@@ -104,7 +102,6 @@ ATRIOX
ATX
aumid
authenticode
AUO
AUTOBUDDY
AUTOCHECKBOX
AUTOHIDE
@@ -122,10 +119,6 @@ azureaiinference
azureinference
azureopenai
backticks
Backlight
Badflags
Badmode
Badparam
bbwe
BCIE
bck
@@ -197,10 +190,7 @@ CARETBLINKING
Carlseibert
CAtl
caub
CAuthn
CAuthz
CBN
Cds
cch
CCHDEVICENAME
CCHFORMNAME
@@ -219,11 +209,9 @@ changecursor
CHILDACTIVATE
CHILDWINDOW
CHOOSEFONT
Chunghwa
cidl
CIELCh
cim
CImp
CImage
cla
CLASSDC
@@ -231,7 +219,7 @@ CLASSNOTAVAILABLE
CLEARTYPE
clickable
clickonce
clientedge
CLIENTEDGE
clientid
clientside
CLIPBOARDUPDATE
@@ -243,7 +231,6 @@ CLSCTX
clsids
Clusion
cmder
CMN
CMDNOTFOUNDMODULEINTERFACE
cmdpal
CMIC
@@ -259,7 +246,7 @@ codereview
Codespaces
Coen
cognitiveservices
coinit
COINIT
colid
colorconv
colorformat
@@ -298,7 +285,6 @@ Corpor
cotaskmem
COULDNOT
countof
Cowait
covrun
cpcontrols
cph
@@ -318,13 +304,11 @@ CRECT
CRH
critsec
cropandlock
crt
Crossdevice
csdevkit
CSearch
CSettings
cso
CSOT
CSRW
CStyle
cswin
@@ -365,17 +349,11 @@ DBPROP
DBPROPIDSET
DBPROPSET
DCBA
DCapabilities
DCOM
DComposition
DCR
ddc
Ddc
Ddcci
ddcutil
DDEIf
Deact
debouncer
debugbreak
decryptor
Dedup
@@ -392,7 +370,6 @@ DEFAULTTOPRIMARY
DEFERERASE
DEFPUSHBUTTON
deinitialization
DELA
DELETEDKEYIMAGE
DELETESCANS
DEMOTYPE
@@ -422,21 +399,18 @@ DISABLEASACTIONKEY
DISABLENOSCROLL
diskmgmt
DISPLAYCHANGE
displayconfig
DISPLAYCONFIG
DISPLAYFLAGS
DISPLAYFREQUENCY
displayname
DISPLAYORIENTATION
Displayport
diu
divyan
Dlg
DLGFRAME
dlgmodalframe
DLGMODALFRAME
dlib
dllhost
dllmain
Dmdo
DNLEN
DONOTROUND
DONTVALIDATEPATH
@@ -453,7 +427,6 @@ DRAWCLIPBOARD
DRAWFRAME
drawingcolor
dreamsofameaningfullife
DREGION
drivedetectionwarning
DROPFILES
DSTINVERT
@@ -465,7 +438,6 @@ dutil
DVASPECT
DVASPECTINFO
DVD
dvi
dvr
DVTARGETDEVICE
dwflags
@@ -485,19 +457,15 @@ DWMWINDOWMAXIMIZEDCHANGE
DWORDLONG
dworigin
dwrite
Dxva
dxgi
eab
EAccess
easeofaccess
ecount
edid
Edid
EDITKEYBOARD
EDITSHORTCUTS
EDITTEXT
EFile
EInvalid
eep
eku
emojis
ENABLEDELAYEDEXPANSION
@@ -507,16 +475,14 @@ ENABLETEMPLATE
encodedlaunch
encryptor
ENDSESSION
ENot
ENSUREVISIBLE
ENTERSIZEMOVE
ENTRYW
ENU
environmentvariables
eoac
EOAC
EPO
epu
EProvider
ERASEBKGND
EREOF
EResize
@@ -570,7 +536,6 @@ fdx
FErase
fesf
FFFF
FFh
FInc
Figma
FILEEXPLORER
@@ -612,9 +577,7 @@ FORMATDLGORD
formatetc
FORPARSING
foundrylocal
FPrimary
FRAMECHANGED
Framechanged
FRestore
frm
FROMTOUCH
@@ -674,8 +637,6 @@ gwl
GWLP
GWLSTYLE
hangeul
Hann
Hantai
Hanzi
Hardlines
hardlinks
@@ -697,8 +658,6 @@ HCRYPTPROV
hcursor
hcwhite
hdc
hdmi
HDMI
hdr
hdrop
hdwwiz
@@ -735,7 +694,6 @@ HKPD
HKU
HMD
hmenu
HMON
hmodule
hmonitor
homies
@@ -753,7 +711,6 @@ hotkeys
hotlight
hotspot
HPAINTBUFFER
HPhysical
HRAWINPUT
hredraw
hres
@@ -764,7 +721,6 @@ hsb
HSCROLL
hsi
HSpeed
HSync
HTCLIENT
hthumbnail
HTOUCHINPUT
@@ -774,7 +730,6 @@ HVal
HValue
Hvci
hwb
HWP
HWHEEL
HWINEVENTHOOK
hwnd
@@ -831,7 +786,6 @@ INITTOLOGFONTSTRUCT
INLINEPREFIX
inlines
Inno
Innolux
INPC
inproc
INPUTHARDWARE
@@ -873,7 +827,6 @@ istep
ith
ITHUMBNAIL
IUI
IVO
IUWP
IWIC
jfif
@@ -885,7 +838,6 @@ jpnime
Jsons
jsonval
jxr
Kantai
keybd
KEYBDDATA
KEYBDINPUT
@@ -907,7 +859,6 @@ KILLFOCUS
killrunner
kmph
kvp
KVM
Kybd
LARGEICON
lastcodeanalysissucceeded
@@ -927,8 +878,6 @@ LEFTTEXT
LError
LEVELID
LExit
Lenovo
LGD
lhwnd
LIBFUZZER
LIBID
@@ -1033,7 +982,6 @@ MAPTOSAMESHORTCUT
MAPVK
MARKDOWNPREVIEWHANDLERCPP
MAXIMIZEBOX
Maximizebox
MAXSHORTCUTSIZE
maxversiontested
mber
@@ -1044,8 +992,6 @@ MDL
mdtext
mdtxt
mdwn
Mccs
mccs
meme
mcp
memicmp
@@ -1067,7 +1013,6 @@ mikeclayton
mindaro
Minimizable
MINIMIZEBOX
Minimizebox
MINIMIZEEND
MINIMIZESTART
MINMAXINFO
@@ -1087,7 +1032,6 @@ MODALFRAME
MODESPRUNED
MONITORENUMPROC
MONITORINFO
Monitorinfo
MONITORINFOEX
MONITORINFOEXW
monitorinfof
@@ -1128,10 +1072,9 @@ MSLLHOOKSTRUCT
Mso
msrc
msstore
mstsc
mswhql
msvcp
MT
mstsc
MTND
MULTIPLEUSE
multizone
@@ -1147,7 +1090,6 @@ MVVMTK
MWBEx
MYICON
NAMECHANGE
Nanjing
namespaceanddescendants
nao
NCACTIVATE
@@ -1216,7 +1158,6 @@ NOMCX
NOMINMAX
NOMIRRORBITMAP
NOMOVE
Nomove
NONANTIALIASED
nonclient
NONCLIENTMETRICSW
@@ -1238,7 +1179,6 @@ NORMALUSER
NOSEARCH
NOSENDCHANGING
NOSIZE
Nosize
NOTHOUSANDS
NOTICKS
NOTIFICATIONSDLL
@@ -1246,11 +1186,9 @@ NOTIFYICONDATA
NOTIFYICONDATAW
NOTIMPL
NOTOPMOST
Notopmost
NOTRACK
NOTSRCCOPY
NOTSRCERASE
Notupdated
notwindows
NOTXORPEN
nowarn
@@ -1292,9 +1230,10 @@ OPENFILENAME
openrdp
opensource
openxmlformats
ollama
onnx
openurl
OPTIMIZEFORINVOKE
Optronics
ORPHANEDDIALOGTITLE
ORSCANS
oss
@@ -1330,7 +1269,6 @@ PATINVERT
PATPAINT
pbc
pbi
PBP
PBlob
pbrush
pcb
@@ -1345,7 +1283,6 @@ PDBs
PDEVMODE
pdisp
PDLL
pdmodels
pdo
pdto
pdtobj
@@ -1368,7 +1305,6 @@ pguid
phbm
phbmp
phicon
PHL
Photoshop
phwnd
pici
@@ -1400,8 +1336,6 @@ Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
powerdisplay
POWERDISPLAYMODULEINTERFACE
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1456,7 +1390,6 @@ projectname
PROPERTYKEY
Propset
PROPVARIANT
prot
PRTL
prvpane
psapi
@@ -1484,15 +1417,12 @@ PTOKEN
PToy
ptstr
pui
pvct
PWAs
pwcs
PWSTR
pwsz
pwtd
QDC
qdc
QDS
qit
QITAB
QITABENT
@@ -1517,6 +1447,7 @@ RAWPATH
rbhid
rclsid
RCZOOMIT
remotedesktop
rdp
RDW
READMODE
@@ -1545,7 +1476,6 @@ remappings
REMAPSUCCESSFUL
REMAPUNSUCCESSFUL
Remotable
remotedesktop
remoteip
Removelnk
renamable
@@ -1619,7 +1549,6 @@ scrollviewer
SDDL
SDKDDK
sdns
Sdr
searchterm
SEARCHUI
secondaryclickaction
@@ -1774,7 +1703,6 @@ STARTUPINFOW
startupscreen
STATFLAG
STATICEDGE
Staticedge
STATSTG
stdafx
STDAPI
@@ -1817,7 +1745,7 @@ SVGIO
svgz
SVSI
SWFO
swp
SWP
SWPNOSIZE
SWPNOZORDER
SWRESTORE
@@ -1837,7 +1765,6 @@ syskeydown
SYSKEYUP
SYSLIB
SYSMENU
Sysmenu
systemai
SYSTEMAPPS
SYSTEMMODAL
@@ -1877,9 +1804,7 @@ THEMECHANGED
themeresources
THH
THICKFRAME
Thickframe
THISCOMPONENT
Tianma
throughs
TILEDWINDOW
TILLSON
@@ -1966,7 +1891,7 @@ unzoom
UOffset
UOI
UPDATENOW
updateregistry
UPDATEREGISTRY
updown
UPGRADINGPRODUCTCODE
upscaling
@@ -1993,9 +1918,6 @@ vcamp
vcenter
vcgtq
VCINSTALLDIR
Vcp
vcp
vcpname
Vcpkg
VCRT
vcruntime
@@ -2008,10 +1930,7 @@ VERIFYCONTEXT
VERSIONINFO
VERTRES
VERTSIZE
VESA
vesa
VFT
Vga
vget
vgetq
viewmodels
@@ -2041,7 +1960,6 @@ VSM
vso
vsonline
VSpeed
VSync
vstemplate
vstest
VSTHRD
@@ -2083,7 +2001,7 @@ winapi
winappsdk
windir
WINDOWCREATED
windowedge
WINDOWEDGE
WINDOWINFO
WINDOWNAME
WINDOWPLACEMENT
@@ -2107,7 +2025,7 @@ WINL
winlogon
winmd
winml
winnt
WINNT
winres
winrt
winsdk
@@ -2124,7 +2042,6 @@ WKSG
Wlkr
wmain
Wman
wmi
WMI
WMICIM
wmimgmt

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",
@@ -203,11 +205,6 @@
"PowerToys.PowerAccentModuleInterface.dll",
"PowerToys.PowerAccentKeyboardService.dll",
"PowerToys.PowerDisplayModuleInterface.dll",
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
"PowerDisplay.Lib.dll",
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
"WinUI3Apps\\PowerToys.PowerRename.exe",
"WinUI3Apps\\PowerToys.PowerRenameContextMenu.dll",
@@ -376,8 +373,6 @@
"UnitsNet.dll",
"UtfUnknown.dll",
"Wpf.Ui.dll",
"WmiLight.dll",
"WmiLight.Native.dll",
"Shmuelie.WinRTServer.dll",
"ToolGood.Words.Pinyin.dll"
],

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

@@ -91,7 +91,6 @@
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
<PackageVersion Include="OpenAI" Version="2.5.0" />
<PackageVersion Include="Polly.Core" Version="8.6.5" />
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
@@ -103,7 +102,6 @@
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.10" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
@@ -133,7 +131,6 @@
<PackageVersion Include="UnitsNet" Version="5.56.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="WinUIEx" Version="2.8.0" />
<PackageVersion Include="WmiLight" Version="6.14.0" />
<PackageVersion Include="WPF-UI" Version="3.0.5" />
<PackageVersion Include="WyHash" Version="1.0.5" />
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />

View File

@@ -10,7 +10,6 @@ This software incorporates material from third parties.
- Installer/Runner
- Measure tool
- Peek
- PowerDisplay
- Registry Preview
## Utility: Color Picker
@@ -1520,35 +1519,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## Utility: PowerDisplay
### Twinkle Tray
PowerDisplay's DDC/CI implementation references techniques from Twinkle Tray.
**Source**: https://github.com/xanderfrangos/twinkle-tray
MIT License
Copyright © 2020 Xander Frangos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## NuGet Packages used by PowerToys
@@ -1587,7 +1557,6 @@ SOFTWARE.
- NLog.Extensions.Logging
- NLog.Schema
- OpenAI
- Polly.Core
- ReverseMarkdown
- ScipBe.Common.Office.OneNote
- SharpCompress
@@ -1600,6 +1569,5 @@ SOFTWARE.
- UnitsNet
- UTF.Unknown
- WinUIEx
- WmiLight
- WPF-UI
- WyHash

View File

@@ -459,6 +459,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/imageresizer/Tests/">
<Project Path="src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj">
@@ -667,23 +671,6 @@
<Deploy />
</Project>
</Folder>
<Folder Name="/modules/PowerDisplay/">
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
</Folder>
<Folder Name="/modules/PowerDisplay/Tests/">
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/MeasureTool/">
<Project Path="src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj" Id="54a93af7-60c7-4f6c-99d2-fbb1f75f853a">
<BuildDependency Project="src/common/Display/Display.vcxproj" />

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`).

File diff suppressed because it is too large Load Diff

View File

@@ -1,223 +0,0 @@
# MCCS Capabilities String Parser - Recursive Descent Design
## Overview
This document describes the recursive descent parser implementation for DDC/CI MCCS (Monitor Control Command Set) capabilities strings.
### Attention!
This document and the code implement are generated by Copilot.
## Grammar Definition (BNF)
```bnf
capabilities ::= ['('] segment* [')']
segment ::= identifier '(' segment_content ')'
segment_content ::= text | vcp_entries | hex_list
vcp_entries ::= vcp_entry*
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
hex_list ::= hex_byte*
hex_byte ::= [0-9A-Fa-f]{2}
identifier ::= [a-z_A-Z]+
text ::= [^()]+
```
## Example Input
```
(prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03 07)vcp(10 12 14(04 05 06) 16 60(11 12 0F) DC DF)mccs_ver(2.2)vcpname(F0(Custom Setting)))
```
## Parser Architecture
### Component Hierarchy
```
MccsCapabilitiesParser (main parser)
├── ParseCapabilities() → MccsParseResult
├── ParseSegment() → ParsedSegment?
├── ParseBalancedContent() → string
├── ParseIdentifier() → ReadOnlySpan<char>
├── ApplySegment() → void
│ ├── ParseHexList() → List<byte>
│ ├── ParseVcpEntries() → Dictionary<byte, VcpCodeInfo>
│ └── ParseVcpNames() → void
├── VcpEntryParser (sub-parser for vcp() content)
│ └── TryParseEntry() → VcpEntry
├── VcpNameParser (sub-parser for vcpname() content)
│ └── TryParseEntry() → (byte code, string name)
└── WindowParser (sub-parser for windowN() content)
├── Parse() → WindowCapability
└── ParseSubSegment() → (name, content)?
```
### Design Principles
1. **ref struct for Zero Allocation**
- Main parser uses `ref struct` to avoid heap allocation
- Works with `ReadOnlySpan<char>` for efficient string slicing
- No intermediate string allocations during parsing
2. **Recursive Descent Pattern**
- Each grammar rule has a corresponding parse method
- Methods call each other recursively for nested structures
- Single-character lookahead via `Peek()`
3. **Error Recovery**
- Errors are accumulated, not thrown
- Parser attempts to continue after errors
- Returns partial results when possible
4. **Sub-parsers for Specialized Content**
- `VcpEntryParser` for VCP code entries
- `VcpNameParser` for custom VCP names
- Each sub-parser handles its own grammar subset
## Parse Methods Detail
### ParseCapabilities()
Entry point. Handles optional outer parentheses and iterates through segments.
```csharp
private MccsParseResult ParseCapabilities()
{
// Handle optional outer parens
// while (!IsAtEnd()) { ParseSegment() }
// Return result with accumulated errors
}
```
### ParseSegment()
Parses a single `identifier(content)` segment.
```csharp
private ParsedSegment? ParseSegment()
{
// 1. ParseIdentifier()
// 2. Expect '('
// 3. ParseBalancedContent()
// 4. Expect ')'
}
```
### ParseBalancedContent()
Extracts content between balanced parentheses, handling nested parens.
```csharp
private string ParseBalancedContent()
{
int depth = 1;
while (depth > 0) {
if (char == '(') depth++;
if (char == ')') depth--;
}
}
```
### ParseVcpEntries()
Delegates to `VcpEntryParser` for the specialized VCP entry grammar.
```csharp
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
Examples:
- "10" code=0x10, values=[]
- "14(04 05 06)" code=0x14, values=[4, 5, 6]
- "60(11 12 0F)" code=0x60, values=[0x11, 0x12, 0x0F]
```
## Comparison with Other Approaches
| Approach | Pros | Cons |
|----------|------|------|
| **Recursive Descent** (this) | Clear structure, handles nesting, extensible | More code |
| **Regex** (DDCSharp) | Concise | Hard to debug, limited nesting |
| **Mixed** (original) | Pragmatic | Inconsistent, hard to maintain |
## Performance Characteristics
- **Time Complexity**: O(n) where n = input length
- **Space Complexity**: O(1) for parsing + O(m) for output where m = number of VCP codes
- **Allocations**: Minimal - only for output structures
## Supported Segments
| Segment | Description | Parser |
|---------|-------------|--------|
| `prot(...)` | Protocol type | Direct assignment |
| `type(...)` | Display type (lcd/crt) | Direct assignment |
| `model(...)` | Model name | Direct assignment |
| `cmds(...)` | Supported commands | ParseHexList |
| `vcp(...)` | VCP code entries | VcpEntryParser |
| `mccs_ver(...)` | MCCS version | Direct assignment |
| `vcpname(...)` | Custom VCP names | VcpNameParser |
| `windowN(...)` | PIP/PBP window capabilities | WindowParser |
### Window Segment Format
The `windowN` segment (where N is 1, 2, 3, etc.) describes PIP/PBP window capabilities:
```
window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10))
```
| Sub-field | Format | Description |
|-----------|--------|-------------|
| `type` | `type(PIP)` or `type(PBP)` | Window type (Picture-in-Picture or Picture-by-Picture) |
| `area` | `area(x1 y1 x2 y2)` | Window area coordinates in pixels |
| `max` | `max(width height)` | Maximum window dimensions |
| `min` | `min(width height)` | Minimum window dimensions |
| `window` | `window(id)` | Window identifier |
All sub-fields are optional; missing fields default to zero values.
## Error Handling
```csharp
public readonly struct ParseError
{
public int Position { get; } // Character position
public string Message { get; } // Human-readable error
}
public sealed class MccsParseResult
{
public VcpCapabilities Capabilities { get; }
public IReadOnlyList<ParseError> Errors { get; }
public bool HasErrors => Errors.Count > 0;
public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
}
```
## Usage Example
```csharp
// Parse capabilities string
var result = MccsCapabilitiesParser.Parse(capabilitiesString);
if (result.IsValid)
{
var caps = result.Capabilities;
Console.WriteLine($"Model: {caps.Model}");
Console.WriteLine($"MCCS Version: {caps.MccsVersion}");
Console.WriteLine($"VCP Codes: {caps.SupportedVcpCodes.Count}");
}
if (result.HasErrors)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Parse error at {error.Position}: {error.Message}");
}
}
```
## Edge Cases Handled
1. **Missing outer parentheses** (Apple Cinema Display)
2. **No spaces between hex bytes** (`010203` vs `01 02 03`)
3. **Nested parentheses** in VCP values
4. **Unknown segments** (logged but not fatal)
5. **Malformed input** (partial results returned)

View File

@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 43> processesToTerminate = {
std::array<std::wstring_view, 42> processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe",
@@ -1565,7 +1565,6 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.PowerRename.exe",
L"PowerToys.ImageResizer.exe",
L"PowerToys.LightSwitchService.exe",
L"PowerToys.PowerDisplay.exe",
L"PowerToys.GcodeThumbnailProvider.exe",
L"PowerToys.BgcodeThumbnailProvider.exe",
L"PowerToys.PdfThumbnailProvider.exe",

View File

@@ -1,29 +0,0 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util" >
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define PowerDisplayAssetsFiles=?>
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
<Fragment>
<!-- Power Display -->
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
</DirectoryRef>
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--PowerDisplayAssetsFiles_Component_Def-->
</DirectoryRef>
<ComponentGroup Id="PowerDisplayComponentGroup">
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
</RegistryKey>
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
</Component>
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -47,7 +47,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
@@ -124,7 +123,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="KeyboardManager.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="PowerDisplay.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="RegistryPreview.wxs" />
<Compile Include="Run.wxs" />

View File

@@ -53,7 +53,6 @@
<ComponentGroupRef Id="LightSwitchComponentGroup" />
<ComponentGroupRef Id="PeekComponentGroup" />
<ComponentGroupRef Id="PowerRenameComponentGroup" />
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
<ComponentGroupRef Id="RunComponentGroup" />
<ComponentGroupRef Id="SettingsComponentGroup" />

View File

@@ -176,10 +176,6 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
#PowerDisplay
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay"
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
#New+
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs

View File

@@ -45,7 +45,6 @@ namespace Common.UI
NewPlus,
CmdPal,
ZoomIt,
PowerDisplay,
}
private static string SettingsWindowNameToString(SettingsWindow value)
@@ -116,8 +115,6 @@ namespace Common.UI
return "CmdPal";
case SettingsWindow.ZoomIt:
return "ZoomIt";
case SettingsWindow.PowerDisplay:
return "PowerDisplay";
default:
{
return string.Empty;

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

@@ -32,10 +32,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredLightSwitchEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredPowerDisplayEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerDisplayEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredFancyZonesEnabledValue());

View File

@@ -14,7 +14,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();

View File

@@ -18,7 +18,6 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();

View File

@@ -30,7 +30,6 @@ namespace ManagedCommon
PowerRename,
PowerLauncher,
PowerAccent,
PowerDisplay,
RegistryPreview,
MeasureTool,
ShortcutGuide,

View File

@@ -247,36 +247,4 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
}
hstring Constants::TogglePowerDisplayEvent()
{
return CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT;
}
hstring Constants::TerminatePowerDisplayEvent()
{
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
}
hstring Constants::RefreshPowerDisplayMonitorsEvent()
{
return CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT;
}
hstring Constants::SettingsUpdatedPowerDisplayEvent()
{
return CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT;
}
hstring Constants::ApplyColorTemperaturePowerDisplayEvent()
{
return CommonSharedConstants::APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT;
}
hstring Constants::ApplyProfilePowerDisplayEvent()
{
return CommonSharedConstants::APPLY_PROFILE_POWER_DISPLAY_EVENT;
}
hstring Constants::PowerDisplaySendSettingsTelemetryEvent()
{
return CommonSharedConstants::POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT;
}
hstring Constants::HotkeyUpdatedPowerDisplayEvent()
{
return CommonSharedConstants::HOTKEY_UPDATED_POWER_DISPLAY_EVENT;
}
}

View File

@@ -65,14 +65,6 @@ namespace winrt::PowerToys::Interop::implementation
static hstring WorkspacesHotkeyEvent();
static hstring PowerToysRunnerTerminateSettingsEvent();
static hstring ShowCmdPalEvent();
static hstring TogglePowerDisplayEvent();
static hstring TerminatePowerDisplayEvent();
static hstring RefreshPowerDisplayMonitorsEvent();
static hstring SettingsUpdatedPowerDisplayEvent();
static hstring ApplyColorTemperaturePowerDisplayEvent();
static hstring ApplyProfilePowerDisplayEvent();
static hstring PowerDisplaySendSettingsTelemetryEvent();
static hstring HotkeyUpdatedPowerDisplayEvent();
};
}

View File

@@ -62,14 +62,6 @@ namespace PowerToys
static String WorkspacesHotkeyEvent();
static String PowerToysRunnerTerminateSettingsEvent();
static String ShowCmdPalEvent();
static String TogglePowerDisplayEvent();
static String TerminatePowerDisplayEvent();
static String RefreshPowerDisplayMonitorsEvent();
static String SettingsUpdatedPowerDisplayEvent();
static String ApplyColorTemperaturePowerDisplayEvent();
static String ApplyProfilePowerDisplayEvent();
static String PowerDisplaySendSettingsTelemetryEvent();
static String HotkeyUpdatedPowerDisplayEvent();
}
}
}

View File

@@ -148,20 +148,6 @@ namespace CommonSharedConstants
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";
// Path to the events used by PowerDisplay
const wchar_t TOGGLE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c";
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
const wchar_t APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
const wchar_t APPLY_PROFILE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyProfileEvent-6e8a3c9d-4f7b-5d2e-8a1c-3e9f7b6d2a5c";
const wchar_t POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsTelemetryEvent-8c4f2a1d-5e3b-7f9c-1a6d-3b8e5f2c9a7d";
const wchar_t HOTKEY_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-HotkeyUpdatedEvent-9d5f3a2b-7e1c-4b8a-6f3d-2a9e5c7b1d4f";
// Path to the events used by LightSwitch to notify PowerDisplay of theme changes
const wchar_t LIGHT_SWITCH_LIGHT_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca";
const wchar_t LIGHT_SWITCH_DARK_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";

View File

@@ -83,7 +83,6 @@ struct LogSettings
inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.log";
inline const static std::string zoomItLoggerName = "zoom-it";
inline const static std::string lightSwitchLoggerName = "light-switch";
inline const static std::string powerDisplayLoggerName = "powerdisplay";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();

View File

@@ -32,7 +32,6 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker";
const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock";
const std::wstring POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH = L"ConfigureEnabledUtilityLightSwitch";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_DISPLAY = L"ConfigureEnabledUtilityPowerDisplay";
const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones";
const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith";
const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview";
@@ -311,11 +310,6 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH);
}
inline gpo_rule_configured_t getConfiguredPowerDisplayEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_DISPLAY);
}
inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_FANCYZONES);

View File

@@ -148,16 +148,6 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityEnvironmentVariables" class="Both" displayName="$(string.ConfigureEnabledUtilityEnvironmentVariables)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityEnvironmentVariables">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />

View File

@@ -247,7 +247,6 @@ If you don't configure this policy, the user will be able to control the setting
<string id="ConfigureEnabledUtilityCmdPal">CmdPal: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
<string id="ConfigureEnabledUtilityLightSwitch">Light Switch: Configure enabled state</string>
<string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string>
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>

View File

@@ -241,46 +241,6 @@ void LightSwitchSettings::LoadSettings()
NotifyObservers(SettingId::ChangeApps);
}
}
// EnableDarkModeProfile
if (const auto jsonVal = values.get_bool_value(L"enableDarkModeProfile"))
{
auto val = *jsonVal;
if (m_settings.enableDarkModeProfile != val)
{
m_settings.enableDarkModeProfile = val;
}
}
// EnableLightModeProfile
if (const auto jsonVal = values.get_bool_value(L"enableLightModeProfile"))
{
auto val = *jsonVal;
if (m_settings.enableLightModeProfile != val)
{
m_settings.enableLightModeProfile = val;
}
}
// DarkModeProfile
if (const auto jsonVal = values.get_string_value(L"darkModeProfile"))
{
auto val = *jsonVal;
if (m_settings.darkModeProfile != val)
{
m_settings.darkModeProfile = val;
}
}
// LightModeProfile
if (const auto jsonVal = values.get_string_value(L"lightModeProfile"))
{
auto val = *jsonVal;
if (m_settings.lightModeProfile != val)
{
m_settings.lightModeProfile = val;
}
}
}
catch (...)
{

View File

@@ -67,11 +67,6 @@ struct LightSwitchConfig
bool changeSystem = false;
bool changeApps = false;
bool enableDarkModeProfile = false;
bool enableLightModeProfile = false;
std::wstring darkModeProfile = L"";
std::wstring lightModeProfile = L"";
};
class LightSwitchSettings

View File

@@ -4,7 +4,6 @@
#include <LightSwitchUtils.h>
#include "ThemeScheduler.h"
#include <ThemeHelper.h>
#include <common/interop/shared_constants.h>
void ApplyTheme(bool shouldBeLight);
@@ -38,7 +37,7 @@ void LightSwitchStateManager::OnTick(int currentMinutes)
}
}
// Called when manual override is triggered (via hotkey)
// Called when manual override is triggered
void LightSwitchStateManager::OnManualOverride()
{
std::lock_guard<std::mutex> lock(_stateMutex);
@@ -46,19 +45,15 @@ void LightSwitchStateManager::OnManualOverride()
_state.isManualOverride = !_state.isManualOverride;
// When entering manual override, sync internal theme state to match the current system
// The hotkey handler in ModuleInterface has already toggled the theme, so we read the new state
if (_state.isManualOverride)
{
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
// Notify PowerDisplay about the theme change triggered by hotkey
// The theme has already been applied by ModuleInterface, we just need to notify PowerDisplay
NotifyPowerDisplay(_state.isSystemLightActive);
}
EvaluateAndApplyIfNeeded();
@@ -269,61 +264,7 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
// Notify PowerDisplay to apply display profile if configured
NotifyPowerDisplay(shouldBeLight);
}
_state.lastTickMinutes = now;
}
// Notify PowerDisplay module about theme change to apply display profiles
void LightSwitchStateManager::NotifyPowerDisplay(bool isLight)
{
const auto& settings = LightSwitchSettings::settings();
// Check if any profile is enabled and configured
bool shouldNotify = false;
if (isLight && settings.enableLightModeProfile && !settings.lightModeProfile.empty())
{
shouldNotify = true;
}
else if (!isLight && settings.enableDarkModeProfile && !settings.darkModeProfile.empty())
{
shouldNotify = true;
}
if (!shouldNotify)
{
return;
}
try
{
// Signal PowerDisplay with the specific theme event
// Using separate events for light/dark eliminates race conditions where PowerDisplay
// might read the registry before LightSwitch has finished updating it
const wchar_t* eventName = isLight
? CommonSharedConstants::LIGHT_SWITCH_LIGHT_THEME_EVENT
: CommonSharedConstants::LIGHT_SWITCH_DARK_THEME_EVENT;
Logger::info(L"[LightSwitchStateManager] Notifying PowerDisplay about theme change (isLight: {})", isLight);
HANDLE hThemeEvent = CreateEventW(nullptr, FALSE, FALSE, eventName);
if (hThemeEvent)
{
SetEvent(hThemeEvent);
CloseHandle(hThemeEvent);
Logger::info(L"[LightSwitchStateManager] Theme event signaled to PowerDisplay: {}", eventName);
}
else
{
Logger::warn(L"[LightSwitchStateManager] Failed to create theme event (error: {})", GetLastError());
}
}
catch (...)
{
Logger::error(L"[LightSwitchStateManager] Failed to notify PowerDisplay");
}
}

View File

@@ -48,7 +48,4 @@ private:
void EvaluateAndApplyIfNeeded();
bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon);
// Notify PowerDisplay module about theme change to apply display profiles
void NotifyPowerDisplay(bool isLight);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
namespace ImageResizer.Cli
{
public static class CliLogger
{
private static bool _initialized;
public static void Initialize(string logSubFolder)
{
if (!_initialized)
{
Logger.InitializeLogger(logSubFolder);
_initialized = true;
}
}
public static void Info(string message) => Logger.LogInfo(message);
public static void Warn(string message) => Logger.LogWarning(message);
public static void Error(string message) => Logger.LogError(message);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class DestinationOption : Option<string>
{
private static readonly string[] _aliases = ["--destination", "-d", "/d"];
public DestinationOption()
: base(_aliases, Properties.Resources.CLI_Option_Destination)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FileNameOption : Option<string>
{
private static readonly string[] _aliases = ["--filename", "-n"];
public FileNameOption()
: base(_aliases, Properties.Resources.CLI_Option_FileName)
{
}
}
}

View File

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

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FitOption : Option<ImageResizer.Models.ResizeFit?>
{
private static readonly string[] _aliases = ["--fit", "-f"];
public FitOption()
: base(_aliases, Properties.Resources.CLI_Option_Fit)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HeightOption : Option<double?>
{
private static readonly string[] _aliases = ["--height", "-h"];
public HeightOption()
: base(_aliases, Properties.Resources.CLI_Option_Height)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HelpOption : Option<bool>
{
private static readonly string[] _aliases = ["--help", "-?", "/?"];
public HelpOption()
: base(_aliases, Properties.Resources.CLI_Option_Help)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class IgnoreOrientationOption : Option<bool>
{
private static readonly string[] _aliases = ["--ignore-orientation"];
public IgnoreOrientationOption()
: base(_aliases, Properties.Resources.CLI_Option_IgnoreOrientation)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class KeepDateModifiedOption : Option<bool>
{
private static readonly string[] _aliases = ["--keep-date-modified"];
public KeepDateModifiedOption()
: base(_aliases, Properties.Resources.CLI_Option_KeepDateModified)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ProgressLinesOption : Option<bool>
{
private static readonly string[] _aliases = ["--progress-lines", "--accessible"];
public ProgressLinesOption()
: base(_aliases, "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
{
}
}
}

View File

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

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class RemoveMetadataOption : Option<bool>
{
private static readonly string[] _aliases = ["--remove-metadata"];
public RemoveMetadataOption()
: base(_aliases, Properties.Resources.CLI_Option_RemoveMetadata)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ReplaceOption : Option<bool>
{
private static readonly string[] _aliases = ["--replace", "-r"];
public ReplaceOption()
: base(_aliases, Properties.Resources.CLI_Option_Replace)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShowConfigOption : Option<bool>
{
private static readonly string[] _aliases = ["--show-config", "--config"];
public ShowConfigOption()
: base(_aliases, Properties.Resources.CLI_Option_ShowConfig)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShrinkOnlyOption : Option<bool>
{
private static readonly string[] _aliases = ["--shrink-only"];
public ShrinkOnlyOption()
: base(_aliases, Properties.Resources.CLI_Option_ShrinkOnly)
{
}
}
}

View File

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

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class UnitOption : Option<ImageResizer.Models.ResizeUnit?>
{
private static readonly string[] _aliases = ["--unit", "-u"];
public UnitOption()
: base(_aliases, Properties.Resources.CLI_Option_Unit)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class WidthOption : Option<double?>
{
private static readonly string[] _aliases = ["--width", "-w"];
public WidthOption()
: base(_aliases, Properties.Resources.CLI_Option_Width)
{
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -347,4 +347,156 @@
<data name="Input_AiSuperResolutionDescription" xml:space="preserve">
<value>Upscale images using on-device AI</value>
</data>
<!-- CLI Processing messages -->
<data name="CLI_ProcessingFiles" xml:space="preserve">
<value>Processing {0} file(s)...</value>
</data>
<data name="CLI_ProgressFormat" xml:space="preserve">
<value>Progress: {0}% ({1}/{2})</value>
</data>
<data name="CLI_CompletedWithErrors" xml:space="preserve">
<value>Completed with {0} error(s):</value>
</data>
<data name="CLI_AllFilesProcessed" xml:space="preserve">
<value>All files processed successfully.</value>
</data>
<data name="CLI_WarningInvalidSizeIndex" xml:space="preserve">
<value>Warning: Invalid size index {0}. Using default.</value>
</data>
<data name="CLI_NoInputFiles" xml:space="preserve">
<value>No input files or pipe specified. Showing usage.</value>
</data>
<!-- CLI Config display -->
<data name="CLI_ConfigTitle" xml:space="preserve">
<value>ImageResizer - Current Configuration</value>
</data>
<data name="CLI_ConfigGeneralSettings" xml:space="preserve">
<value>General Settings:</value>
</data>
<data name="CLI_ConfigShrinkOnly" xml:space="preserve">
<value> Shrink Only: {0}</value>
</data>
<data name="CLI_ConfigReplaceOriginal" xml:space="preserve">
<value> Replace Original: {0}</value>
</data>
<data name="CLI_ConfigIgnoreOrientation" xml:space="preserve">
<value> Ignore Orientation: {0}</value>
</data>
<data name="CLI_ConfigRemoveMetadata" xml:space="preserve">
<value> Remove Metadata: {0}</value>
</data>
<data name="CLI_ConfigKeepDateModified" xml:space="preserve">
<value> Keep Date Modified: {0}</value>
</data>
<data name="CLI_ConfigJpegQuality" xml:space="preserve">
<value> JPEG Quality: {0}</value>
</data>
<data name="CLI_ConfigPngInterlace" xml:space="preserve">
<value> PNG Interlace: {0}</value>
</data>
<data name="CLI_ConfigTiffCompress" xml:space="preserve">
<value> TIFF Compress: {0}</value>
</data>
<data name="CLI_ConfigFilenameFormat" xml:space="preserve">
<value> Filename Format: {0}</value>
</data>
<data name="CLI_ConfigCustomSize" xml:space="preserve">
<value>Custom Size:</value>
</data>
<data name="CLI_ConfigWidth" xml:space="preserve">
<value> Width: {0} {1}</value>
</data>
<data name="CLI_ConfigHeight" xml:space="preserve">
<value> Height: {0} {1}</value>
</data>
<data name="CLI_ConfigFitMode" xml:space="preserve">
<value> Fit Mode: {0}</value>
</data>
<data name="CLI_ConfigPresetSizes" xml:space="preserve">
<value>Preset Sizes: (* = currently selected)</value>
</data>
<data name="CLI_ConfigPresetSizeFormat" xml:space="preserve">
<value> [{0}]{1} {2}: {3}x{4} {5} ({6})</value>
</data>
<data name="CLI_ConfigCustomSelected" xml:space="preserve">
<value> [Custom]* {0}x{1} {2} ({3})</value>
</data>
<!-- CLI Usage help -->
<data name="CLI_UsageTitle" xml:space="preserve">
<value>ImageResizer - PowerToys Image Resizer CLI</value>
</data>
<data name="CLI_UsageLine" xml:space="preserve">
<value>Usage: PowerToys.ImageResizerCLI.exe [options] [files...]</value>
</data>
<data name="CLI_UsageOptions" xml:space="preserve">
<value>Options:</value>
</data>
<data name="CLI_UsageExamples" xml:space="preserve">
<value>Examples:</value>
</data>
<data name="CLI_UsageExampleHelp" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --help</value>
</data>
<data name="CLI_UsageExampleDimensions" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --width 800 --height 600 image.jpg</value>
</data>
<data name="CLI_UsageExamplePercent" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe -w 50 -h 50 -u Percent *.jpg</value>
</data>
<data name="CLI_UsageExamplePreset" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --size 0 -d "C:\Output" photo.png</value>
</data>
<!-- CLI Option Descriptions -->
<data name="CLI_Option_Destination" xml:space="preserve">
<value>Set destination directory</value>
</data>
<data name="CLI_Option_FileName" xml:space="preserve">
<value>Set output filename format (%1=original name, %2=size name)</value>
</data>
<data name="CLI_Option_Files" xml:space="preserve">
<value>Image files to resize</value>
</data>
<data name="CLI_Option_Fit" xml:space="preserve">
<value>Set fit mode (Fill, Fit, Stretch)</value>
</data>
<data name="CLI_Option_Height" xml:space="preserve">
<value>Set height</value>
</data>
<data name="CLI_Option_Help" xml:space="preserve">
<value>Show help information</value>
</data>
<data name="CLI_Option_IgnoreOrientation" xml:space="preserve">
<value>Ignore image orientation</value>
</data>
<data name="CLI_Option_KeepDateModified" xml:space="preserve">
<value>Keep original date modified</value>
</data>
<data name="CLI_Option_Quality" xml:space="preserve">
<value>Set JPEG quality level (1-100)</value>
</data>
<data name="CLI_Option_Replace" xml:space="preserve">
<value>Replace original files</value>
</data>
<data name="CLI_Option_ShowConfig" xml:space="preserve">
<value>Show current configuration</value>
</data>
<data name="CLI_Option_ShrinkOnly" xml:space="preserve">
<value>Only shrink images, don't enlarge</value>
</data>
<data name="CLI_Option_RemoveMetadata" xml:space="preserve">
<value>Remove metadata from resized images</value>
</data>
<data name="CLI_Option_Size" xml:space="preserve">
<value>Use preset size by index (0-based)</value>
</data>
<data name="CLI_Option_Unit" xml:space="preserve">
<value>Set unit (Pixel, Percent, Inch, Centimeter)</value>
</data>
<data name="CLI_Option_Width" xml:space="preserve">
<value>Set width</value>
</data>
</root>

View File

@@ -15,6 +15,7 @@ using System.IO.Abstractions;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Windows.Media.Imaging;
@@ -42,6 +43,7 @@ namespace ImageResizer.Properties
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
WriteIndented = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);

View File

@@ -130,6 +130,7 @@ namespace Peek.FilePreviewer.Previewers
}
else if (isMarkdown)
{
IsDevFilePreview = false;
var raw = await ReadHelper.Read(File.Path.ToString());
Preview = new Uri(MarkdownHelper.PreviewTempFile(raw, File.Path, TempFolderPath.Path));
}

View File

@@ -1,737 +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.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.UnitTests;
/// <summary>
/// Unit tests for MccsCapabilitiesParser class.
/// </summary>
[TestClass]
public class MccsCapabilitiesParserTests
{
private const string DellU3011Capabilities =
"(prot(monitor)type(lcd)model(U3011)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 06 08 10 12 14(01 05 08 0B 0C) 16 18 1A 52 60(01 03 04 0C 0F 11 12) AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 04 05) DF FD)mccs_ver(2.1)mswhql(1))";
// Real capabilities string from Dell P2416D monitor
private const string DellP2416DCapabilities =
"(prot(monitor)type(LCD)model(P2416D)cmds(01 02 03 07 0C E3 F3) vcp(02 04 05 08 10 12 14(05 08 0B 0C) 16 18 1A 52 60(01 11 0F) AA(01 02) AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 05) DF E0 E1 E2(00 01 02 04 0E 12 14 19) F0(00 08) F1(01 02) F2 FD) mswhql(1)asset_eep(40)mccs_ver(2.1))";
// Simple test string
private const string SimpleCapabilities =
"(prot(monitor)type(lcd)model(TestMonitor)vcp(10 12)mccs_ver(2.2))";
// Capabilities without outer parentheses (some monitors like Apple Cinema Display)
private const string NoOuterParensCapabilities =
"prot(monitor)type(lcd)model(TestMonitor)vcp(10 12)mccs_ver(2.0)";
// Concatenated hex format (no spaces between hex bytes)
private const string ConcatenatedHexCapabilities =
"(prot(monitor)cmds(01020307)vcp(101214)mccs_ver(2.1))";
[TestMethod]
public void Parse_NullInput_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(null);
// Assert
Assert.IsNotNull(result);
Assert.IsNotNull(result.Capabilities);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
Assert.IsFalse(result.HasErrors);
}
[TestMethod]
public void Parse_EmptyString_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(string.Empty);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_WhitespaceOnly_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(" \t\n ");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_DellU3011_ParsesProtocol()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
}
[TestMethod]
public void Parse_DellU3011_ParsesType()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("lcd", result.Capabilities.Type);
}
[TestMethod]
public void Parse_DellU3011_ParsesModel()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("U3011", result.Capabilities.Model);
}
[TestMethod]
public void Parse_DellU3011_ParsesMccsVersion()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("2.1", result.Capabilities.MccsVersion);
}
[TestMethod]
public void Parse_DellU3011_ParsesCommands()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
var cmds = result.Capabilities.SupportedCommands;
Assert.IsNotNull(cmds);
Assert.AreEqual(7, cmds.Count);
CollectionAssert.Contains(cmds, (byte)0x01);
CollectionAssert.Contains(cmds, (byte)0x02);
CollectionAssert.Contains(cmds, (byte)0x03);
CollectionAssert.Contains(cmds, (byte)0x07);
CollectionAssert.Contains(cmds, (byte)0x0C);
CollectionAssert.Contains(cmds, (byte)0xE3);
CollectionAssert.Contains(cmds, (byte)0xF3);
}
[TestMethod]
public void Parse_DellU3011_ParsesBrightnessVcpCode()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x10 is Brightness
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
var brightnessInfo = result.Capabilities.GetVcpCodeInfo(0x10);
Assert.IsNotNull(brightnessInfo);
Assert.AreEqual(0x10, brightnessInfo.Value.Code);
Assert.IsTrue(brightnessInfo.Value.IsContinuous);
}
[TestMethod]
public void Parse_DellU3011_ParsesContrastVcpCode()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x12 is Contrast
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_DellU3011_ParsesInputSourceWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x60 is Input Source with discrete values
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x60));
var inputSourceInfo = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsNotNull(inputSourceInfo);
Assert.IsTrue(inputSourceInfo.Value.HasDiscreteValues);
// Should have values: 01 03 04 0C 0F 11 12
var values = inputSourceInfo.Value.SupportedValues;
Assert.AreEqual(7, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x03));
Assert.IsTrue(values.Contains(0x04));
Assert.IsTrue(values.Contains(0x0C));
Assert.IsTrue(values.Contains(0x0F));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x12));
}
[TestMethod]
public void Parse_DellU3011_ParsesColorPresetWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x14 is Color Preset
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
var colorPresetInfo = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsNotNull(colorPresetInfo);
Assert.IsTrue(colorPresetInfo.Value.HasDiscreteValues);
// Should have values: 01 05 08 0B 0C
var values = colorPresetInfo.Value.SupportedValues;
Assert.AreEqual(5, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x05));
Assert.IsTrue(values.Contains(0x08));
Assert.IsTrue(values.Contains(0x0B));
Assert.IsTrue(values.Contains(0x0C));
}
[TestMethod]
public void Parse_DellU3011_ParsesPowerModeWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0xD6 is Power Mode
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xD6));
var powerModeInfo = result.Capabilities.GetVcpCodeInfo(0xD6);
Assert.IsNotNull(powerModeInfo);
Assert.IsTrue(powerModeInfo.Value.HasDiscreteValues);
// Should have values: 01 04 05
var values = powerModeInfo.Value.SupportedValues;
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x04));
Assert.IsTrue(values.Contains(0x05));
}
[TestMethod]
public void Parse_DellU3011_TotalVcpCodeCount()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP codes: 02 04 05 06 08 10 12 14 16 18 1A 52 60 AC AE B2 B6 C6 C8 C9 D6 DC DF FD
Assert.AreEqual(24, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_DellP2416D_ParsesModel()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert
Assert.AreEqual("P2416D", result.Capabilities.Model);
}
[TestMethod]
public void Parse_DellP2416D_ParsesTypeWithDifferentCase()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - Type is "LCD" (uppercase) in this monitor
Assert.AreEqual("LCD", result.Capabilities.Type);
}
[TestMethod]
public void Parse_DellP2416D_ParsesMccsVersion()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert
Assert.AreEqual("2.1", result.Capabilities.MccsVersion);
}
[TestMethod]
public void Parse_DellP2416D_ParsesInputSourceWithThreeValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - VCP 0x60 Input Source has values: 01 11 0F
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x60));
var inputSourceInfo = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsNotNull(inputSourceInfo);
var values = inputSourceInfo.Value.SupportedValues;
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x0F));
}
[TestMethod]
public void Parse_DellP2416D_ParsesE2WithManyValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - VCP 0xE2 has values: 00 01 02 04 0E 12 14 19
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xE2));
var e2Info = result.Capabilities.GetVcpCodeInfo(0xE2);
Assert.IsNotNull(e2Info);
var values = e2Info.Value.SupportedValues;
Assert.AreEqual(8, values.Count);
}
[TestMethod]
public void Parse_NoOuterParentheses_StillParses()
{
// Act - Some monitors like Apple Cinema Display omit outer parens
var result = MccsCapabilitiesParser.Parse(NoOuterParensCapabilities);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual("TestMonitor", result.Capabilities.Model);
Assert.AreEqual("2.0", result.Capabilities.MccsVersion);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_ConcatenatedHexFormat_ParsesCorrectly()
{
// Act - Some monitors output hex without spaces: cmds(01020307)
var result = MccsCapabilitiesParser.Parse(ConcatenatedHexCapabilities);
// Assert
var cmds = result.Capabilities.SupportedCommands;
Assert.AreEqual(4, cmds.Count);
CollectionAssert.Contains(cmds, (byte)0x01);
CollectionAssert.Contains(cmds, (byte)0x02);
CollectionAssert.Contains(cmds, (byte)0x03);
CollectionAssert.Contains(cmds, (byte)0x07);
// VCP codes without spaces
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
}
[TestMethod]
public void Parse_NestedParenthesesInVcp_HandlesCorrectly()
{
// Arrange - VCP code 0x14 with nested discrete values
var input = "(vcp(14(01 05 08)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
var vcpInfo = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsNotNull(vcpInfo);
Assert.AreEqual(3, vcpInfo.Value.SupportedValues.Count);
}
[TestMethod]
public void Parse_MultipleVcpCodesWithMixedFormats_ParsesAll()
{
// Arrange - Mixed: some with values, some without
var input = "(vcp(10 12 14(01 05) 16 60(0F 11)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual(5, result.Capabilities.SupportedVcpCodes.Count);
// Continuous codes (no discrete values)
var brightness = result.Capabilities.GetVcpCodeInfo(0x10);
Assert.IsTrue(brightness?.IsContinuous ?? false);
var contrast = result.Capabilities.GetVcpCodeInfo(0x12);
Assert.IsTrue(contrast?.IsContinuous ?? false);
// Discrete codes (with values)
var colorPreset = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsTrue(colorPreset?.HasDiscreteValues ?? false);
Assert.AreEqual(2, colorPreset?.SupportedValues.Count);
var inputSource = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsTrue(inputSource?.HasDiscreteValues ?? false);
Assert.AreEqual(2, inputSource?.SupportedValues.Count);
}
[TestMethod]
public void Parse_UnknownSegments_DoesNotFail()
{
// Arrange - Contains unknown segments like mswhql and asset_eep
var input = "(prot(monitor)mswhql(1)asset_eep(40)vcp(10))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsFalse(result.HasErrors);
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
}
[TestMethod]
public void Parse_ExtraWhitespace_HandlesCorrectly()
{
// Arrange - Extra spaces everywhere
var input = "( prot( monitor ) type( lcd ) vcp( 10 12 14( 01 05 ) ) )";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual(3, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_LowercaseHex_ParsesCorrectly()
{
// Arrange - All lowercase hex
var input = "(cmds(01 0c e3 f3)vcp(10 ac ae))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
CollectionAssert.Contains(result.Capabilities.SupportedCommands, (byte)0xE3);
CollectionAssert.Contains(result.Capabilities.SupportedCommands, (byte)0xF3);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAC));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAE));
}
[TestMethod]
public void Parse_MixedCaseHex_ParsesCorrectly()
{
// Arrange - Mixed case hex
var input = "(vcp(Aa Bb cC Dd))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAA));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xBB));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xCC));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xDD));
}
[TestMethod]
public void Parse_MalformedInput_ReturnsPartialResults()
{
// Arrange - Missing closing paren for vcp section
var input = "(prot(monitor)vcp(10 12";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should still parse what it can
Assert.AreEqual("monitor", result.Capabilities.Protocol);
// VCP codes should still be parsed
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_InvalidHexInVcp_SkipsAndContinues()
{
// Arrange - Contains invalid hex "GG"
var input = "(vcp(10 GG 12 14))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should skip invalid and parse valid codes
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
Assert.AreEqual(3, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_SingleCharacterHex_Skipped()
{
// Arrange - Single char "A" is not valid (need 2 chars)
var input = "(vcp(10 A 12))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should only have 10 and 12
Assert.AreEqual(2, result.Capabilities.SupportedVcpCodes.Count);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void GetVcpCodesAsHexStrings_ReturnsSortedList()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60 10 14 12))");
// Act
var hexStrings = result.Capabilities.GetVcpCodesAsHexStrings();
// Assert - Should be sorted
Assert.AreEqual(4, hexStrings.Count);
Assert.AreEqual("0x10", hexStrings[0]);
Assert.AreEqual("0x12", hexStrings[1]);
Assert.AreEqual("0x14", hexStrings[2]);
Assert.AreEqual("0x60", hexStrings[3]);
}
[TestMethod]
public void GetSortedVcpCodes_ReturnsSortedEnumerable()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60 10 14 12))");
// Act
var sortedCodes = result.Capabilities.GetSortedVcpCodes().ToList();
// Assert
Assert.AreEqual(0x10, sortedCodes[0].Code);
Assert.AreEqual(0x12, sortedCodes[1].Code);
Assert.AreEqual(0x14, sortedCodes[2].Code);
Assert.AreEqual(0x60, sortedCodes[3].Code);
}
[TestMethod]
public void HasDiscreteValues_ContinuousCode_ReturnsFalse()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(10))");
// Act & Assert
Assert.IsFalse(result.Capabilities.HasDiscreteValues(0x10));
}
[TestMethod]
public void HasDiscreteValues_DiscreteCode_ReturnsTrue()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60(01 11)))");
// Act & Assert
Assert.IsTrue(result.Capabilities.HasDiscreteValues(0x60));
}
[TestMethod]
public void GetSupportedValues_DiscreteCode_ReturnsValues()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60(01 11 0F)))");
// Act
var values = result.Capabilities.GetSupportedValues(0x60);
// Assert
Assert.IsNotNull(values);
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x0F));
}
[TestMethod]
public void IsValid_ValidCapabilities_ReturnsTrue()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.IsTrue(result.IsValid);
Assert.IsFalse(result.HasErrors);
}
[TestMethod]
public void IsValid_EmptyVcpCodes_ReturnsFalse()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse("(prot(monitor)type(lcd))");
// Assert - No VCP codes = not valid
Assert.IsFalse(result.IsValid);
}
[TestMethod]
public void Capabilities_RawProperty_ContainsOriginalString()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse(SimpleCapabilities);
// Assert
Assert.AreEqual(SimpleCapabilities, result.Capabilities.Raw);
}
[TestMethod]
public void Parse_Window1Segment_ParsesCorrectly()
{
// Arrange - Full window segment with all fields
var input = "(vcp(10)window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
var window = result.Capabilities.Windows[0];
Assert.AreEqual(1, window.WindowNumber);
Assert.AreEqual("PIP", window.Type);
Assert.AreEqual(25, window.Area.X1);
Assert.AreEqual(25, window.Area.Y1);
Assert.AreEqual(1895, window.Area.X2);
Assert.AreEqual(1175, window.Area.Y2);
Assert.AreEqual(640, window.MaxSize.Width);
Assert.AreEqual(480, window.MaxSize.Height);
Assert.AreEqual(10, window.MinSize.Width);
Assert.AreEqual(10, window.MinSize.Height);
Assert.AreEqual(10, window.WindowId);
}
[TestMethod]
public void Parse_MultipleWindows_ParsesAll()
{
// Arrange - Two windows (PIP and PBP)
var input = "(window1(type(PIP) area(0 0 640 480))window2(type(PBP) area(640 0 1280 480)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(2, result.Capabilities.Windows.Count);
var window1 = result.Capabilities.Windows[0];
Assert.AreEqual(1, window1.WindowNumber);
Assert.AreEqual("PIP", window1.Type);
Assert.AreEqual(0, window1.Area.X1);
Assert.AreEqual(640, window1.Area.X2);
var window2 = result.Capabilities.Windows[1];
Assert.AreEqual(2, window2.WindowNumber);
Assert.AreEqual("PBP", window2.Type);
Assert.AreEqual(640, window2.Area.X1);
Assert.AreEqual(1280, window2.Area.X2);
}
[TestMethod]
public void Parse_WindowWithMissingFields_HandlesGracefully()
{
// Arrange - Window with only type and area (missing max, min, window)
var input = "(window1(type(PIP) area(0 0 640 480)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
var window = result.Capabilities.Windows[0];
Assert.AreEqual(1, window.WindowNumber);
Assert.AreEqual("PIP", window.Type);
Assert.AreEqual(640, window.Area.X2);
Assert.AreEqual(480, window.Area.Y2);
// Default values for missing fields
Assert.AreEqual(0, window.MaxSize.Width);
Assert.AreEqual(0, window.MinSize.Width);
Assert.AreEqual(0, window.WindowId);
}
[TestMethod]
public void Parse_WindowWithOnlyType_ParsesType()
{
// Arrange
var input = "(window1(type(PBP)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
Assert.AreEqual("PBP", result.Capabilities.Windows[0].Type);
}
[TestMethod]
public void Parse_NoWindowSegment_HasWindowSupportFalse()
{
// Arrange
var input = "(prot(monitor)vcp(10 12))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsFalse(result.Capabilities.HasWindowSupport);
Assert.AreEqual(0, result.Capabilities.Windows.Count);
}
[TestMethod]
public void Parse_WindowAreaDimensions_CalculatesCorrectly()
{
// Arrange
var input = "(window1(area(100 200 500 600)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
var area = result.Capabilities.Windows[0].Area;
Assert.AreEqual(400, area.Width); // 500 - 100
Assert.AreEqual(400, area.Height); // 600 - 200
}
[TestMethod]
public void Parse_RealWorldMccsWindowExample_ParsesCorrectly()
{
// Arrange - Example from MCCS 2.2a specification
var input = "(prot(display)type(lcd)model(PD3220U)cmds(01 02 03)vcp(10 12)mccs_ver(2.2)window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual("PD3220U", result.Capabilities.Model);
Assert.AreEqual("2.2", result.Capabilities.MccsVersion);
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual("PIP", result.Capabilities.Windows[0].Type);
}
[TestMethod]
public void Parse_WindowWithExtraSpaces_HandlesCorrectly()
{
// Arrange - Extra spaces in content
var input = "(window1( type( PIP ) area( 0 0 640 480 ) ))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual("PIP", result.Capabilities.Windows[0].Type);
Assert.AreEqual(640, result.Capabilities.Windows[0].Area.X2);
}
}

View File

@@ -1,94 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.UnitTests;
/// <summary>
/// Unit tests for MonitorMatchingHelper class.
/// Tests monitor key generation and matching logic.
/// </summary>
[TestClass]
public class MonitorMatchingHelperTests
{
[TestMethod]
public void GetMonitorKey_WithMonitor_ReturnsId()
{
// Arrange
var monitor = new Monitor { Id = "DDC_GSM5C6D_1", Name = "LG Monitor" };
// Act
var result = MonitorMatchingHelper.GetMonitorKey(monitor);
// Assert
Assert.AreEqual("DDC_GSM5C6D_1", result);
}
[TestMethod]
public void GetMonitorKey_NullMonitor_ReturnsEmptyString()
{
// Act
var result = MonitorMatchingHelper.GetMonitorKey(null);
// Assert
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void GetMonitorKey_EmptyId_ReturnsEmptyString()
{
// Arrange
var monitor = new Monitor { Id = string.Empty, Name = "Display Name" };
// Act
var result = MonitorMatchingHelper.GetMonitorKey(monitor);
// Assert
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void AreMonitorsSame_SameId_ReturnsTrue()
{
// Arrange
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
var monitor2 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 2" };
// Act
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, monitor2);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public void AreMonitorsSame_DifferentId_ReturnsFalse()
{
// Arrange
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
var monitor2 = new Monitor { Id = "DDC_GSM5C6D_2", Name = "Monitor 2" };
// Act
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, monitor2);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public void AreMonitorsSame_NullMonitor_ReturnsFalse()
{
// Arrange
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
// Act
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, null!);
// Assert
Assert.IsFalse(result);
}
}

View File

@@ -1,41 +0,0 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
<!-- Licensed under the MIT License. See LICENSE file in the project root for license information. -->
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>PowerDisplay.UnitTests</RootNamespace>
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\PowerDisplay.Lib.UnitTests\</OutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Hide build log files from Solution Explorer -->
<None Remove="*.log" />
<None Remove="*.binlog" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<PackageReference Include="Moq" />
<PackageReference Include="System.CodeDom">
<!-- This package is a transitive dependency, but we need to set it here so we can exclude the assets,
so it doesn't conflict with the dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="System.Diagnostics.EventLog">
<!-- This package is a transitive dependency, but we need to set it here so we can exclude the assets,
so it doesn't conflict with the dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowerDisplay.Lib\PowerDisplay.Lib.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,650 +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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Polly;
using Polly.Retry;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.NativeDelegates;
using static PowerDisplay.Common.Drivers.PInvoke;
using Monitor = PowerDisplay.Common.Models.Monitor;
// Type aliases matching Windows API naming conventions for better readability when working with native structures.
// These uppercase aliases are used consistently throughout this file to match Win32 API documentation.
using MONITORINFOEX = PowerDisplay.Common.Drivers.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// DDC/CI monitor controller for controlling external monitors
/// </summary>
public partial class DdcCiController : IMonitorController, IDisposable
{
/// <summary>
/// Represents a candidate monitor discovered during Phase 1 of monitor enumeration.
/// </summary>
/// <param name="Handle">Physical monitor handle for DDC/CI communication</param>
/// <param name="PhysicalMonitor">Native physical monitor structure with description</param>
/// <param name="MonitorInfo">Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)</param>
private readonly record struct CandidateMonitor(
IntPtr Handle,
PHYSICAL_MONITOR PhysicalMonitor,
MonitorDisplayInfo MonitorInfo);
/// <summary>
/// Delay between retry attempts for DDC/CI operations (in milliseconds)
/// </summary>
private const int RetryDelayMs = 100;
/// <summary>
/// Retry pipeline for getting capabilities string length (3 retries).
/// </summary>
private static readonly ResiliencePipeline<uint> CapabilitiesLengthRetryPipeline =
new ResiliencePipelineBuilder<uint>()
.AddRetry(new RetryStrategyOptions<uint>
{
MaxRetryAttempts = 2, // 2 retries = 3 total attempts
Delay = TimeSpan.FromMilliseconds(RetryDelayMs),
ShouldHandle = new PredicateBuilder<uint>().HandleResult(len => len == 0),
OnRetry = static args =>
{
Logger.LogWarning($"[Retry] GetCapabilitiesStringLength returned invalid result on attempt {args.AttemptNumber + 1}, retrying...");
return default;
},
})
.Build();
/// <summary>
/// Retry pipeline for getting capabilities string (5 retries).
/// </summary>
private static readonly ResiliencePipeline<string?> CapabilitiesStringRetryPipeline =
new ResiliencePipelineBuilder<string?>()
.AddRetry(new RetryStrategyOptions<string?>
{
MaxRetryAttempts = 4, // 4 retries = 5 total attempts
Delay = TimeSpan.FromMilliseconds(RetryDelayMs),
ShouldHandle = new PredicateBuilder<string?>().HandleResult(static str => string.IsNullOrEmpty(str)),
OnRetry = static args =>
{
Logger.LogWarning($"[Retry] GetCapabilitiesString returned invalid result on attempt {args.AttemptNumber + 1}, retrying...");
return default;
},
})
.Build();
private readonly PhysicalMonitorHandleManager _handleManager = new();
private readonly MonitorDiscoveryHelper _discoveryHelper;
private bool _disposed;
public DdcCiController()
{
_discoveryHelper = new MonitorDiscoveryHelper();
}
public string Name => "DDC/CI Monitor Controller";
/// <summary>
/// Get monitor brightness using VCP code 0x10
/// </summary>
public async Task<VcpFeatureValue> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return await GetVcpFeatureAsync(monitor, VcpCodeBrightness, cancellationToken);
}
/// <summary>
/// Set monitor brightness using VCP code 0x10
/// </summary>
public Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeBrightness, brightness, cancellationToken);
/// <summary>
/// Set monitor contrast
/// </summary>
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, contrast, cancellationToken);
/// <summary>
/// Set monitor volume
/// </summary>
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, cancellationToken);
/// <summary>
/// Get monitor color temperature using VCP code 0x14 (Select Color Preset)
/// Returns the raw VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
/// </summary>
public async Task<VcpFeatureValue> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return await GetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, cancellationToken);
}
/// <summary>
/// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
/// </summary>
public Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, colorTemperature, cancellationToken);
/// <summary>
/// Get current input source using VCP code 0x60
/// Returns the raw VCP value (e.g., 0x11 for HDMI-1)
/// </summary>
public async Task<VcpFeatureValue> GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return await GetVcpFeatureAsync(monitor, VcpCodeInputSource, cancellationToken);
}
/// <summary>
/// Set input source using VCP code 0x60
/// </summary>
public Task<MonitorOperationResult> SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, VcpCodeInputSource, inputSource, cancellationToken);
/// <summary>
/// Get monitor capabilities string with retry logic.
/// Uses cached CapabilitiesRaw if available to avoid slow I2C operations.
/// </summary>
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
// Check if capabilities are already cached
if (!string.IsNullOrEmpty(monitor.CapabilitiesRaw))
{
return monitor.CapabilitiesRaw;
}
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return string.Empty;
}
try
{
// Step 1: Get capabilities string length with retry
var length = CapabilitiesLengthRetryPipeline.Execute(() =>
{
if (GetCapabilitiesStringLength(monitor.Handle, out uint len) && len > 0)
{
return len;
}
return 0u;
});
if (length == 0)
{
Logger.LogWarning("[Retry] GetCapabilitiesStringLength failed after 3 attempts");
return string.Empty;
}
// Step 2: Get actual capabilities string with retry
var capsString = CapabilitiesStringRetryPipeline.Execute(
() => TryGetCapabilitiesString(monitor.Handle, length));
if (!string.IsNullOrEmpty(capsString))
{
return capsString;
}
Logger.LogWarning("[Retry] GetCapabilitiesString failed after 5 attempts");
}
catch (Exception ex)
{
Logger.LogError($"Exception getting capabilities string: {ex.Message}");
}
return string.Empty;
},
cancellationToken);
}
/// <summary>
/// Try to get capabilities string from monitor handle.
/// </summary>
private string? TryGetCapabilitiesString(IntPtr handle, uint length)
{
var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
try
{
if (CapabilitiesRequestAndCapabilitiesReply(handle, buffer, length))
{
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer);
}
return null;
}
finally
{
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
}
}
/// <summary>
/// Discover supported monitors using a three-phase approach:
/// Phase 1: Enumerate and collect candidate monitors with their handles
/// Phase 2: Fetch DDC/CI capabilities in parallel (slow I2C operations)
/// Phase 3: Create Monitor objects for valid DDC/CI monitors
/// </summary>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
try
{
// Get monitor display info from QueryDisplayConfig, keyed by device path (unique per target)
var allMonitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
// Phase 1: Collect candidate monitors
var monitorHandles = EnumerateMonitorHandles();
if (monitorHandles.Count == 0)
{
return Enumerable.Empty<Monitor>();
}
var candidateMonitors = await CollectCandidateMonitorsAsync(
monitorHandles, allMonitorDisplayInfo, cancellationToken);
if (candidateMonitors.Count == 0)
{
return Enumerable.Empty<Monitor>();
}
// Phase 2: Fetch capabilities in parallel
var fetchResults = await FetchCapabilitiesInParallelAsync(
candidateMonitors, cancellationToken);
// Phase 3: Create monitor objects
return CreateValidMonitors(fetchResults);
}
catch (Exception ex)
{
Logger.LogError($"DDC: DiscoverMonitorsAsync exception: {ex.Message}\nStack: {ex.StackTrace}");
return Enumerable.Empty<Monitor>();
}
}
/// <summary>
/// Enumerate all logical monitor handles using Win32 API.
/// </summary>
private List<IntPtr> EnumerateMonitorHandles()
{
var handles = new List<IntPtr>();
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
{
handles.Add(hMonitor);
return true;
}
if (!EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero))
{
Logger.LogWarning("DDC: EnumDisplayMonitors failed");
}
return handles;
}
/// <summary>
/// Get GDI device name for a monitor handle (e.g., "\\.\DISPLAY1").
/// </summary>
private unsafe string? GetGdiDeviceName(IntPtr hMonitor)
{
var monitorInfo = new MONITORINFOEX { CbSize = (uint)sizeof(MONITORINFOEX) };
if (GetMonitorInfo(hMonitor, &monitorInfo))
{
return monitorInfo.GetDeviceName();
}
return null;
}
/// <summary>
/// Phase 1: Collect all candidate monitors with their physical handles.
/// Matches physical monitors with MonitorDisplayInfo using GDI device name and friendly name.
/// Supports mirror mode where multiple physical monitors share the same GDI name.
/// </summary>
private async Task<List<CandidateMonitor>> CollectCandidateMonitorsAsync(
List<IntPtr> monitorHandles,
Dictionary<string, MonitorDisplayInfo> allMonitorDisplayInfo,
CancellationToken cancellationToken)
{
var candidates = new List<CandidateMonitor>();
foreach (var hMonitor in monitorHandles)
{
// Get GDI device name for this monitor (e.g., "\\.\DISPLAY1")
var gdiDeviceName = GetGdiDeviceName(hMonitor);
if (string.IsNullOrEmpty(gdiDeviceName))
{
Logger.LogWarning($"DDC: Failed to get GDI device name for hMonitor 0x{hMonitor:X}");
continue;
}
var physicalMonitors = await GetPhysicalMonitorsWithRetryAsync(hMonitor, cancellationToken);
if (physicalMonitors == null || physicalMonitors.Length == 0)
{
Logger.LogWarning($"DDC: Failed to get physical monitors for {gdiDeviceName} after retries");
continue;
}
// Find all MonitorDisplayInfo entries that match this GDI device name
// In mirror mode, multiple targets share the same GDI name
var matchingInfos = allMonitorDisplayInfo.Values
.Where(info => string.Equals(info.GdiDeviceName, gdiDeviceName, StringComparison.OrdinalIgnoreCase))
.ToList();
if (matchingInfos.Count == 0)
{
Logger.LogWarning($"DDC: No QueryDisplayConfig info for {gdiDeviceName}, skipping");
continue;
}
for (int i = 0; i < physicalMonitors.Length; i++)
{
var physicalMonitor = physicalMonitors[i];
if (i >= matchingInfos.Count)
{
Logger.LogWarning($"DDC: Physical monitor index {i} exceeds available QueryDisplayConfig entries ({matchingInfos.Count}) for {gdiDeviceName}");
break;
}
var monitorInfo = matchingInfos[i];
candidates.Add(new CandidateMonitor(physicalMonitor.HPhysicalMonitor, physicalMonitor, monitorInfo));
}
}
return candidates;
}
/// <summary>
/// Phase 2: Fetch DDC/CI capabilities in parallel for all candidate monitors.
/// This is the slow I2C operation (~4s per monitor), but parallelization
/// significantly reduces total time when multiple monitors are connected.
/// </summary>
private async Task<(CandidateMonitor Candidate, DdcCiValidationResult Result)[]> FetchCapabilitiesInParallelAsync(
List<CandidateMonitor> candidates,
CancellationToken cancellationToken)
{
var tasks = candidates.Select(candidate =>
Task.Run(
() => (Candidate: candidate, Result: DdcCiNative.FetchCapabilities(candidate.Handle)),
cancellationToken));
var results = await Task.WhenAll(tasks);
return results;
}
/// <summary>
/// Phase 3: Create Monitor objects for valid DDC/CI monitors.
/// A monitor is valid if it has capabilities with brightness support.
/// </summary>
private List<Monitor> CreateValidMonitors(
(CandidateMonitor Candidate, DdcCiValidationResult Result)[] fetchResults)
{
var monitors = new List<Monitor>();
var newHandleMap = new Dictionary<string, IntPtr>();
foreach (var (candidate, capResult) in fetchResults)
{
if (!capResult.IsValid)
{
continue;
}
var monitor = _discoveryHelper.CreateMonitorFromPhysical(
candidate.PhysicalMonitor,
candidate.MonitorInfo);
if (monitor == null)
{
continue;
}
// Set capabilities data
if (!string.IsNullOrEmpty(capResult.CapabilitiesString))
{
monitor.CapabilitiesRaw = capResult.CapabilitiesString;
}
if (capResult.VcpCapabilitiesInfo != null)
{
monitor.VcpCapabilitiesInfo = capResult.VcpCapabilitiesInfo;
UpdateMonitorCapabilitiesFromVcp(monitor, capResult.VcpCapabilitiesInfo);
// Initialize input source if supported
if (monitor.SupportsInputSource)
{
InitializeInputSource(monitor, candidate.Handle);
}
// Initialize color temperature if supported
if (monitor.SupportsColorTemperature)
{
InitializeColorTemperature(monitor, candidate.Handle);
}
}
// Initialize brightness (always supported for DDC/CI monitors)
InitializeBrightness(monitor, candidate.Handle);
monitors.Add(monitor);
newHandleMap[monitor.Id] = candidate.Handle;
}
_handleManager.UpdateHandleMap(newHandleMap);
return monitors;
}
/// <summary>
/// Initialize input source value for a monitor using VCP 0x60.
/// </summary>
private static void InitializeInputSource(Monitor monitor, IntPtr handle)
{
if (GetVCPFeatureAndVCPFeatureReply(handle, VcpCodeInputSource, IntPtr.Zero, out uint current, out uint _))
{
monitor.CurrentInputSource = (int)current;
}
}
/// <summary>
/// Initialize color temperature value for a monitor using VCP 0x14.
/// </summary>
private static void InitializeColorTemperature(Monitor monitor, IntPtr handle)
{
if (GetVCPFeatureAndVCPFeatureReply(handle, VcpCodeSelectColorPreset, IntPtr.Zero, out uint current, out uint _))
{
monitor.CurrentColorTemperature = (int)current;
}
}
/// <summary>
/// Initialize brightness value for a monitor using VCP 0x10.
/// </summary>
private static void InitializeBrightness(Monitor monitor, IntPtr handle)
{
if (GetVCPFeatureAndVCPFeatureReply(handle, VcpCodeBrightness, IntPtr.Zero, out uint current, out uint max))
{
var brightnessInfo = new VcpFeatureValue((int)current, 0, (int)max);
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
}
}
/// <summary>
/// Update monitor capability flags based on parsed VCP capabilities.
/// </summary>
private static void UpdateMonitorCapabilitiesFromVcp(Monitor monitor, VcpCapabilities vcpCaps)
{
// Check for Contrast support (VCP 0x12)
if (vcpCaps.SupportsVcpCode(VcpCodeContrast))
{
monitor.Capabilities |= MonitorCapabilities.Contrast;
}
// Check for Volume support (VCP 0x62)
if (vcpCaps.SupportsVcpCode(VcpCodeVolume))
{
monitor.Capabilities |= MonitorCapabilities.Volume;
}
// Check for Color Temperature support (VCP 0x14)
if (vcpCaps.SupportsVcpCode(VcpCodeSelectColorPreset))
{
monitor.SupportsColorTemperature = true;
}
}
/// <summary>
/// Get physical monitors with retry logic to handle Windows API occasionally returning NULL handles.
/// NULL handles are automatically filtered out by GetPhysicalMonitors; retry if any were filtered.
/// </summary>
/// <param name="hMonitor">Handle to the monitor</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Array of valid physical monitors, or null if failed after retries</returns>
private async Task<PHYSICAL_MONITOR[]?> GetPhysicalMonitorsWithRetryAsync(
IntPtr hMonitor,
CancellationToken cancellationToken)
{
const int maxRetries = 3;
const int retryDelayMs = 200;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
if (attempt > 0)
{
await Task.Delay(retryDelayMs, cancellationToken);
}
var monitors = _discoveryHelper.GetPhysicalMonitors(hMonitor, out bool hasNullHandles);
// Success: got valid monitors with no NULL handles filtered out
if (monitors != null && !hasNullHandles)
{
return monitors;
}
// Got monitors but some had NULL handles - retry to see if API stabilizes
if (monitors != null && hasNullHandles && attempt < maxRetries - 1)
{
Logger.LogWarning($"DDC: Some monitors had NULL handles on attempt {attempt + 1}, will retry");
continue;
}
// No monitors returned - retry
if (monitors == null && attempt < maxRetries - 1)
{
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null on attempt {attempt + 1}, will retry");
continue;
}
// Last attempt - return whatever we have (may have NULL handles filtered)
if (monitors != null && hasNullHandles)
{
Logger.LogWarning($"DDC: NULL handles still present after {maxRetries} attempts, using filtered result");
}
return monitors;
}
return null;
}
/// <summary>
/// Generic method to get VCP feature value.
/// </summary>
/// <param name="monitor">Monitor to query</param>
/// <param name="vcpCode">VCP code to read</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task<VcpFeatureValue> GetVcpFeatureAsync(
Monitor monitor,
byte vcpCode,
CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return VcpFeatureValue.Invalid;
}
if (GetVCPFeatureAndVCPFeatureReply(monitor.Handle, vcpCode, IntPtr.Zero, out uint current, out uint max))
{
return new VcpFeatureValue((int)current, 0, (int)max);
}
return VcpFeatureValue.Invalid;
},
cancellationToken);
}
/// <summary>
/// Generic method to set VCP feature value directly.
/// </summary>
private Task<MonitorOperationResult> SetVcpFeatureAsync(
Monitor monitor,
byte vcpCode,
int value,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return MonitorOperationResult.Failure("Invalid monitor handle");
}
try
{
if (SetVCPFeature(monitor.Handle, vcpCode, (uint)value))
{
return MonitorOperationResult.Success();
}
var lastError = GetLastError();
return MonitorOperationResult.Failure($"Failed to set VCP 0x{vcpCode:X2}", (int)lastError);
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Exception setting VCP 0x{vcpCode:X2}: {ex.Message}");
}
},
cancellationToken);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_handleManager?.Dispose();
_disposed = true;
}
}
}
}

View File

@@ -1,277 +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.Collections.Generic;
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.PInvoke;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// DDC/CI native API wrapper
/// </summary>
public static class DdcCiNative
{
/// <summary>
/// Fetches VCP capabilities string from a monitor and returns a validation result.
/// This is the slow I2C operation (~4 seconds per monitor) that should only be done once.
/// The result is cached regardless of success or failure.
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <returns>Validation result with capabilities data (or failure status)</returns>
public static DdcCiValidationResult FetchCapabilities(IntPtr hPhysicalMonitor)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return DdcCiValidationResult.Invalid;
}
try
{
// Try to get capabilities string (slow I2C operation)
var capsString = TryGetCapabilitiesString(hPhysicalMonitor);
if (string.IsNullOrEmpty(capsString))
{
return DdcCiValidationResult.Invalid;
}
// Parse the capabilities string
var parseResult = Utils.MccsCapabilitiesParser.Parse(capsString);
var capabilities = parseResult.Capabilities;
if (capabilities == null || capabilities.SupportedVcpCodes.Count == 0)
{
return DdcCiValidationResult.Invalid;
}
// Check if brightness (VCP 0x10) is supported - determines DDC/CI validity
bool supportsBrightness = capabilities.SupportsVcpCode(NativeConstants.VcpCodeBrightness);
return new DdcCiValidationResult(supportsBrightness, capsString, capabilities);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
return DdcCiValidationResult.Invalid;
}
}
/// <summary>
/// Try to get capabilities string from a physical monitor handle.
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <returns>Capabilities string, or null if failed</returns>
private static string? TryGetCapabilitiesString(IntPtr hPhysicalMonitor)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return null;
}
try
{
// Get capabilities string length
if (!GetCapabilitiesStringLength(hPhysicalMonitor, out uint length) || length == 0)
{
return null;
}
// Allocate buffer and get capabilities string
var buffer = Marshal.AllocHGlobal((int)length);
try
{
if (!CapabilitiesRequestAndCapabilitiesReply(hPhysicalMonitor, buffer, length))
{
return null;
}
return Marshal.PtrToStringAnsi(buffer);
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
return null;
}
}
/// <summary>
/// Gets GDI device name for a source (e.g., "\\.\DISPLAY1").
/// </summary>
/// <param name="adapterId">Adapter ID</param>
/// <param name="sourceId">Source ID</param>
/// <returns>GDI device name, or null if retrieval fails</returns>
private static unsafe string? GetSourceGdiDeviceName(LUID adapterId, uint sourceId)
{
try
{
var sourceName = new DisplayConfigSourceDeviceName
{
Header = new DisplayConfigDeviceInfoHeader
{
Type = DisplayconfigDeviceInfoGetSourceName,
Size = (uint)sizeof(DisplayConfigSourceDeviceName),
AdapterId = adapterId,
Id = sourceId,
},
};
var result = DisplayConfigGetDeviceInfo(&sourceName);
if (result == 0)
{
return sourceName.GetViewGdiDeviceName();
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
}
return null;
}
/// <summary>
/// Gets friendly name, hardware ID, and device path for a monitor target.
/// </summary>
/// <param name="adapterId">Adapter ID</param>
/// <param name="targetId">Target ID</param>
/// <returns>Tuple of (friendlyName, hardwareId, devicePath), any may be null if retrieval fails</returns>
private static unsafe (string? FriendlyName, string? HardwareId, string? DevicePath) GetTargetDeviceInfo(LUID adapterId, uint targetId)
{
try
{
var deviceName = new DisplayConfigTargetDeviceName
{
Header = new DisplayConfigDeviceInfoHeader
{
Type = DisplayconfigDeviceInfoGetTargetName,
Size = (uint)sizeof(DisplayConfigTargetDeviceName),
AdapterId = adapterId,
Id = targetId,
},
};
var result = DisplayConfigGetDeviceInfo(&deviceName);
if (result == 0)
{
// Extract friendly name
var friendlyName = deviceName.GetMonitorFriendlyDeviceName();
// Extract device path (unique per target, used as key)
var devicePath = deviceName.GetMonitorDevicePath();
// Extract hardware ID from EDID data
var manufacturerId = deviceName.EdidManufactureId;
var manufactureCode = ConvertManufactureIdToString(manufacturerId);
var productCode = deviceName.EdidProductCodeId.ToString("X4", System.Globalization.CultureInfo.InvariantCulture);
var hardwareId = $"{manufactureCode}{productCode}";
return (friendlyName, hardwareId, devicePath);
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
}
return (null, null, null);
}
/// <summary>
/// Converts manufacturer ID to 3-character manufacturer code
/// </summary>
/// <param name="manufacturerId">Manufacturer ID</param>
/// <returns>3-character manufacturer code</returns>
private static string ConvertManufactureIdToString(ushort manufacturerId)
{
// EDID manufacturer ID requires byte order swap first
manufacturerId = (ushort)(((manufacturerId & 0xff00) >> 8) | ((manufacturerId & 0x00ff) << 8));
// Extract 3 5-bit characters (each character is A-Z, where A=1, B=2, ..., Z=26)
var char1 = (char)('A' - 1 + ((manufacturerId >> 0) & 0x1f));
var char2 = (char)('A' - 1 + ((manufacturerId >> 5) & 0x1f));
var char3 = (char)('A' - 1 + ((manufacturerId >> 10) & 0x1f));
// Combine characters in correct order
return $"{char3}{char2}{char1}";
}
/// <summary>
/// Gets complete information for all monitors, keyed by GDI device name (e.g., "\\.\DISPLAY1").
/// This allows reliable matching with GetMonitorInfo results.
/// </summary>
/// <returns>Dictionary keyed by GDI device name containing monitor information</returns>
public static unsafe Dictionary<string, MonitorDisplayInfo> GetAllMonitorDisplayInfo()
{
var monitorInfo = new Dictionary<string, MonitorDisplayInfo>(StringComparer.OrdinalIgnoreCase);
try
{
// Get buffer sizes
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
if (result != 0)
{
return monitorInfo;
}
// Allocate buffers
var paths = new DisplayConfigPathInfo[pathCount];
var modes = new DisplayConfigModeInfo[modeCount];
// Query display configuration using fixed pointer
fixed (DisplayConfigPathInfo* pathsPtr = paths)
{
fixed (DisplayConfigModeInfo* modesPtr = modes)
{
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, pathsPtr, ref modeCount, modesPtr, IntPtr.Zero);
if (result != 0)
{
return monitorInfo;
}
}
}
// Get information for each path
// The path index corresponds to Windows Display Settings "Identify" number
for (int i = 0; i < pathCount; i++)
{
var path = paths[i];
// Get GDI device name from source info (e.g., "\\.\DISPLAY1")
var gdiDeviceName = GetSourceGdiDeviceName(path.SourceInfo.AdapterId, path.SourceInfo.Id);
if (string.IsNullOrEmpty(gdiDeviceName))
{
continue;
}
// Get target info (friendly name, hardware ID, device path)
var (friendlyName, hardwareId, devicePath) = GetTargetDeviceInfo(path.TargetInfo.AdapterId, path.TargetInfo.Id);
// Use device path as key - unique per target, supports mirror mode
if (string.IsNullOrEmpty(devicePath))
{
continue;
}
monitorInfo[devicePath] = new MonitorDisplayInfo
{
DevicePath = devicePath,
GdiDeviceName = gdiDeviceName,
FriendlyName = friendlyName ?? string.Empty,
HardwareId = hardwareId ?? string.Empty,
AdapterId = path.TargetInfo.AdapterId,
TargetId = path.TargetInfo.Id,
MonitorNumber = i + 1, // 1-based, matches Windows Display Settings
};
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
}
return monitorInfo;
}
}
}

View File

@@ -1,57 +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.
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// DDC/CI validation result containing both validation status and cached capabilities data.
/// This allows reusing capabilities data retrieved during validation, avoiding duplicate I2C calls.
/// </summary>
public struct DdcCiValidationResult
{
/// <summary>
/// Gets a value indicating whether the monitor has a valid DDC/CI connection with brightness support.
/// </summary>
public bool IsValid { get; }
/// <summary>
/// Gets the raw capabilities string retrieved during validation.
/// Null if retrieval failed.
/// </summary>
public string? CapabilitiesString { get; }
/// <summary>
/// Gets the parsed VCP capabilities info retrieved during validation.
/// Null if parsing failed.
/// </summary>
public Models.VcpCapabilities? VcpCapabilitiesInfo { get; }
/// <summary>
/// Gets a value indicating whether capabilities retrieval was attempted.
/// True means the result is from an actual attempt (success or failure).
/// </summary>
public bool WasAttempted { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DdcCiValidationResult"/> struct.
/// </summary>
public DdcCiValidationResult(bool isValid, string? capabilitiesString = null, Models.VcpCapabilities? vcpCapabilitiesInfo = null, bool wasAttempted = true)
{
IsValid = isValid;
CapabilitiesString = capabilitiesString;
VcpCapabilitiesInfo = vcpCapabilitiesInfo;
WasAttempted = wasAttempted;
}
/// <summary>
/// Gets an invalid validation result with no cached data.
/// </summary>
public static DdcCiValidationResult Invalid => new(false, null, null, true);
/// <summary>
/// Gets a result indicating validation was not attempted yet.
/// </summary>
public static DdcCiValidationResult NotAttempted => new(false, null, null, false);
}
}

View File

@@ -1,154 +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.Collections.Generic;
using ManagedCommon;
using PowerDisplay.Common.Models;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.PInvoke;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Helper class for discovering and creating monitor objects
/// </summary>
public class MonitorDiscoveryHelper
{
public MonitorDiscoveryHelper()
{
}
/// <summary>
/// Get physical monitors for a logical monitor.
/// Filters out any monitors with NULL handles (Windows API bug workaround).
/// </summary>
/// <param name="hMonitor">Handle to the logical monitor</param>
/// <param name="hasNullHandles">Output: true if any NULL handles were filtered out</param>
/// <returns>Array of valid physical monitors, or null if API call failed</returns>
internal PHYSICAL_MONITOR[]? GetPhysicalMonitors(IntPtr hMonitor, out bool hasNullHandles)
{
hasNullHandles = false;
try
{
if (!GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, out uint numMonitors))
{
Logger.LogWarning($"GetPhysicalMonitors: GetNumberOfPhysicalMonitorsFromHMONITOR failed for 0x{hMonitor:X}");
return null;
}
if (numMonitors == 0)
{
Logger.LogWarning($"GetPhysicalMonitors: numMonitors is 0");
return null;
}
var physicalMonitors = new PHYSICAL_MONITOR[numMonitors];
bool apiResult;
unsafe
{
fixed (PHYSICAL_MONITOR* ptr = physicalMonitors)
{
apiResult = GetPhysicalMonitorsFromHMONITOR(hMonitor, numMonitors, ptr);
}
}
if (!apiResult)
{
Logger.LogWarning($"GetPhysicalMonitors: GetPhysicalMonitorsFromHMONITOR failed");
return null;
}
// Filter out NULL handles and log each physical monitor
var validMonitors = new List<PHYSICAL_MONITOR>();
for (int i = 0; i < numMonitors; i++)
{
IntPtr handle = physicalMonitors[i].HPhysicalMonitor;
if (handle == IntPtr.Zero)
{
Logger.LogWarning($"GetPhysicalMonitors: Monitor [{i}] has NULL handle, filtering out");
hasNullHandles = true;
continue;
}
validMonitors.Add(physicalMonitors[i]);
}
return validMonitors.Count > 0 ? validMonitors.ToArray() : null;
}
catch (Exception ex)
{
Logger.LogError($"GetPhysicalMonitors: Exception: {ex.Message}");
return null;
}
}
/// <summary>
/// Create Monitor object from physical monitor and display info.
/// Uses MonitorDisplayInfo directly from QueryDisplayConfig for stable identification.
/// Note: Brightness is not initialized here - MonitorManager handles brightness initialization
/// after discovery to avoid slow I2C operations during the discovery phase.
/// </summary>
/// <param name="physicalMonitor">Physical monitor structure with handle and description</param>
/// <param name="monitorInfo">Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)</param>
internal Monitor? CreateMonitorFromPhysical(
PHYSICAL_MONITOR physicalMonitor,
MonitorDisplayInfo monitorInfo)
{
try
{
// Get hardware ID and friendly name directly from MonitorDisplayInfo
string edidId = monitorInfo.HardwareId ?? string.Empty;
string name = physicalMonitor.GetDescription() ?? string.Empty;
// Use FriendlyName from QueryDisplayConfig if available and not generic
if (!string.IsNullOrEmpty(monitorInfo.FriendlyName) &&
!monitorInfo.FriendlyName.Contains("Generic"))
{
name = monitorInfo.FriendlyName;
}
// Generate unique monitor Id: "DDC_{EdidId}_{MonitorNumber}"
string monitorId = !string.IsNullOrEmpty(edidId)
? $"DDC_{edidId}_{monitorInfo.MonitorNumber}"
: $"DDC_Unknown_{monitorInfo.MonitorNumber}";
// If still no good name, use default value
if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
{
name = "External Display";
}
var monitor = new Monitor
{
Id = monitorId,
Name = name.Trim(),
CurrentBrightness = 50, // Default value, will be updated by MonitorManager after discovery
MinBrightness = 0,
MaxBrightness = 100,
IsAvailable = true,
Handle = physicalMonitor.HPhysicalMonitor,
Capabilities = MonitorCapabilities.DdcCi,
CommunicationMethod = "DDC/CI",
MonitorNumber = monitorInfo.MonitorNumber,
GdiDeviceName = monitorInfo.GdiDeviceName ?? string.Empty,
Orientation = DmdoDefault, // Orientation will be set separately if needed
};
// Note: Feature detection (brightness, contrast, color temp, volume) is now done
// in MonitorManager after capabilities string is retrieved and parsed.
// This ensures we rely on capabilities data rather than trial-and-error probing.
return monitor;
}
catch (Exception ex)
{
Logger.LogError($"DDC: CreateMonitorFromPhysical exception: {ex.Message}");
return null;
}
}
}
}

View File

@@ -1,48 +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 Windows.Win32.Foundation;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Monitor display information structure
/// </summary>
public struct MonitorDisplayInfo
{
/// <summary>
/// Gets or sets the monitor device path (e.g., "\\?\DISPLAY#DELA1D8#...").
/// This is unique per target and used as the primary key.
/// </summary>
public string DevicePath { get; set; }
/// <summary>
/// Gets or sets the GDI device name (e.g., "\\.\DISPLAY1").
/// This is used to match with GetMonitorInfo results from HMONITOR.
/// In mirror mode, multiple targets may share the same GDI name.
/// </summary>
public string GdiDeviceName { get; set; }
/// <summary>
/// Gets or sets the friendly display name from EDID.
/// </summary>
public string FriendlyName { get; set; }
/// <summary>
/// Gets or sets the hardware ID derived from EDID manufacturer and product code.
/// </summary>
public string HardwareId { get; set; }
public LUID AdapterId { get; set; }
public uint TargetId { get; set; }
/// <summary>
/// Gets or sets the monitor number based on QueryDisplayConfig path index.
/// This matches the number shown in Windows Display Settings "Identify" feature.
/// 1-based index (paths[0] = 1, paths[1] = 2, etc.)
/// </summary>
public int MonitorNumber { get; set; }
}
}

View File

@@ -1,106 +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.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using ManagedCommon;
using static PowerDisplay.Common.Drivers.PInvoke;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Manages physical monitor handles - reuse, cleanup, and validation
/// </summary>
public partial class PhysicalMonitorHandleManager : IDisposable
{
// Mapping: monitorId -> physical handle (thread-safe)
private readonly ConcurrentDictionary<string, IntPtr> _monitorIdToHandleMap = new();
private readonly object _handleLock = new();
private bool _disposed;
/// <summary>
/// Update the handle mapping with new handles
/// </summary>
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
{
// Lock to ensure atomic update (cleanup + replace)
lock (_handleLock)
{
// Clean up unused handles before updating
CleanupUnusedHandles(newHandleMap);
// Update the device key map
_monitorIdToHandleMap.Clear();
foreach (var kvp in newHandleMap)
{
_monitorIdToHandleMap[kvp.Key] = kvp.Value;
}
}
}
/// <summary>
/// Clean up handles that are no longer in use.
/// Called within lock context. Optimized to O(n) using HashSet lookup.
/// </summary>
private void CleanupUnusedHandles(Dictionary<string, IntPtr> newHandles)
{
if (_monitorIdToHandleMap.IsEmpty)
{
return;
}
// Build HashSet of handles that will be reused (O(m))
var reusedHandles = new HashSet<IntPtr>(newHandles.Values);
// Find handles to destroy: in old map but not reused (O(n) with O(1) lookup)
var handlesToDestroy = _monitorIdToHandleMap.Values
.Where(h => h != IntPtr.Zero && !reusedHandles.Contains(h))
.ToList();
// Destroy unused handles
foreach (var handle in handlesToDestroy)
{
try
{
DestroyPhysicalMonitor(handle);
}
catch
{
// Silently ignore cleanup failures
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
// Release all physical monitor handles - get snapshot to avoid holding lock during cleanup
var handles = _monitorIdToHandleMap.Values.ToList();
foreach (var handle in handles)
{
if (handle != IntPtr.Zero)
{
try
{
DestroyPhysicalMonitor(handle);
}
catch
{
// Silently ignore cleanup failures
}
}
}
_monitorIdToHandleMap.Clear();
_disposed = true;
GC.SuppressFinalize(this);
}
}
}

View File

@@ -1,138 +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.
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// Windows API constant definitions
/// </summary>
public static class NativeConstants
{
/// <summary>
/// VCP code: Brightness (0x10)
/// Standard VESA MCCS brightness control.
/// This is the ONLY brightness code used by PowerDisplay.
/// </summary>
public const byte VcpCodeBrightness = 0x10;
/// <summary>
/// VCP code: Contrast (0x12)
/// Standard VESA MCCS contrast control.
/// </summary>
public const byte VcpCodeContrast = 0x12;
/// <summary>
/// VCP code: Audio Speaker Volume (0x62)
/// Standard VESA MCCS volume control for monitors with built-in speakers.
/// </summary>
public const byte VcpCodeVolume = 0x62;
/// <summary>
/// VCP code: Select Color Preset (0x14)
/// Standard VESA MCCS color temperature preset selection.
/// Supports discrete values like: 0x01=sRGB, 0x04=5000K, 0x05=6500K, 0x08=9300K.
/// This is the standard method for color temperature control.
/// </summary>
public const byte VcpCodeSelectColorPreset = 0x14;
/// <summary>
/// VCP code: Input Source (0x60)
/// Standard VESA MCCS input source selection.
/// Supports values like: 0x0F=DisplayPort-1, 0x10=DisplayPort-2, 0x11=HDMI-1, 0x12=HDMI-2, 0x1B=USB-C.
/// Note: Actual supported values depend on monitor capabilities.
/// </summary>
public const byte VcpCodeInputSource = 0x60;
/// <summary>
/// Query display config: only active paths
/// </summary>
public const uint QdcOnlyActivePaths = 0x00000002;
/// <summary>
/// Get source name (GDI device name like "\\.\DISPLAY1")
/// </summary>
public const uint DisplayconfigDeviceInfoGetSourceName = 1;
/// <summary>
/// Get target name (monitor friendly name and hardware ID)
/// </summary>
public const uint DisplayconfigDeviceInfoGetTargetName = 2;
/// <summary>
/// Retrieve the current settings for the display device.
/// </summary>
public const int EnumCurrentSettings = -1;
/// <summary>
/// The display is in the natural orientation of the device.
/// </summary>
public const int DmdoDefault = 0;
/// <summary>
/// The display is rotated 180 degrees (measured clockwise) from its natural orientation.
/// </summary>
public const int Dmdo180 = 2;
// ==================== DEVMODE field flags ====================
/// <summary>
/// DmDisplayOrientation field is valid.
/// </summary>
public const int DmDisplayOrientation = 0x00000080;
/// <summary>
/// DmPelsWidth field is valid.
/// </summary>
public const int DmPelsWidth = 0x00080000;
/// <summary>
/// DmPelsHeight field is valid.
/// </summary>
public const int DmPelsHeight = 0x00100000;
// ==================== ChangeDisplaySettings flags ====================
/// <summary>
/// Test the graphics mode but don't actually set it.
/// </summary>
public const uint CdsTest = 0x00000002;
// ==================== ChangeDisplaySettings result codes ====================
/// <summary>
/// The settings change was successful.
/// </summary>
public const int DispChangeSuccessful = 0;
/// <summary>
/// The computer must be restarted for the graphics mode to work.
/// </summary>
public const int DispChangeRestart = 1;
/// <summary>
/// The display driver failed the specified graphics mode.
/// </summary>
public const int DispChangeFailed = -1;
/// <summary>
/// The graphics mode is not supported.
/// </summary>
public const int DispChangeBadmode = -2;
/// <summary>
/// Unable to write settings to the registry.
/// </summary>
public const int DispChangeNotupdated = -3;
/// <summary>
/// An invalid set of flags was passed in.
/// </summary>
public const int DispChangeBadflags = -4;
/// <summary>
/// An invalid parameter was passed in.
/// </summary>
public const int DispChangeBadparam = -5;
}
}

View File

@@ -1,25 +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.Runtime.InteropServices;
namespace PowerDisplay.Common.Drivers;
/// <summary>
/// Native delegate type definitions
/// </summary>
public static class NativeDelegates
{
/// <summary>
/// Monitor enumeration procedure delegate
/// </summary>
/// <param name="hMonitor">Monitor handle</param>
/// <param name="hdcMonitor">Monitor device context</param>
/// <param name="lprcMonitor">Pointer to monitor rectangle</param>
/// <param name="dwData">User data</param>
/// <returns>True to continue enumeration</returns>
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
}

View File

@@ -1,55 +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.Runtime.InteropServices;
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// The DEVMODE structure contains information about the initialization and environment of a printer or a display device.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct DevMode
{
/// <summary>
/// Device name - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort DmDeviceName[32];
public short DmSpecVersion;
public short DmDriverVersion;
public short DmSize;
public short DmDriverExtra;
public int DmFields;
public int DmPositionX;
public int DmPositionY;
public int DmDisplayOrientation;
public int DmDisplayFixedOutput;
public short DmColor;
public short DmDuplex;
public short DmYResolution;
public short DmTTOption;
public short DmCollate;
/// <summary>
/// Form name - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort DmFormName[32];
public short DmLogPixels;
public int DmBitsPerPel;
public int DmPelsWidth;
public int DmPelsHeight;
public int DmDisplayFlags;
public int DmDisplayFrequency;
public int DmICMMethod;
public int DmICMIntent;
public int DmMediaType;
public int DmDitherType;
public int DmReserved1;
public int DmReserved2;
public int DmPanningWidth;
public int DmPanningHeight;
}
}

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.Runtime.InteropServices;
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// Display configuration 2D region
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DisplayConfig2DRegion
{
public uint Cx;
public uint Cy;
}
}

View File

@@ -1,22 +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.Runtime.InteropServices;
using Windows.Win32.Foundation;
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// Display configuration device information header
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DisplayConfigDeviceInfoHeader
{
public uint Type;
public uint Size;
public LUID AdapterId;
public uint Id;
}
}

View File

@@ -1,22 +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.Runtime.InteropServices;
using Windows.Win32.Foundation;
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// Display configuration mode information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DisplayConfigModeInfo
{
public uint InfoType;
public uint Id;
public LUID AdapterId;
public DisplayConfigModeInfoUnion ModeInfo;
}
}

View File

@@ -1,21 +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.Runtime.InteropServices;
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// Display configuration mode information union
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct DisplayConfigModeInfoUnion
{
[FieldOffset(0)]
public DisplayConfigTargetMode TargetMode;
[FieldOffset(0)]
public DisplayConfigSourceMode SourceMode;
}
}

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