mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-06 04:17:04 +01:00
Compare commits
7 Commits
copilot/su
...
shawn/fixI
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04a59d0c8 | ||
|
|
673cd5aba3 | ||
|
|
97997035f7 | ||
|
|
59962ffd3a | ||
|
|
3f106344b3 | ||
|
|
ab531b2620 | ||
|
|
48e95caf39 |
3
.github/actions/spell-check/allow/code.txt
vendored
3
.github/actions/spell-check/allow/code.txt
vendored
@@ -330,6 +330,9 @@ HHH
|
||||
riday
|
||||
YYY
|
||||
|
||||
# Unicode
|
||||
precomposed
|
||||
|
||||
# GitHub issue/PR commands
|
||||
azp
|
||||
feedbackhub
|
||||
|
||||
111
.github/actions/spell-check/expect.txt
vendored
111
.github/actions/spell-check/expect.txt
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
32
NOTICE.md
32
NOTICE.md
@@ -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
|
||||
@@ -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" />
|
||||
|
||||
93
doc/devdocs/cli-conventions.md
Normal file
93
doc/devdocs/cli-conventions.md
Normal 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
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace ManagedCommon
|
||||
PowerRename,
|
||||
PowerLauncher,
|
||||
PowerAccent,
|
||||
PowerDisplay,
|
||||
RegistryPreview,
|
||||
MeasureTool,
|
||||
ShortcutGuide,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (...)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
353
src/modules/fancyzones/FancyZonesCLI/Properties/Resources.Designer.cs
generated
Normal file
353
src/modules/fancyzones/FancyZonesCLI/Properties/Resources.Designer.cs
generated
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
src/modules/fancyzones/FancyZonesCLI/Properties/Resources.resx
Normal file
233
src/modules/fancyzones/FancyZonesCLI/Properties/Resources.resx
Normal 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 <UUID>' 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 >= 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 + <number> 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
50
src/modules/imageresizer/ImageResizerCLI/Program.cs
Normal file
50
src/modules/imageresizer/ImageResizerCLI/Program.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
320
src/modules/imageresizer/tests/Cli/CliSettingsApplierTests.cs
Normal file
320
src/modules/imageresizer/tests/Cli/CliSettingsApplierTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
268
src/modules/imageresizer/tests/Models/CliOptionsTests.cs
Normal file
268
src/modules/imageresizer/tests/Models/CliOptionsTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
28
src/modules/imageresizer/ui/Cli/CliLogger.cs
Normal file
28
src/modules/imageresizer/ui/Cli/CliLogger.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
122
src/modules/imageresizer/ui/Cli/CliSettingsApplier.cs
Normal file
122
src/modules/imageresizer/ui/Cli/CliSettingsApplier.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
124
src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs
Normal file
124
src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/DestinationOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/DestinationOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/FileNameOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/FileNameOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/modules/imageresizer/ui/Cli/Options/FilesArgument.cs
Normal file
17
src/modules/imageresizer/ui/Cli/Options/FilesArgument.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/FitOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/FitOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/HeightOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/HeightOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/HelpOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/HelpOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%)")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/modules/imageresizer/ui/Cli/Options/QualityOption.cs
Normal file
26
src/modules/imageresizer/ui/Cli/Options/QualityOption.cs
Normal 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.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/ReplaceOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/ReplaceOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/ShowConfigOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/ShowConfigOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/ShrinkOnlyOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/ShrinkOnlyOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/modules/imageresizer/ui/Cli/Options/SizeOption.cs
Normal file
26
src/modules/imageresizer/ui/Cli/Options/SizeOption.cs
Normal 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.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/UnitOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/UnitOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/WidthOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/WidthOption.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
261
src/modules/imageresizer/ui/Models/CliOptions.cs
Normal file
261
src/modules/imageresizer/ui/Models/CliOptions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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] <files>.
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user