diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 7c1b9f65dd..a43e81d077 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -11,6 +11,7 @@ ACCESSDENIED
ACCESSTOKEN
acfs
ACIE
+ACR
AClient
AColumn
acrt
@@ -58,7 +59,6 @@ AOC
aocfnapldcnfbofgmbbllojgocaelgdd
AOklab
aot
-APARTMENTTHREADED
APeriod
apicontract
apidl
@@ -96,6 +96,7 @@ asf
Ashcraft
AShortcut
ASingle
+ASUS
ASSOCCHANGED
ASSOCF
ASSOCSTR
@@ -105,6 +106,7 @@ atl
ATRIOX
aumid
authenticode
+AUO
AUTOBUDDY
AUTOCHECKBOX
AUTOHIDE
@@ -122,6 +124,10 @@ azureaiinference
azureinference
azureopenai
backticks
+Backlight
+Badflags
+Badmode
+Badparam
bbwe
BCIE
bck
@@ -195,6 +201,7 @@ Carlseibert
CAtl
caub
CBN
+Cds
cch
CCHDEVICENAME
CCHFORMNAME
@@ -214,6 +221,7 @@ checkmarks
CHILDACTIVATE
CHILDWINDOW
CHOOSEFONT
+Chunghwa
CIBUILD
cidl
CIELCh
@@ -228,7 +236,7 @@ claude
CLEARTYPE
clickable
clickonce
-CLIENTEDGE
+clientedge
clientid
clientside
CLIPBOARDUPDATE
@@ -240,6 +248,7 @@ CLSCTX
clsids
Clusion
cmder
+CMN
CMDNOTFOUNDMODULEINTERFACE
cmdpal
CMIC
@@ -294,6 +303,7 @@ Corpor
cotaskmem
COULDNOT
countof
+Cowait
covrun
cpcontrols
cph
@@ -312,12 +322,14 @@ CRECT
CRH
critsec
cropandlock
+crt
CROPTOSQUARE
Crossdevice
csdevkit
CSearch
CSettings
cso
+CSOT
CSRW
CStyle
cswin
@@ -360,11 +372,14 @@ DBPROPIDSET
DBPROPSET
DBT
DCBA
+DCapabilities
DCOM
DComposition
DCR
+ddc
DDEIf
Deact
+debouncer
debugbreak
decryptor
Dedup
@@ -382,6 +397,7 @@ DEFAULTTOPRIMARY
DEFERERASE
DEFPUSHBUTTON
deinitialization
+DELA
DELETEDKEYIMAGE
DELETESCANS
DEMOTYPE
@@ -416,18 +432,20 @@ DISABLEASACTIONKEY
DISABLENOSCROLL
diskmgmt
DISPLAYCHANGE
-DISPLAYCONFIG
+displayconfig
DISPLAYFLAGS
DISPLAYFREQUENCY
displayname
DISPLAYORIENTATION
+diu
divyan
Dlg
DLGFRAME
-DLGMODALFRAME
+dlgmodalframe
dlib
dllhost
dllmain
+Dmdo
DNLEN
DONOTROUND
DONTVALIDATEPATH
@@ -437,6 +455,7 @@ downsampling
downscale
DPICHANGED
DPIs
+DPMS
DPSAPI
DQTAT
DQTYPE
@@ -474,15 +493,19 @@ DWMWINDOWMAXIMIZEDCHANGE
DWORDLONG
dworigin
dwrite
+Dxva
dxgi
eab
+EAccess
easeofaccess
ecount
-Edid
+edid
EDITKEYBOARD
EDITSHORTCUTS
EDITTEXT
EFile
+EInvalid
+eep
eku
emojis
ENABLEDELAYEDEXPANSION
@@ -492,14 +515,15 @@ ENABLETEMPLATE
encodedlaunch
encryptor
ENDSESSION
+ENot
ENSUREVISIBLE
ENTERSIZEMOVE
ENTRYW
ENU
environmentvariables
-EOAC
EPO
epu
+EProvider
ERASEBKGND
EREOF
EResize
@@ -553,6 +577,7 @@ fdx
FErase
fesf
FFFF
+FFh
Figma
FILEEXPLORER
fileexploreraddons
@@ -595,6 +620,7 @@ formatetc
FORPARSING
foundrylocal
FRAMECHANGED
+Framechanged
FRestore
frm
FROMTOUCH
@@ -656,6 +682,8 @@ gwl
GWLP
GWLSTYLE
hangeul
+Hann
+Hantai
Hanzi
Hardlines
hardlinks
@@ -714,6 +742,7 @@ HKPD
HKU
HMD
hmenu
+HMON
hmodule
hmonitor
homies
@@ -731,6 +760,7 @@ hotkeys
hotlight
hotspot
HPAINTBUFFER
+HPhysical
HRAWINPUT
hredraw
hres
@@ -741,6 +771,7 @@ hsb
HSCROLL
hsi
HSpeed
+HSync
HTCLIENT
hthumbnail
HTOUCHINPUT
@@ -750,6 +781,7 @@ HVal
HValue
Hvci
hwb
+HWP
HWHEEL
HWINEVENTHOOK
hwnd
@@ -807,6 +839,7 @@ INITTOLOGFONTSTRUCT
INLINEPREFIX
inlines
Inno
+Innolux
INPC
inproc
INPUTHARDWARE
@@ -848,6 +881,7 @@ istep
ith
ITHUMBNAIL
IUI
+IVO
IUWP
IWIC
jeli
@@ -861,6 +895,7 @@ jpnime
Jsons
jsonval
jxr
+Kantai
keybd
KEYBDDATA
KEYBDINPUT
@@ -882,6 +917,7 @@ KILLFOCUS
killrunner
kmph
kvp
+KVM
Kybd
LARGEICON
lastcodeanalysissucceeded
@@ -903,6 +939,8 @@ LEFTTEXT
LError
LEVELID
LExit
+Lenovo
+LGD
LFU
lhwnd
LIBFUZZER
@@ -1008,6 +1046,7 @@ MAPTOSAMESHORTCUT
MAPVK
MARKDOWNPREVIEWHANDLERCPP
MAXIMIZEBOX
+Maximizebox
MAXSHORTCUTSIZE
maxversiontested
mber
@@ -1020,6 +1059,7 @@ MDL
mdtext
mdtxt
mdwn
+mccs
meme
memicmp
MENUITEMINFO
@@ -1029,9 +1069,7 @@ MERGEPAINT
Metacharacter
metadatamatters
Metadatas
-Metacharacter
metafile
-Metacharacter
mfc
Mgmt
Microwaved
@@ -1043,6 +1081,7 @@ mikeclayton
mindaro
Minimizable
MINIMIZEBOX
+Minimizebox
MINIMIZEEND
MINIMIZESTART
MINMAXINFO
@@ -1079,6 +1118,7 @@ MOVESIZEEND
MOVESIZESTART
MRM
Mrt
+mrt
mru
MSAL
msc
@@ -1104,6 +1144,7 @@ Mso
msrc
msstore
mstsc
+mswhql
msvcp
MT
MTND
@@ -1121,6 +1162,7 @@ MYICON
myorg
myrepo
NAMECHANGE
+Nanjing
namespaceanddescendants
nao
NCACTIVATE
@@ -1189,6 +1231,7 @@ NOMCX
NOMINMAX
NOMIRRORBITMAP
NOMOVE
+Nomove
NONANTIALIASED
nonclient
NONCLIENTMETRICSW
@@ -1210,6 +1253,7 @@ NORMALUSER
NOSEARCH
NOSENDCHANGING
NOSIZE
+Nosize
NOTHOUSANDS
NOTICKS
NOTIFICATIONSDLL
@@ -1217,9 +1261,11 @@ NOTIFYICONDATA
NOTIFYICONDATAW
NOTIMPL
NOTOPMOST
+Notopmost
NOTRACK
NOTSRCCOPY
NOTSRCERASE
+Notupdated
notwindows
NOTXORPEN
nowarn
@@ -1263,6 +1309,7 @@ opensource
openurl
openxmlformats
OPTIMIZEFORINVOKE
+Optronics
ORPHANEDDIALOGTITLE
ORSCANS
oss
@@ -1298,6 +1345,7 @@ PATINVERT
PATPAINT
pbc
pbi
+PBP
PBlob
pbrush
pcb
@@ -1312,6 +1360,7 @@ PDBs
PDEVMODE
pdisp
PDLL
+pdmodels
pdo
pdto
pdtobj
@@ -1334,6 +1383,7 @@ pguid
phbm
phbmp
phicon
+PHL
Photoshop
phwnd
pici
@@ -1366,6 +1416,8 @@ Popups
POPUPWINDOW
POSITIONITEM
POWERBROADCAST
+powerdisplay
+POWERDISPLAYMODULEINTERFACE
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1420,6 +1472,7 @@ projectname
PROPERTYKEY
Propset
PROPVARIANT
+prot
PRTL
prvpane
psapi
@@ -1447,12 +1500,16 @@ PTOKEN
PToy
ptstr
pui
+pvct
PWAs
pwcs
PWSTR
pwsz
pwtd
+Qdc
QDC
+qdc
+QDS
qit
QITAB
QITABENT
@@ -1740,6 +1797,7 @@ STARTUPINFOW
startupscreen
STATFLAG
STATICEDGE
+Staticedge
staticmethod
STATSTG
stdafx
@@ -1776,6 +1834,7 @@ subkeys
sublang
SUBMODULEUPDATE
subresource
+swp
Superbar
sut
svchost
@@ -1784,7 +1843,8 @@ SVGIO
svgz
SVSI
SWFO
-swp
+SWP
+Swp
SWPNOSIZE
SWPNOZORDER
SWRESTORE
@@ -1844,7 +1904,9 @@ THEMECHANGED
themeresources
THH
THICKFRAME
+Thickframe
THISCOMPONENT
+Tianma
throughs
TILEDWINDOW
TILLSON
@@ -1925,13 +1987,13 @@ UNLEN
UNORM
unremapped
Unsubscribes
+unsubscribes
unvirtualized
unwide
unzoom
UOffset
UOI
UPDATENOW
-UPDATEREGISTRY
updown
UPGRADINGPRODUCTCODE
upscaling
@@ -1958,6 +2020,8 @@ vcamp
vcenter
vcgtq
VCINSTALLDIR
+vcp
+vcpname
Vcpkg
VCRT
vcruntime
@@ -1970,6 +2034,8 @@ VERIFYCONTEXT
VERSIONINFO
VERTRES
VERTSIZE
+VESA
+vesa
VFT
vget
vgetq
@@ -2001,6 +2067,7 @@ VSM
vso
vsonline
VSpeed
+VSync
vstemplate
vstest
VSTHRD
@@ -2042,7 +2109,7 @@ winapi
winappsdk
windir
WINDOWCREATED
-WINDOWEDGE
+windowedge
WINDOWINFO
WINDOWNAME
WINDOWPLACEMENT
@@ -2066,12 +2133,12 @@ WINL
winlogon
winmd
winml
-WINNT
winres
winrt
winsdk
winsta
WINTHRESHOLD
+WINNT
WINVER
winxamlmanager
withinrafael
@@ -2083,6 +2150,7 @@ WKSG
Wlkr
wmain
Wman
+wmi
WMI
WMICIM
wmimgmt
@@ -2095,6 +2163,7 @@ WNDCLASSEX
WNDCLASSEXW
WNDCLASSW
WNDPROC
+Wndproc
wnode
wom
WORKSPACESEDITOR
diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt
index 024cef81a1..34b2ad9fe9 100644
--- a/.github/actions/spell-check/patterns.txt
+++ b/.github/actions/spell-check/patterns.txt
@@ -274,5 +274,18 @@ St&yle
# 0x6f677548 is user name but user folder causes a flag
\bx6f677548\b
+# Windows API constants and hardware interface terms
+\bCOINIT[_A-Z]*\b
+\bEOAC[_A-Z]*\b
+\b(?:RPC_C_AUTHN_)?WINNT\b
+\bUPDATEREGISTRY\b
+\b(?:CDS_)?UPDATEREGISTRY\b
+
+# Display interface terms (HDMI, DVI, DisplayPort)
+\b(?:HDMI|DVI|DisplayPort)(?:-\d+)?\b
+
+# 2D Region struct names
+\bDisplayConfig2?D?Region\b
+
# Microsoft Store URLs and product IDs
ms-windows-store://\S+
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index c1cd63aef0..6c51889d77 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -210,6 +210,11 @@
"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",
@@ -378,6 +383,8 @@
"UnitsNet.dll",
"UtfUnknown.dll",
"Wpf.Ui.dll",
+ "WmiLight.dll",
+ "WmiLight.Native.dll",
"Shmuelie.WinRTServer.dll",
"ToolGood.Words.Pinyin.dll"
],
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6b75c4159a..750a57fdfc 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -93,6 +93,7 @@
+
@@ -104,6 +105,7 @@
+
@@ -133,6 +135,7 @@
+
diff --git a/NOTICE.md b/NOTICE.md
index 4273edbb18..e1a32d6f76 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -10,6 +10,7 @@ This software incorporates material from third parties.
- Installer/Runner
- Measure tool
- Peek
+- PowerDisplay
- Registry Preview
## Utility: Color Picker
@@ -1519,6 +1520,35 @@ 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
@@ -1557,6 +1587,7 @@ SOFTWARE.
- NLog.Extensions.Logging
- NLog.Schema
- OpenAI
+- Polly.Core
- ReverseMarkdown
- ScipBe.Common.Office.OneNote
- SharpCompress
@@ -1569,5 +1600,6 @@ SOFTWARE.
- UnitsNet
- UTF.Unknown
- WinUIEx
+- WmiLight
- WPF-UI
- WyHash
\ No newline at end of file
diff --git a/PowerToys.slnx b/PowerToys.slnx
index 1dc26be394..e94d8a079d 100644
--- a/PowerToys.slnx
+++ b/PowerToys.slnx
@@ -684,6 +684,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/devdocs/modules/powerdisplay/design.md b/doc/devdocs/modules/powerdisplay/design.md
new file mode 100644
index 0000000000..ae2eb26479
--- /dev/null
+++ b/doc/devdocs/modules/powerdisplay/design.md
@@ -0,0 +1,1616 @@
+# PowerDisplay Module Design Document
+
+## Table of Contents
+
+1. [Background](#background)
+2. [Problem Statement](#problem-statement)
+3. [Goals](#goals)
+4. [Technical Terminology](#technical-terminology)
+ - [DDC/CI (Display Data Channel Command Interface)](#ddcci-display-data-channel-command-interface)
+ - [WMI (Windows Management Instrumentation)](#wmi-windows-management-instrumentation)
+5. [Architecture Overview](#architecture-overview)
+ - [High-Level Component Architecture](#high-level-component-architecture)
+ - [Project Structure](#project-structure)
+6. [Component Design](#component-design)
+ - [PowerDisplay Module Internal Structure](#powerdisplay-module-internal-structure)
+ - [DisplayChangeWatcher - Monitor Hot-Plug Detection](#displaychangewatcher---monitor-hot-plug-detection)
+ - [DDC/CI and WMI Interaction Architecture](#ddcci-and-wmi-interaction-architecture)
+ - [IMonitorController Interface Methods](#imonitorcontroller-interface-methods)
+ - [Why WmiLight Instead of System.Management](#why-wmilight-instead-of-systemmanagement)
+ - [Why We Need an MCCS Capabilities String Parser](#why-we-need-an-mccs-capabilities-string-parser)
+ - [Monitor Identification: Handles, IDs, and Names](#monitor-identification-handles-ids-and-names)
+ - [Settings UI and PowerDisplay Interaction Architecture](#settings-ui-and-powerdisplay-interaction-architecture)
+ - [Windows Events for IPC](#windows-events-for-ipc)
+ - [LightSwitch Profile Integration Architecture](#lightswitch-profile-integration-architecture)
+ - [LightSwitch Settings JSON Structure](#lightswitch-settings-json-structure)
+7. [Data Flow and Communication](#data-flow-and-communication)
+ - [Monitor Discovery Flow](#monitor-discovery-flow)
+8. [Sequence Diagrams](#sequence-diagrams)
+ - [Sequence: Modifying Color Temperature in Flyout UI](#sequence-modifying-color-temperature-in-flyout-ui)
+ - [Sequence: Creating and Saving a Profile](#sequence-creating-and-saving-a-profile)
+ - [Sequence: Applying Profile via LightSwitch Theme Change](#sequence-applying-profile-via-lightswitch-theme-change)
+ - [Sequence: UI Slider Adjustment (Brightness)](#sequence-ui-slider-adjustment-brightness)
+ - [Sequence: Module Enable/Disable Lifecycle](#sequence-module-enabledisable-lifecycle)
+9. [Future Considerations](#future-considerations)
+ - [Already Implemented](#already-implemented)
+ - [Potential Future Enhancements](#potential-future-enhancements)
+10. [References](#references)
+
+---
+
+## Background
+
+PowerDisplay is a PowerToys module designed to provide unified control over display
+settings across multiple monitors. Users often work with multiple displays (external monitors or laptop screens) and need a
+convenient way to adjust display parameters such as brightness, contrast, color
+temperature, volume, and input source without navigating through individual monitor
+OSD menus.
+
+The module leverages two primary technologies for monitor control:
+
+1. **DDC/CI (Display Data Channel Command Interface)** - For external monitors
+2. **WMI (Windows Management Instrumentation)** - For internal(laptop) displays
+
+---
+
+## Problem Statement
+
+Users with multiple monitors face several challenges:
+
+1. **Fragmented Control**: Each monitor requires separate OSD navigation
+2. **Inconsistent Brightness**: Difficult to maintain uniform brightness across displays
+3. **No Profile Support**: Cannot quickly switch display configurations for different
+ scenarios (gaming, productivity, movie watching)
+4. **Theme Integration Gap**: No automatic display adjustment when switching between
+ light and dark themes
+
+---
+
+## Goals
+
+- Provide unified control for brightness, contrast, volume, color temperature, and
+ input source across all connected monitors
+- Support both DDC/CI (external monitors) and WMI (laptop displays)
+- Support user-defined profiles for quick configuration switching
+- Integrate with LightSwitch module for automatic profile application on theme changes
+- Support global hotkey activation
+
+---
+
+## Technical Terminology
+
+### DDC/CI (Display Data Channel Command Interface)
+
+**DDC/CI** is a VESA standard (defined in the DDC specification) that allows
+bidirectional communication between a computer and a display over the I2C bus
+embedded in display cables.
+
+Most external monitors support DDC/CI, allowing applications to read and modify settings
+like brightness and contrast programmatically. But unfortunately, some manufacturers have poor implementations of their product's driver. They may not support DDC/CI or report itself supports DDC/CI (through capabilities string) when it does not. Even if a monitor supports DDC/CI, they may only support a limited subset of VCP codes, or have buggy implementations.
+
+And sometimes, users may connect monitor through a KVM switch or docking station that does not pass through DDC/CI commands correctly, and their docking may report it supports (hard code a capabilities string) but in reality, it does not. And will do thing when we try to send DDC/CI commands.
+
+PowerDisplay relies on the monitor-reported capabilities string to determine supported features. But if your monitor's manufacturer has a poor DDC/CI implementation, or you are connecting through a docking station that does not properly support DDC/CI, some features may not work as expected. And we can do nothing about it.
+
+**Key Concepts:**
+
+| Term | Description |
+|------|-------------|
+| **VCP (Virtual Control Panel)** | Standardized codes for monitor settings |
+| **MCCS (Monitor Command Control Set)** | VESA standard defining VCP codes |
+| **Capabilities String** | Monitor-reported string describing supported features |
+
+**Common VCP Codes Used:**
+
+| VCP Code | Name | Description |
+|----------|------|-------------|
+| `0x10` | Brightness | Display luminance (0-100) |
+| `0x12` | Contrast | Display contrast ratio (0-100) |
+| `0x14` | Select Color Preset | Color temperature presets (sRGB, 5000K, 6500K, etc.) |
+| `0x60` | Input Source | Active video input (HDMI, DP, USB-C, etc.) |
+| `0x62` | Volume | Speaker/headphone volume (0-100) |
+
+---
+
+### WMI (Windows Management Instrumentation)
+
+**WMI** is Microsoft's implementation of Web-Based Enterprise Management (WBEM),
+providing a standardized interface for accessing management information in Windows.
+For display control, WMI is primarily used for laptop internal displays that may not
+support DDC/CI.
+
+---
+
+## Architecture Overview
+
+### High-Level Component Architecture
+
+```mermaid
+flowchart TB
+ subgraph PowerToys["PowerToys Application"]
+ Runner["Runner (PowerToys.exe)"]
+ SettingsUI["Settings UI (WinUI 3)"]
+ LightSwitch["LightSwitch Module"]
+ end
+
+ subgraph PowerDisplayModule["PowerDisplay Module"]
+ ModuleInterface["Module Interface
(PowerDisplayModuleInterface.dll)"]
+ PowerDisplayApp["PowerDisplay App
(PowerToys.PowerDisplay.exe)"]
+ PowerDisplayLib["PowerDisplay.Lib
(Shared Library)"]
+ end
+
+ subgraph External["External"]
+ Hardware["Display Hardware
(External + Internal)"]
+ Storage["Persistent Storage
(settings.json, profiles.json)"]
+ end
+
+ Runner -->|"Loads DLL"| ModuleInterface
+ Runner -->|"Hotkey Events"| ModuleInterface
+ SettingsUI <-->|"Named Pipes"| Runner
+ SettingsUI -->|"Custom Actions
(Launch, ApplyProfile)"| ModuleInterface
+
+ ModuleInterface <-->|"Windows Events
(Show/Toggle/Terminate)"| PowerDisplayApp
+ PowerDisplayApp -->|"RefreshMonitors Event"| SettingsUI
+ LightSwitch -->|"Theme Events
(Light/Dark)"| PowerDisplayApp
+
+ PowerDisplayApp --> PowerDisplayLib
+ PowerDisplayLib -->|"DDC/CI (Dxva2.dll)"| Hardware
+ PowerDisplayLib -->|"WMI (WmiLight)"| Hardware
+ PowerDisplayLib -->|"ChangeDisplaySettingsEx"| Hardware
+ PowerDisplayApp <--> Storage
+
+ style Runner fill:#e1f5fe
+ style SettingsUI fill:#e1f5fe
+ style LightSwitch fill:#e1f5fe
+ style ModuleInterface fill:#fff3e0
+ style PowerDisplayApp fill:#fff3e0
+ style PowerDisplayLib fill:#e8f5e9
+ style Hardware fill:#f3e5f5
+ style Storage fill:#fffde7
+```
+
+This high-level view shows the module boundaries. See [Component Design](#component-design)
+for internal structure details.
+
+---
+
+### Project Structure
+
+```
+src/modules/powerdisplay/
+├── PowerDisplay.Lib/ # Core library (shared)
+│ ├── Drivers/
+│ │ ├── DDC/
+│ │ │ ├── DdcCiController.cs # DDC/CI implementation
+│ │ │ ├── DdcCiNative.cs # P/Invoke declarations & QueryDisplayConfig
+│ │ │ ├── MonitorDiscoveryHelper.cs
+│ │ │ └── PhysicalMonitorHandleManager.cs
+│ │ ├── WMI/
+│ │ │ └── WmiController.cs # WMI implementation (WmiLight library)
+│ │ ├── NativeConstants.cs # Win32 constants (VCP codes, etc.)
+│ │ ├── NativeDelegates.cs # P/Invoke delegate types
+│ │ ├── NativeStructures.cs # Win32 structures
+│ │ └── PInvoke.cs # P/Invoke declarations
+│ ├── Interfaces/
+│ │ ├── IMonitorController.cs # Controller abstraction
+│ │ ├── IMonitorData.cs # Monitor data interface
+│ │ └── IProfileService.cs # Profile service interface
+│ ├── Models/
+│ │ ├── Monitor.cs # Runtime monitor data
+│ │ ├── MonitorCapabilities.cs # Monitor capability flags
+│ │ ├── MonitorOperationResult.cs # Operation result
+│ │ ├── MonitorStateEntry.cs # Persisted monitor state
+│ │ ├── MonitorStateFile.cs # State file schema
+│ │ ├── PowerDisplayProfile.cs # Profile definition
+│ │ ├── PowerDisplayProfiles.cs # Profile collection
+│ │ ├── ProfileMonitorSetting.cs # Per-monitor profile settings
+│ │ ├── ColorPresetItem.cs # Color preset UI item
+│ │ ├── VcpCapabilities.cs # Parsed VCP capabilities
+│ │ └── VcpFeatureValue.cs # VCP feature value (current/min/max)
+│ ├── Serialization/
+│ │ └── ProfileSerializationContext.cs # JSON source generation
+│ ├── Services/
+│ │ ├── DisplayRotationService.cs # Display rotation via ChangeDisplaySettingsEx
+│ │ ├── MonitorStateManager.cs # State persistence (debounced save) and restore on startup
+│ │ └── ProfileService.cs # Profile persistence
+│ ├── Utils/
+│ │ ├── ColorTemperatureHelper.cs # Color temp utilities
+│ │ ├── EventHelper.cs # Windows Event utilities
+│ │ ├── MccsCapabilitiesParser.cs # DDC/CI capabilities parser
+│ │ ├── MonitorFeatureHelper.cs # Monitor feature utilities
+│ │ ├── MonitorMatchingHelper.cs # Profile-to-monitor matching
+│ │ ├── MonitorValueConverter.cs # Value conversion utilities
+│ │ ├── PnpIdHelper.cs # PnP manufacturer ID lookup
+│ │ ├── ProfileHelper.cs # Profile helper utilities
+│ │ ├── SimpleDebouncer.cs # Generic debouncer
+│ │ └── VcpNames.cs # VCP code and value name lookup
+│ └── PathConstants.cs # File path constants
+│
+├── PowerDisplay/ # WinUI 3 application
+│ ├── Assets/ # App icons and images
+│ ├── Configuration/
+│ │ └── AppConstants.cs # Application constants
+│ ├── Helpers/
+│ │ ├── DisplayChangeWatcher.cs # Monitor hot-plug detection (WinRT DeviceWatcher)
+│ │ ├── MonitorManager.cs # Discovery orchestrator
+│ │ ├── NativeEventWaiter.cs # Windows Event waiting
+│ │ ├── ResourceLoaderInstance.cs # Resource loader singleton
+│ │ ├── SettingsDeepLink.cs # Deep link to Settings UI
+│ │ ├── TrayIconService.cs # System tray integration
+│ │ ├── TypePreservation.cs # AOT type preservation
+│ │ └── WindowHelper.cs # Window utilities
+│ ├── PowerDisplayXAML/
+│ │ ├── App.xaml / App.xaml.cs # Application entry point
+│ │ ├── MainWindow.xaml / .cs # Main UI window
+│ │ ├── IdentifyWindow.xaml / .cs # Monitor identify overlay
+│ │ └── MonitorIcon.xaml / .cs # Monitor icon control
+│ ├── Serialization/
+│ │ └── JsonSourceGenerationContext.cs # JSON source generation
+│ ├── Services/
+│ │ └── LightSwitchService.cs # LightSwitch theme change handling
+│ ├── Strings/ # Localization resources (en-us)
+│ ├── Telemetry/
+│ │ └── Events/
+│ │ └── PowerDisplayStartEvent.cs # Telemetry event
+│ ├── ViewModels/
+│ │ ├── ColorTemperatureItem.cs # Color temperature dropdown item
+│ │ ├── InputSourceItem.cs # Input source dropdown item
+│ │ ├── MainViewModel.cs # Main VM (partial class)
+│ │ ├── MainViewModel.Monitors.cs # Monitor discovery methods
+│ │ ├── MainViewModel.Settings.cs # Settings persistence methods
+│ │ └── MonitorViewModel.cs # Per-monitor VM
+│ ├── GlobalUsings.cs # Global using directives
+│ └── Program.cs # Application entry point
+│
+├── PowerDisplay.Lib.UnitTests/ # Unit tests
+│ ├── MccsCapabilitiesParserTests.cs
+│ └── MonitorMatchingHelperTests.cs
+│
+└── PowerDisplayModuleInterface/ # C++ DLL (module interface)
+ ├── dllmain.cpp # PowertoyModuleIface impl
+ ├── Constants.h # Module constants (event names, timeouts)
+ ├── resource.h # Resource definitions
+ ├── pch.h / pch.cpp # Precompiled headers
+ └── Trace.h / Trace.cpp # ETW telemetry tracing
+```
+
+---
+
+## Component Design
+
+### PowerDisplay Module Internal Structure
+
+```mermaid
+flowchart TB
+ subgraph ExternalInputs["External Inputs"]
+ ModuleInterface["Module Interface
(C++ DLL)"]
+ LightSwitch["LightSwitch Module"]
+ end
+
+ subgraph WindowsEvents["Windows Events (IPC)"]
+ direction LR
+ ShowToggleEvents["Show/Toggle/Terminate
Events"]
+ ThemeChangedEvent["ThemeChanged
Events"]
+ end
+
+ subgraph PowerDisplayModule["PowerDisplay Module"]
+ subgraph PowerDisplayApp["PowerDisplay App (WinUI 3)"]
+ MainViewModel
+ MonitorViewModel
+ MonitorManager
+ DisplayChangeWatcher["DisplayChangeWatcher
(Hot-Plug Detection)"]
+ LightSwitchService["LightSwitchService
(Theme Handler)"]
+ end
+
+ subgraph PowerDisplayLib["PowerDisplay.Lib"]
+ subgraph Services
+ ProfileService
+ MonitorStateManager
+ DisplayRotationService
+ end
+ subgraph Drivers
+ DdcCiController
+ WmiController
+ end
+ subgraph Utils
+ PnpIdHelper["PnpIdHelper
(Manufacturer Names)"]
+ end
+ end
+ end
+
+ subgraph Storage["Persistent Storage"]
+ SettingsJson[("settings.json")]
+ ProfilesJson[("profiles.json")]
+ MonitorStateJson[("monitor_state.json")]
+ end
+
+ subgraph Hardware["Display Hardware"]
+ ExternalMonitor["External Monitor"]
+ LaptopDisplay["Laptop Display"]
+ end
+
+ %% External to Windows Events
+ ModuleInterface -->|"SetEvent()"| ShowToggleEvents
+ LightSwitch -->|"SetEvent()"| ThemeChangedEvent
+
+ %% Windows Events to App
+ ShowToggleEvents --> MainViewModel
+ ThemeChangedEvent --> LightSwitchService
+
+ %% App internal
+ LightSwitchService -.->|"Get profile name"| MainViewModel
+ MainViewModel --> MonitorViewModel
+ MonitorViewModel --> MonitorManager
+ DisplayChangeWatcher -.->|"DisplayChanged event"| MainViewModel
+
+ %% App to Lib services
+ MainViewModel --> ProfileService
+ MonitorViewModel --> MonitorStateManager
+ MonitorManager --> Drivers
+ MonitorManager --> DisplayRotationService
+
+ %% Utils used during discovery
+ WmiController --> PnpIdHelper
+
+ %% Services to Storage
+ ProfileService --> ProfilesJson
+ MonitorStateManager --> MonitorStateJson
+
+ %% Drivers to Hardware
+ DdcCiController -->|"DDC/CI"| ExternalMonitor
+ WmiController -->|"WMI"| LaptopDisplay
+ DisplayRotationService -->|"ChangeDisplaySettingsEx"| ExternalMonitor
+ DisplayRotationService -->|"ChangeDisplaySettingsEx"| LaptopDisplay
+
+ %% Force vertical layout: PowerDisplay.Lib above Storage/Hardware
+ PowerDisplayLib ~~~ Storage
+ PowerDisplayLib ~~~ Hardware
+
+ %% Styling
+ style ExternalInputs fill:#e3f2fd,stroke:#1976d2
+ style WindowsEvents fill:#fce4ec,stroke:#c2185b
+ style PowerDisplayModule fill:#fff8e1,stroke:#f57c00,stroke-width:2px
+ style PowerDisplayApp fill:#ffe0b2,stroke:#ef6c00
+ style PowerDisplayLib fill:#c8e6c9,stroke:#388e3c
+ style Services fill:#a5d6a7,stroke:#2e7d32
+ style Drivers fill:#ffccbc,stroke:#e64a19
+ style Utils fill:#dcedc8,stroke:#689f38
+ style Storage fill:#e1bee7,stroke:#8e24aa
+ style Hardware fill:#b2dfdb,stroke:#00897b
+```
+
+---
+
+### DisplayChangeWatcher - Monitor Hot-Plug Detection
+
+The `DisplayChangeWatcher` component provides automatic detection of monitor connect/disconnect events using the WinRT DeviceWatcher API.
+
+**Key Features:**
+- Uses `DisplayMonitor.GetDeviceSelector()` to watch for display device changes
+- Implements 1-second debouncing to coalesce rapid connect/disconnect events
+- Triggers `DisplayChanged` event to notify `MainViewModel` for monitor list refresh
+- Runs continuously after initial monitor discovery completes
+
+**Implementation Details:**
+```csharp
+// Device selector for display monitors
+string selector = DisplayMonitor.GetDeviceSelector();
+_deviceWatcher = DeviceInformation.CreateWatcher(selector);
+
+// Events monitored
+_deviceWatcher.Added += OnDeviceAdded; // New monitor connected
+_deviceWatcher.Removed += OnDeviceRemoved; // Monitor disconnected
+_deviceWatcher.Updated += OnDeviceUpdated; // Monitor properties changed
+```
+
+**Debouncing Strategy:**
+- Each device change event schedules a `DisplayChanged` event after 1 second
+- Subsequent events within the debounce window cancel the previous timer
+- This prevents excessive refreshes when multiple monitors change simultaneously
+
+---
+
+### DDC/CI and WMI Interaction Architecture
+
+```mermaid
+flowchart TB
+ subgraph Application["Application Layer"]
+ MM["MonitorManager"]
+ end
+
+ subgraph Abstraction["Abstraction Layer"]
+ IMC["IMonitorController Interface"]
+ end
+
+ subgraph Controllers["Controller Implementations"]
+ DDC["DdcCiController"]
+ WMI["WmiController"]
+ end
+
+ subgraph DDCStack["DDC/CI Stack"]
+ DDCNative["DdcCiNative
(P/Invoke)"]
+ PhysicalMonitorMgr["PhysicalMonitorHandleManager"]
+ MonitorDiscovery["MonitorDiscoveryHelper"]
+ CapParser["MccsCapabilitiesParser"]
+
+ subgraph Win32["Win32 APIs"]
+ User32["User32.dll
EnumDisplayMonitors
GetMonitorInfo"]
+ Dxva2["Dxva2.dll
GetVCPFeature
SetVCPFeature
Capabilities"]
+ end
+ end
+
+ subgraph WMIStack["WMI Stack"]
+ WmiLight["WmiLight Library
(Native AOT compatible,
NuGet package)"]
+ PnpHelper["PnpIdHelper
(Manufacturer name lookup)"]
+
+ subgraph WMIClasses["WMI Classes (root\\WMI)"]
+ WmiMonBright["WmiMonitorBrightness"]
+ WmiMonBrightMethods["WmiMonitorBrightnessMethods"]
+ end
+ end
+
+ subgraph Hardware["Hardware Layer"]
+ ExtMon["External Monitor
(DDC/CI capable)"]
+ LaptopMon["Laptop Display
(WMI only)"]
+ end
+
+ MM --> IMC
+ IMC -.-> DDC
+ IMC -.-> WMI
+
+ DDC --> DDCNative
+ DDC --> PhysicalMonitorMgr
+ DDC --> MonitorDiscovery
+ DDC --> CapParser
+
+ DDCNative --> User32
+ DDCNative --> Dxva2
+ MonitorDiscovery --> User32
+ PhysicalMonitorMgr --> Dxva2
+
+ Dxva2 -->|"I2C/DDC"| ExtMon
+
+ WMI --> WmiLight
+ WMI --> PnpHelper
+ WmiLight --> WmiMonBright
+ WmiLight --> WmiMonBrightMethods
+
+ WmiMonBrightMethods -->|"WMI Provider"| LaptopMon
+
+ style IMC fill:#bbdefb
+ style DDC fill:#c8e6c9
+ style WMI fill:#ffccbc
+```
+
+### IMonitorController Interface Methods
+
+```mermaid
+classDiagram
+ class IMonitorController {
+ <>
+ +Name: string
+ +DiscoverMonitorsAsync(cancellationToken) IEnumerable~Monitor~
+ +GetBrightnessAsync(monitor, cancellationToken) VcpFeatureValue
+ +SetBrightnessAsync(monitor, brightness, cancellationToken) MonitorOperationResult
+ +SetContrastAsync(monitor, contrast, cancellationToken) MonitorOperationResult
+ +SetVolumeAsync(monitor, volume, cancellationToken) MonitorOperationResult
+ +GetColorTemperatureAsync(monitor, cancellationToken) VcpFeatureValue
+ +SetColorTemperatureAsync(monitor, vcpValue, cancellationToken) MonitorOperationResult
+ +GetInputSourceAsync(monitor, cancellationToken) VcpFeatureValue
+ +SetInputSourceAsync(monitor, inputSource, cancellationToken) MonitorOperationResult
+ +Dispose()
+ }
+
+ class DdcCiController {
+ -_handleManager: PhysicalMonitorHandleManager
+ -_discoveryHelper: MonitorDiscoveryHelper
+ +Name: "DDC/CI Monitor Controller"
+ +DiscoverMonitorsAsync()
+ +GetBrightnessAsync(monitor)
+ +SetBrightnessAsync(monitor, brightness)
+ +SetContrastAsync(monitor, contrast)
+ +SetVolumeAsync(monitor, volume)
+ +GetColorTemperatureAsync(monitor)
+ +SetColorTemperatureAsync(monitor, colorTemperature)
+ +GetInputSourceAsync(monitor)
+ +SetInputSourceAsync(monitor, inputSource)
+ +GetCapabilitiesStringAsync(monitor) string
+ -GetVcpFeatureAsync(monitor, vcpCode)
+ -CollectCandidateMonitorsAsync()
+ -FetchCapabilitiesInParallelAsync()
+ -GetPhysicalMonitorsWithRetryAsync()
+ }
+
+ class WmiController {
+ +Name: "WMI Monitor Controller"
+ +DiscoverMonitorsAsync()
+ +GetBrightnessAsync(monitor)
+ +SetBrightnessAsync(monitor, brightness)
+ +SetContrastAsync(monitor, contrast)
+ +SetVolumeAsync(monitor, volume)
+ +GetColorTemperatureAsync(monitor)
+ +SetColorTemperatureAsync(monitor, colorTemperature)
+ +GetInputSourceAsync(monitor)
+ +SetInputSourceAsync(monitor, inputSource)
+ -ExtractHardwareIdFromInstanceName()
+ -GetMonitorDisplayInfoByHardwareId()
+ }
+
+ IMonitorController <|.. DdcCiController
+ IMonitorController <|.. WmiController
+```
+
+---
+
+### Why WmiLight Instead of System.Management
+
+PowerDisplay uses the [WmiLight](https://github.com/MartinKuschnik/WmiLight) NuGet package
+for WMI operations instead of the built-in `System.Management` namespace. This decision was
+driven by several technical requirements:
+
+#### Native AOT Compatibility
+
+PowerDisplay is built with Native AOT (Ahead-of-Time compilation) enabled for improved startup
+performance and reduced memory footprint. The standard `System.Management` namespace is **not
+compatible with Native AOT** because it relies heavily on runtime reflection and COM interop
+patterns that cannot be statically analyzed.
+
+WmiLight provides Native AOT support since version 5.0.0, making it the appropriate choice for
+AOT-compiled applications.
+
+```xml
+
+
+ true
+
+
+
+
+```
+
+#### Memory Leak Prevention
+
+The `System.Management` implementation has a known issue where it leaks memory on each WMI
+operation. While this might be acceptable for short-lived applications, PowerDisplay runs as
+a long-running background process that may perform frequent WMI queries (e.g., polling
+brightness levels, responding to theme changes). WmiLight addresses this memory leak issue.
+
+#### Lightweight API
+
+WmiLight provides a simpler, more lightweight API compared to `System.Management`:
+
+```csharp
+// WmiLight - Simple and direct
+using (var connection = new WmiConnection(@"root\WMI"))
+{
+ var results = connection.CreateQuery("SELECT * FROM WmiMonitorBrightness");
+ foreach (var obj in results)
+ {
+ var brightness = obj.GetPropertyValue("CurrentBrightness");
+ }
+}
+
+// System.Management - More verbose
+using (var searcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM WmiMonitorBrightness"))
+{
+ foreach (ManagementObject obj in searcher.Get())
+ {
+ var brightness = (byte)obj["CurrentBrightness"];
+ }
+}
+```
+
+#### Comparison Summary
+
+| Aspect | System.Management | WmiLight |
+|--------|-------------------|----------|
+| **Native AOT Support** | ❌ Not supported | ✅ Supported (v5.0.0+) |
+| **Memory Leaks** | ⚠️ Leaks on remote operations | ✅ No known leaks |
+| **API Complexity** | More verbose | Simpler, lighter |
+| **Long-running Services** | Not recommended | ✅ Recommended |
+| **Static Linking** | ❌ Not available | ✅ Optional (`PublishWmiLightStaticallyLinked`) |
+
+#### References
+
+- [WmiLight GitHub Repository](https://github.com/MartinKuschnik/WmiLight)
+- [WmiLight NuGet Package](https://www.nuget.org/packages/WmiLight)
+
+---
+
+### Why We Need an MCCS Capabilities String Parser
+
+DDC/CI monitors report their supported features via a **capabilities string** - a structured
+text format defined by the VESA MCCS (Monitor Control Command Set) standard. This string
+tells PowerDisplay which VCP codes the monitor supports and what values are valid for each.
+
+#### Example Capabilities String
+
+```
+(prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03 07)vcp(10 12 14(04 05 06) 60(11 12 0F))mccs_ver(2.2))
+```
+
+This string encodes:
+- **Protocol**: monitor
+- **Type**: LCD display
+- **Model**: PD3220U
+- **Supported commands**: 0x01, 0x02, 0x03, 0x07
+- **VCP codes**: 0x10 (brightness), 0x12 (contrast), 0x14 (color preset with values 4,5,6), 0x60 (input source with values 0x11, 0x12, 0x0F)
+- **MCCS version**: 2.2
+
+#### Why Parse It?
+
+| Use Case | How Parser Helps |
+|----------|------------------|
+| **Feature Detection** | Determine if monitor supports contrast, volume, color temperature, input switching |
+| **Input Source Dropdown** | Extract valid input source values (e.g., HDMI-1=0x11, DP=0x0F) for UI dropdown |
+| **Color Preset List** | Extract supported color presets (e.g., sRGB, 5000K, 6500K) |
+| **Diagnostics** | Display raw VCP codes in Settings UI for troubleshooting |
+| **PIP/PBP Support** | Parse window capabilities for Picture-in-Picture features |
+
+#### Why Not Use Regex?
+
+The MCCS capabilities string format has **nested parentheses** that regex cannot reliably handle:
+
+```
+vcp(10 12 14(04 05 06) 60(11 12 0F))
+ ^^^^^^^^^^^^ nested values
+```
+
+A recursive descent parser properly handles:
+- Nested parentheses at arbitrary depth
+- Variable whitespace (some monitors use `01 02 03`, others use `010203`)
+- Optional outer parentheses (some monitors omit them)
+- Unknown segments (graceful skip without failing)
+
+#### Implementation
+
+PowerDisplay implements a **zero-allocation recursive descent parser** using `ref struct` and
+`ReadOnlySpan` for optimal performance during monitor discovery.
+
+```csharp
+// Usage in DdcCiController
+var result = MccsCapabilitiesParser.Parse(capabilitiesString);
+if (result.IsValid)
+{
+ monitor.VcpCapabilitiesInfo = result.Capabilities;
+ // Now we know which features this monitor supports
+}
+```
+
+> **Detailed Design:** See [mccsParserDesign.md](./mccsParserDesign.md) for the complete
+> parser architecture, grammar definition, and implementation details.
+
+---
+
+### Monitor Identification: Handles, IDs, and Names
+
+Understanding how Windows identifies monitors is critical for PowerDisplay's operation.
+Different Windows APIs use different identifiers, and PowerDisplay must correlate these
+to provide a unified view across DDC/CI and WMI subsystems.
+
+#### Windows Display Subsystem Overview
+
+```mermaid
+flowchart TB
+ subgraph WindowsAPIs["Windows Display APIs"]
+ EnumDisplayMonitors["EnumDisplayMonitors
(User32.dll)"]
+ QueryDisplayConfig["QueryDisplayConfig
(User32.dll)"]
+ GetPhysicalMonitors["GetPhysicalMonitorsFromHMONITOR
(Dxva2.dll)"]
+ WmiMonitor["WMI root\\WMI
(WmiLight)"]
+ end
+
+ subgraph Identifiers["Monitor Identifiers"]
+ HMONITOR["HMONITOR
(Logical Monitor Handle)"]
+ GdiDeviceName["GDI Device Name
(e.g., \\\\.\\DISPLAY1)"]
+ PhysicalHandle["Physical Monitor Handle
(IntPtr for DDC/CI)"]
+ DevicePath["Device Path
(Unique per target)"]
+ HardwareId["Hardware ID
(e.g., DEL41B4)"]
+ InstanceName["WMI Instance Name
(e.g., DISPLAY\\BOE0900\\...)"]
+ MonitorNumber["Monitor Number
(1-based, matches Windows Settings)"]
+ end
+
+ EnumDisplayMonitors --> HMONITOR
+ HMONITOR --> GdiDeviceName
+ GetPhysicalMonitors --> PhysicalHandle
+
+ QueryDisplayConfig --> GdiDeviceName
+ QueryDisplayConfig --> DevicePath
+ QueryDisplayConfig --> HardwareId
+ QueryDisplayConfig --> MonitorNumber
+
+ WmiMonitor --> InstanceName
+ InstanceName --> HardwareId
+
+ style HMONITOR fill:#e3f2fd
+ style GdiDeviceName fill:#fff3e0
+ style PhysicalHandle fill:#c8e6c9
+ style DevicePath fill:#f3e5f5
+ style HardwareId fill:#ffccbc
+ style InstanceName fill:#ffe0b2
+ style MonitorNumber fill:#b2dfdb
+```
+
+#### Identifier Definitions
+
+| Identifier | Source | Format | Example | Scope |
+|------------|--------|--------|---------|-------|
+| **HMONITOR** | `EnumDisplayMonitors` | `IntPtr` | `0x00010001` | Logical monitor (may represent multiple physical monitors in clone mode) |
+| **GDI Device Name** | `GetMonitorInfo` / `QueryDisplayConfig` | String | `\\.\DISPLAY1` | Adapter output; multiple targets can share same GDI name in mirror mode |
+| **Physical Monitor Handle** | `GetPhysicalMonitorsFromHMONITOR` | `IntPtr` | `0x00000B14` | DDC/CI communication handle; valid for `GetVCPFeature` / `SetVCPFeature` |
+| **Device Path** | `QueryDisplayConfig` | String | `\\?\DISPLAY#DEL41B4#5&12a3b4c&0&UID123#{...}` | Unique per target; used as primary key in `MonitorDisplayInfo` |
+| **Hardware ID** | EDID (via `QueryDisplayConfig`) | String | `DEL41B4` | Manufacturer (3-char PnP ID) + Product Code (4-char hex); identifies monitor model |
+| **WMI Instance Name** | `WmiMonitorBrightness` | String | `DISPLAY\BOE0900\4&10fd3ab1&0&UID265988_0` | WMI object identifier; contains hardware ID in second segment |
+| **Monitor Number** | `QueryDisplayConfig` path index | Integer | `1`, `2`, `3` | 1-based; matches Windows Settings → Display → "Identify" feature |
+
+#### DDC/CI Monitor Discovery Flow
+
+```mermaid
+sequenceDiagram
+ participant App as PowerDisplay
+ participant Enum as EnumDisplayMonitors
+ participant Info as GetMonitorInfo
+ participant QDC as QueryDisplayConfig
+ participant Phys as GetPhysicalMonitors
+ participant DDC as DDC/CI (I2C)
+
+ App->>Enum: EnumDisplayMonitors(callback)
+ Enum-->>App: HMONITOR handles
+
+ loop For each HMONITOR
+ App->>Info: GetMonitorInfo(hMonitor)
+ Info-->>App: GDI Device Name (e.g., "\\.\DISPLAY1")
+
+ App->>Phys: GetPhysicalMonitorsFromHMONITOR(hMonitor)
+ Phys-->>App: Physical Monitor Handle(s) + Description
+ end
+
+ App->>QDC: QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS)
+ QDC-->>App: MonitorDisplayInfo[] (DevicePath, GdiDeviceName, HardwareId, MonitorNumber)
+
+ Note over App: Match Physical Handles to MonitorDisplayInfo
using GDI Device Name
+
+ loop For each Physical Handle
+ App->>DDC: GetCapabilitiesStringLength(handle)
+ DDC-->>App: Capabilities length
+ App->>DDC: CapabilitiesRequestAndCapabilitiesReply(handle)
+ DDC-->>App: Capabilities string (MCCS format)
+ end
+
+ Note over App: Create Monitor objects with:
- Handle (Physical Monitor Handle)
- MonitorNumber (from QueryDisplayConfig)
- GdiDeviceName (for rotation APIs)
+```
+
+#### WMI Monitor Discovery Flow
+
+```mermaid
+sequenceDiagram
+ participant App as PowerDisplay
+ participant WMI as WmiLight
+ participant QDC as QueryDisplayConfig
+ participant PnP as PnpIdHelper
+
+ App->>WMI: Query WmiMonitorBrightness
+ WMI-->>App: InstanceName, CurrentBrightness
+
+ Note over App: Extract HardwareId from InstanceName
"DISPLAY\BOE0900\..." → "BOE0900"
+
+ App->>QDC: GetAllMonitorDisplayInfo()
+ QDC-->>App: MonitorDisplayInfo[] (keyed by DevicePath)
+
+ Note over App: Match WMI monitor to QueryDisplayConfig
by comparing HardwareId
+
+ App->>PnP: GetBuiltInDisplayName("BOE0900")
+ PnP-->>App: "BOE Built-in Display"
+
+ Note over App: Create Monitor objects with:
- InstanceName (for WMI queries)
- MonitorNumber (from QueryDisplayConfig)
- GdiDeviceName (for rotation APIs)
+```
+
+#### Key Relationships
+
+##### GDI Device Name ↔ Physical Monitors
+
+```mermaid
+flowchart TB
+ HMON["HMONITOR (Logical)"]
+
+ HMON --> GDI["GetMonitorInfo()
→ GDI Device Name
\.DISPLAY1"]
+ HMON --> GetPhys["GetPhysicalMonitorsFromHMONITOR()"]
+
+ GetPhys --> PM0["Physical Monitor 0
Handle: 0x0B14
Desc: Dell U2722D"]
+ GetPhys --> PM1["Physical Monitor 1
Handle: 0x0B18
Desc: Dell U2722D
Mirror mode"]
+
+ style HMON fill:#e3f2fd
+ style PM0 fill:#fff3e0
+ style PM1 fill:#fff3e0
+```
+
+In **mirror/clone mode**, multiple physical monitors share the same GDI device name.
+QueryDisplayConfig returns multiple paths with the same `GdiDeviceName` but different
+`DevicePath` values, allowing us to distinguish them.
+
+##### DisplayPort Daisy Chain (MST - Multi-Stream Transport)
+
+**Daisy chaining** allows multiple monitors to be connected in series through a single
+DisplayPort output using MST (Multi-Stream Transport) technology. This creates unique
+challenges for monitor identification.
+
+```mermaid
+flowchart LR
+ GPU["GPU
(Single DP Port)"]
+ MonA["Monitor A
(MST Hub)"]
+ MonB["Monitor B
(End)"]
+
+ GPU -->|"DP"| MonA -->|"DP"| MonB
+
+ subgraph Result["Result: Multiple Logical Displays"]
+ D1["DISPLAY1"]
+ D2["DISPLAY2"]
+ end
+
+ GPU -.-> Result
+
+ style GPU fill:#bbdefb
+ style MonA fill:#c8e6c9
+ style MonB fill:#c8e6c9
+ style Result fill:#fff3e0
+```
+
+**How Windows Handles MST:**
+
+| Aspect | Behavior |
+|--------|----------|
+| **HMONITOR** | Each daisy-chained monitor gets its own HMONITOR |
+| **GDI Device Name** | Each monitor gets a unique GDI name (e.g., `\\.\DISPLAY1`, `\\.\DISPLAY2`) |
+| **Physical Monitor Handle** | Each monitor has its own physical handle for DDC/CI |
+| **Device Path** | Unique for each monitor in the chain |
+| **Hardware ID** | Different if monitors are different models; same if identical models |
+
+**MST vs Clone Mode Comparison:**
+
+| Property | MST Daisy Chain (Extended Desktop) | Clone/Mirror Mode |
+|----------|-----------------------------------|-------------------|
+| **HMONITOR** | Separate per monitor (HMONITOR_1, HMONITOR_2, ...) | Shared (single HMONITOR_1) |
+| **GDI Device Name** | Unique per monitor (`\\.\DISPLAY1`, `\\.\DISPLAY2`, ...) | Shared (`\\.\DISPLAY1`) |
+| **Physical Handle** | One per HMONITOR (A, B, C) | Multiple per HMONITOR (A, B) |
+| **DevicePath** | Unique per monitor (unique1, unique2, ...) | Unique per monitor (unique1, unique2) |
+| **Behavior** | Each monitor = independent logical display | Multiple monitors share same logical display |
+
+**PowerDisplay Handling of MST:**
+
+1. **Discovery**: `EnumDisplayMonitors` returns separate HMONITOR for each MST monitor
+2. **Physical Handles**: `GetPhysicalMonitorsFromHMONITOR` returns one handle per HMONITOR
+3. **Matching**: QueryDisplayConfig provides unique DevicePath for each MST target
+4. **DDC/CI**: Each monitor in the chain can be controlled independently via its handle
+
+**Identifying Same-Model Monitors in Daisy Chain:**
+
+When multiple identical monitors are daisy-chained (same Hardware ID), PowerDisplay
+distinguishes them using:
+
+- **MonitorNumber**: Different path index in QueryDisplayConfig (1, 2, 3...)
+- **DevicePath**: Unique system-generated path for each target
+- **Monitor.Id**: Format `DDC_{HardwareId}_{MonitorNumber}` ensures uniqueness
+
+Example with two identical Dell U2722D monitors:
+
+| Monitor | Id | MonitorNumber |
+|---------|-----|---------------|
+| Monitor 1 | `DDC_DEL41B4_1` | 1 |
+| Monitor 2 | `DDC_DEL41B4_2` | 2 |
+
+##### Connection Mode Summary
+
+| Mode | HMONITOR | GDI Device Name | Physical Handles | Use Case |
+|------|----------|-----------------|------------------|----------|
+| **Standard** (separate cables) | 1 per monitor | Unique per monitor | 1 per HMONITOR | Most common setup |
+| **Clone/Mirror** | 1 shared | Shared | Multiple per HMONITOR | Presentation, duplication |
+| **MST Daisy Chain** | 1 per monitor | Unique per monitor | 1 per HMONITOR | Reduced cable clutter |
+| **USB-C/Thunderbolt Hub** | 1 per monitor | Unique per monitor | 1 per HMONITOR | Laptop docking |
+
+**Key Insight**: From PowerDisplay's perspective, MST daisy chain and standard multi-cable
+setups behave identically - each monitor appears as an independent display with unique
+identifiers. Only clone/mirror mode requires special handling due to shared HMONITOR/GDI names.
+
+##### Hardware ID Composition
+
+```mermaid
+flowchart TB
+ HardwareId["Hardware ID: DEL41B4"]
+
+ HardwareId --> PnpId["DEL
PnP Manufacturer ID
3 chars, EDID bytes 8-9"]
+ HardwareId --> ProductCode["41B4
Product Code
4 hex chars, EDID bytes 10-11"]
+
+ style HardwareId fill:#fff3e0
+ style PnpId fill:#c8e6c9
+ style ProductCode fill:#bbdefb
+```
+
+The **PnP Manufacturer ID** is a 3-character code assigned by UEFI Forum.
+Common laptop display manufacturers:
+
+| PnP ID | Manufacturer |
+|--------|--------------|
+| `BOE` | BOE Technology |
+| `LGD` | LG Display |
+| `AUO` | AU Optronics |
+| `CMN` | Chi Mei Innolux |
+| `SDC` | Samsung Display |
+| `SHP` | Sharp |
+| `LEN` | Lenovo |
+| `DEL` | Dell |
+
+##### WMI Instance Name Parsing
+
+```mermaid
+flowchart TB
+ InstanceName["WMI InstanceName:
DISPLAY\BOE0900\4#amp;10fd3ab1#amp;0#amp;UID265988_0"]
+
+ InstanceName --> Seg1["Segment 1: DISPLAY
Constant prefix"]
+ InstanceName --> Seg2["Segment 2: BOE0900
Hardware ID
Used for matching with QueryDisplayConfig"]
+ InstanceName --> Seg3["Segment 3: Device instance
4#amp;10fd3ab1#amp;0#amp;UID265988_0"]
+
+ style InstanceName fill:#fff3e0
+ style Seg1 fill:#e0e0e0
+ style Seg2 fill:#c8e6c9
+ style Seg3 fill:#e0e0e0
+```
+
+##### Monitor Number (Windows Display Settings)
+
+The `MonitorNumber` in PowerDisplay corresponds exactly to the number shown in:
+- Windows Settings → System → Display → "Identify" button
+- The number overlay that appears on each display
+
+This is derived from the **path index** in `QueryDisplayConfig`:
+- `paths[0]` → Monitor 1
+- `paths[1]` → Monitor 2
+- etc.
+
+#### Display Rotation and GDI Device Name
+
+The `ChangeDisplaySettingsEx` API requires the **GDI Device Name** to target a specific display:
+
+```cpp
+// Correct: Target specific display by GDI name
+ChangeDisplaySettingsEx("\\.\DISPLAY2", &devMode, NULL, 0, NULL);
+
+// Wrong: NULL affects primary display only
+ChangeDisplaySettingsEx(NULL, &devMode, NULL, 0, NULL);
+```
+
+PowerDisplay stores `GdiDeviceName` in each `Monitor` object specifically for rotation operations.
+
+#### Cross-Reference Summary
+
+| PowerDisplay Property | DDC/CI Source | WMI Source |
+|-----------------------|---------------|------------|
+| `Monitor.Id` | `"DDC_{HardwareId}_{MonitorNumber}"` | `"WMI_{HardwareId}_{MonitorNumber}"` |
+| `Monitor.Handle` | Physical Monitor Handle | N/A (uses InstanceName) |
+| `Monitor.InstanceName` | N/A | WMI InstanceName |
+| `Monitor.GdiDeviceName` | QueryDisplayConfig | QueryDisplayConfig |
+| `Monitor.MonitorNumber` | QueryDisplayConfig path index | QueryDisplayConfig (matched by HardwareId) |
+| `Monitor.Name` | EDID FriendlyName or Description | PnpIdHelper.GetBuiltInDisplayName() |
+
+---
+
+### Settings UI and PowerDisplay Interaction Architecture
+
+```mermaid
+flowchart LR
+ subgraph SettingsUI["Settings UI Process"]
+ direction TB
+ Page["PowerDisplayPage.xaml"]
+ VM["PowerDisplayViewModel"]
+ Page --> VM
+ end
+
+ subgraph Runner["Runner Process"]
+ direction TB
+ Exe["PowerToys.exe"]
+ Pipe["Named Pipe IPC"]
+ Module["PowerDisplayModuleInterface.dll"]
+ Pipe --> Exe --> Module
+ end
+
+ subgraph PDApp["PowerDisplay Process"]
+ direction TB
+ MainVM["MainViewModel"]
+ Events["Event Listeners
Refresh / Profile"]
+ Events --> MainVM
+ end
+
+ subgraph Storage["File System"]
+ direction TB
+ Settings[("settings.json")]
+ Profiles[("profiles.json")]
+ end
+
+ %% Main flow: Settings UI → Runner → PowerDisplay
+ VM -->|"IPC Message"| Pipe
+ Module -->|"SetEvent()"| Events
+
+ %% File access
+ VM <-.->|"Read/Write"| Settings
+ VM <-.->|"Read/Write"| Profiles
+ MainVM <-.->|"Read"| Settings
+ MainVM <-.->|"Read/Write"| Profiles
+
+ style SettingsUI fill:#e3f2fd
+ style Runner fill:#fff3e0
+ style PDApp fill:#e8f5e9
+ style Storage fill:#fffde7
+```
+
+**Data Models (in Settings.UI.Library):**
+
+| Model | Purpose |
+|-------|---------|
+| `PowerDisplaySettings` | Main settings container with properties |
+| `MonitorInfo` | Per-monitor settings displayed in Settings UI (includes feature visibility flags like `EnableColorTemperature`) |
+
+### Windows Events for IPC
+
+Event names use fixed GUID suffixes to ensure uniqueness (defined in `shared_constants.h`).
+
+| Constant | Direction | Purpose |
+|----------|-----------|---------|
+| `TOGGLE_POWER_DISPLAY_EVENT` | Runner → App | Toggle visibility |
+| `TERMINATE_POWER_DISPLAY_EVENT` | Runner → App | Terminate process |
+| `REFRESH_POWER_DISPLAY_MONITORS_EVENT` | Settings → App | Refresh monitor list |
+| `SETTINGS_UPDATED_POWER_DISPLAY_EVENT` | Settings → App | Notify settings changed (feature visibility, tray icon) |
+| `LightSwitchLightThemeEventName` | LightSwitch → App | Apply light mode profile |
+| `LightSwitchDarkThemeEventName` | LightSwitch → App | Apply dark mode profile |
+
+**Profile Application via Named Pipe IPC:**
+
+Profile application from Settings UI uses Named Pipe IPC (via Runner's `call_custom_action`) instead of
+Windows Events. When the user clicks "Apply" on a profile in Settings UI, the message is sent through
+the Runner to the Module Interface, which forwards it to PowerDisplay.exe via Named Pipe.
+
+**Event Name Format:** `Local\PowerToysPowerDisplay-{EventType}-{GUID}`
+
+Example: `Local\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c`
+
+---
+
+### LightSwitch Profile Integration Architecture
+
+```mermaid
+flowchart TB
+ subgraph LightSwitchModule["LightSwitch Module (C++)"]
+ StateManager["LightSwitchStateManager"]
+ ThemeEval["Theme Evaluation
(Time/System)"]
+ LightSwitchSettings["LightSwitchSettings"]
+ NotifyPD["NotifyPowerDisplay(isLight)"]
+ end
+
+ subgraph PowerDisplayModule["PowerDisplay Module (C#)"]
+ subgraph App["PowerDisplay App"]
+ EventWaiter["NativeEventWaiter
(Background Thread)"]
+ LightSwitchSvc["LightSwitchService
(Static Helper)"]
+ MainViewModel["MainViewModel"]
+ end
+
+ ProfileService["ProfileService"]
+ MonitorVMs["MonitorViewModels"]
+ Controllers["IMonitorController"]
+ end
+
+ subgraph WindowsEvents["Windows Events"]
+ LightEvent["Local\\PowerToys_LightSwitch_LightTheme"]
+ DarkEvent["Local\\PowerToys_LightSwitch_DarkTheme"]
+ end
+
+ subgraph FileSystem["File System"]
+ LSSettingsJson["LightSwitch/settings.json
{lightProfile, darkProfile}"]
+ PDProfilesJson["PowerDisplay/profiles.json
{profiles: [...]}"]
+ end
+
+ subgraph Hardware["Hardware"]
+ Monitors["Connected Monitors"]
+ end
+
+ %% LightSwitch flow
+ ThemeEval -->|"Time boundary
or manual"| StateManager
+ StateManager --> LightSwitchSettings
+ StateManager --> NotifyPD
+ NotifyPD -->|"isLight=true"| LightEvent
+ NotifyPD -->|"isLight=false"| DarkEvent
+
+ %% PowerDisplay flow - theme determined from event
+ LightEvent -->|"Event signaled"| EventWaiter
+ DarkEvent -->|"Event signaled"| EventWaiter
+ EventWaiter -->|"isLightMode"| LightSwitchSvc
+ LightSwitchSvc -->|"GetProfileForTheme()"| LSSettingsJson
+ LightSwitchSvc -->|"Profile name"| MainViewModel
+ MainViewModel -->|"LoadProfiles()"| ProfileService
+ ProfileService <--> PDProfilesJson
+ MainViewModel -->|"ApplyProfileAsync()"| MonitorVMs
+ MonitorVMs --> Controllers
+ Controllers --> Monitors
+
+ style LightSwitchModule fill:#ffccbc
+ style PowerDisplayModule fill:#c8e6c9
+ style App fill:#a5d6a7
+ style WindowsEvents fill:#e3f2fd
+ style FileSystem fill:#fffde7
+```
+
+### LightSwitch Settings JSON Structure
+
+```json
+{
+ "properties": {
+ "apply_monitor_settings": { "value": true },
+ "enable_light_mode_profile": { "value": true },
+ "light_mode_profile": { "value": "Productivity" },
+ "enable_dark_mode_profile": { "value": true },
+ "dark_mode_profile": { "value": "Night Mode" }
+ }
+}
+```
+
+---
+
+## Data Flow and Communication
+
+### Monitor Discovery Flow
+
+```mermaid
+flowchart TB
+ Start([Start Discovery])
+ Start --> MM["MonitorManager.DiscoverMonitorsAsync()"]
+
+ MM --> DDC["DdcCiController.DiscoverMonitorsAsync()"]
+ MM --> WMI["WmiController.DiscoverMonitorsAsync()"]
+
+ DDC --> Merge["Merge Results"]
+ WMI --> Merge
+
+ Merge --> Sort["Sort by MonitorNumber"]
+ Sort --> Update["UpdateMonitorList()"]
+ Update --> Check{"RestoreSettingsOnStartup?"}
+ Check -->|Yes| Restore["RestoreMonitorSettingsAsync()
(Set hardware values)"]
+ Check -->|No| Done
+ Restore --> Done([Discovery Complete])
+
+ style Start fill:#e8f5e9
+ style Done fill:#e8f5e9
+ style DDC fill:#e3f2fd
+ style WMI fill:#fff3e0
+ style Restore fill:#fff9c4
+```
+
+> **Note:** DDC/CI and WMI discovery run in parallel via `Task.WhenAll`.
+>
+> **Settings Restore:** When `RestoreSettingsOnStartup` is enabled, `RestoreMonitorSettingsAsync()` is called
+> after monitor discovery to restore saved brightness, contrast, color temperature, and volume values
+> to the hardware. The UI remains in "scanning" state until restore completes.
+
+#### DDC/CI Discovery (Three-Phase Approach)
+
+**Phase 1: Collect Candidates**
+
+```mermaid
+flowchart LR
+ QDC["QueryDisplayConfig"] --> Match["Match by GDI Name"]
+ Enum["EnumDisplayMonitors"] --> GetPhys["GetPhysicalMonitors"] --> Match
+ Match --> Candidates["CandidateMonitor List"]
+
+ style QDC fill:#e3f2fd
+ style Enum fill:#e3f2fd
+```
+
+**Phase 2: Fetch Capabilities (Parallel)**
+
+```mermaid
+flowchart LR
+ Candidates["CandidateMonitor List"] --> Fetch["Task.WhenAll:
FetchCapabilities
~4s per monitor via I2C"]
+ Fetch --> Results["DdcCiValidationResult Array"]
+
+ style Fetch fill:#fff3e0
+```
+
+**Phase 3: Create Monitors**
+
+```mermaid
+flowchart LR
+ Results["Validation Results"] --> Check{"IsValid?"}
+ Check -->|Yes| Create["Create Monitor"]
+ Create --> Init["Initialize VCP Values:
Brightness, ColorTemp, InputSource"]
+ Init --> Add["Add to List"]
+ Check -->|No| Skip([Skip])
+
+ style Create fill:#e8f5e9
+ style Init fill:#e8f5e9
+```
+
+#### WMI Discovery
+
+```mermaid
+flowchart LR
+ Query["Query WmiMonitorBrightness"] --> Extract["Extract HardwareId
from InstanceName"]
+ QDC["QueryDisplayConfig"] --> Match["Match by HardwareId"]
+ Extract --> Match
+ Match --> Name["Get Display Name
via PnpIdHelper"]
+ Name --> Create["Create Monitor
Brightness + WMI"]
+
+ style Query fill:#fff3e0
+ style Create fill:#fff3e0
+```
+
+#### Key Differences
+
+| Aspect | DDC/CI | WMI |
+|--------|--------|-----|
+| **Target** | External monitors | Internal laptop displays |
+| **Capabilities** | Full VCP support (brightness, contrast, volume, color temp, input) | Brightness only |
+| **Discovery** | Three-phase with parallel I2C fetching | Single WMI query |
+| **Initialization** | Reads current values for all supported VCP codes | Brightness from query result |
+| **Performance** | ~4s per monitor (I2C), parallelized | Fast (~100ms total) |
+
+---
+
+## Sequence Diagrams
+
+### Sequence: Modifying Color Temperature in Flyout UI
+
+Color temperature adjustment is now handled directly in the PowerDisplay Flyout UI,
+providing a more responsive user experience without requiring IPC round-trips to Settings UI.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Flyout as MainWindow (Flyout)
+ participant MonitorVM as MonitorViewModel
+ participant MonitorManager
+ participant Controller as DdcCiController
+ participant StateManager as MonitorStateManager
+ participant Monitor as Physical Monitor
+
+ User->>Flyout: Opens PowerDisplay flyout
(via hotkey or tray icon)
+
+ Note over Flyout: Color temperature switcher visible
(if enabled in Settings)
+
+ User->>Flyout: Selects color temperature preset
from dropdown (e.g., 6500K)
+
+ Flyout->>MonitorVM: ColorTemperatureListView_SelectionChanged
+ MonitorVM->>MonitorVM: SetColorTemperatureAsync(vcpValue)
+
+ MonitorVM->>MonitorManager: SetColorTemperatureAsync(monitor, vcpValue)
+
+ MonitorManager->>Controller: SetColorTemperatureAsync(monitor, vcpValue)
+ Controller->>Controller: SetVcpFeatureAsync(VcpCodeColorTemperature)
+ Controller->>Monitor: SetVCPFeature(0x14, vcpValue)
+ Monitor-->>Controller: OK
+
+ Controller-->>MonitorManager: MonitorOperationResult.Success
+ MonitorManager-->>MonitorVM: Success
+
+ MonitorVM->>MonitorVM: RefreshAvailableColorPresets()
+ Note over MonitorVM: Regenerate ColorTemperatureItem list
with updated IsSelected flags
+
+ MonitorVM->>StateManager: UpdateMonitorParameter("ColorTemperature", vcpValue)
+
+ Note over StateManager: Debounced save (2 seconds)
+ StateManager->>StateManager: Schedule file write
+
+ Note over StateManager: After 2s idle
+ StateManager->>StateManager: SaveToFile(monitor_state.json)
+
+ Note over MonitorVM: UI updates to show
selected preset with checkmark
+```
+
+**Color Temperature Selection UI:**
+
+The color temperature switcher displays a list of available presets (e.g., 5000K, 6500K, sRGB). Each preset
+shows a checkmark icon when selected. The `ColorTemperatureItem` class stores `IsSelected` state, which is
+updated by regenerating the entire `AvailableColorPresets` list after a successful color temperature change.
+This ensures the checkmark displays correctly for the newly selected preset.
+
+**Flyout Display Options:**
+
+The Flyout UI visibility is controlled by a combination of global settings and per-monitor settings:
+
+**Global Settings (in `PowerDisplayProperties`):**
+
+| Setting | Default | Description |
+|---------|---------|-------------|
+| `ShowProfileSwitcher` | `true` | Show profile switcher (also requires profiles to exist) |
+| `ShowIdentifyMonitorsButton` | `true` | Show "Identify Monitors" button |
+
+**Per-Monitor Settings (in `MonitorInfo`):**
+
+| Setting | Default | Description |
+|---------|---------|-------------|
+| `EnableContrast` | `true` (if supported) | Show contrast slider for this monitor |
+| `EnableVolume` | `true` (if supported) | Show volume slider for this monitor |
+| `EnableInputSource` | `true` (if supported) | Show input source selector for this monitor |
+| `EnableRotation` | `true` | Show rotation control for this monitor |
+| `EnableColorTemperature` | `true` (if supported) | Show color temperature switcher for this monitor |
+| `IsHidden` | `false` | Hide this monitor from the flyout entirely |
+
+Users can configure per-monitor visibility in Settings UI under the "Monitors" section. Each monitor
+shows checkboxes for the features it supports, allowing fine-grained control over the flyout UI.
+
+**Color Temperature Warning Dialog:**
+
+When enabling `EnableColorTemperature` for a monitor in Settings UI, a warning dialog is displayed to inform
+users about potential risks. Color temperature changes can cause unpredictable results on some monitors,
+including incorrect colors, display malfunction, or settings that cannot be reverted. The dialog requires
+explicit confirmation before enabling the feature.
+
+Implementation notes:
+- The warning dialog only appears when the user explicitly checks the checkbox (not during initial page load)
+- A `_isPageLoaded` flag prevents the dialog from appearing during data binding
+- If the user cancels the dialog, the checkbox is reverted to unchecked state
+
+---
+
+### Sequence: Creating and Saving a Profile
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant SettingsPage as PowerDisplayPage
+ participant ViewModel as PowerDisplayViewModel
+ participant ProfileDialog as ProfileEditorDialog
+ participant ProfileService
+ participant FileSystem as profiles.json
+
+ User->>SettingsPage: Clicks "Add Profile" button
+ SettingsPage->>ViewModel: ShowProfileEditor()
+
+ ViewModel->>ProfileDialog: Show(monitors, existingProfiles)
+ ProfileDialog->>ProfileDialog: Display monitor selection UI
+
+ User->>ProfileDialog: Enters profile name
+ User->>ProfileDialog: Selects monitors to include
+ User->>ProfileDialog: Configures settings per monitor
(brightness, contrast, etc.)
+ User->>ProfileDialog: Clicks "Save"
+
+ ProfileDialog->>ProfileDialog: Validate inputs
+ Note over ProfileDialog: Check name unique,
at least one monitor selected
+
+ ProfileDialog-->>ViewModel: ResultProfile (PowerDisplayProfile)
+
+ ViewModel->>ProfileService: AddOrUpdateProfile(profile)
+
+ ProfileService->>ProfileService: lock(_lock)
+ ProfileService->>FileSystem: Read profiles.json
+ FileSystem-->>ProfileService: Existing profiles
+ ProfileService->>ProfileService: Add/update profile in collection
+ ProfileService->>ProfileService: Set LastUpdated = DateTime.Now
+ ProfileService->>FileSystem: Write profiles.json
+ FileSystem-->>ProfileService: Success
+ ProfileService-->>ViewModel: true
+
+ ViewModel->>ViewModel: RefreshProfilesList()
+ ViewModel-->>SettingsPage: PropertyChanged(Profiles)
+ SettingsPage->>SettingsPage: Update UI with new profile
+```
+
+---
+
+### Sequence: Applying Profile via LightSwitch Theme Change
+
+```mermaid
+sequenceDiagram
+ participant System as Windows System
+ participant LightSwitch as LightSwitchStateManager (C++)
+ participant WinEvent as Windows Events
+ participant EventWaiter as NativeEventWaiter
+ participant LSSvc as LightSwitchService
+ participant MainVM as MainViewModel
+ participant ProfileService
+ participant MonitorVM as MonitorViewModel
+ participant Controller as IMonitorController
+ participant Monitor as Physical Monitor
+
+ Note over System: Time reaches threshold
or user changes theme
+ System->>LightSwitch: Theme change detected
+
+ LightSwitch->>LightSwitch: EvaluateAndApplyIfNeeded()
+ LightSwitch->>LightSwitch: ApplyTheme(isLight)
+
+ LightSwitch->>LightSwitch: NotifyPowerDisplay(isLight)
+ Note over LightSwitch: Check if profile enabled
+
+ alt isLight == true
+ LightSwitch->>WinEvent: SetEvent("Local\\PowerToys_LightSwitch_LightTheme")
+ else isLight == false
+ LightSwitch->>WinEvent: SetEvent("Local\\PowerToys_LightSwitch_DarkTheme")
+ end
+
+ Note over EventWaiter: Background thread waiting
on both Light and Dark events
+ EventWaiter->>WinEvent: WaitAny([lightEvent, darkEvent]) returns index
+
+ Note over EventWaiter: Theme determined from event:
index 0 = Light, index 1 = Dark
+ EventWaiter->>LSSvc: GetProfileForTheme(isLightMode)
+ LSSvc->>LSSvc: Read LightSwitch/settings.json
+ LSSvc-->>EventWaiter: profileName (or null)
+
+ EventWaiter->>MainVM: Dispatch to UI thread with profileName
+
+ MainVM->>ProfileService: LoadProfiles()
+ ProfileService-->>MainVM: PowerDisplayProfiles
+
+ MainVM->>MainVM: Find profile by name
+ MainVM->>MainVM: ApplyProfileAsync(profile.MonitorSettings)
+
+ loop For each ProfileMonitorSetting
+ MainVM->>MainVM: Find MonitorViewModel by InternalName
+
+ alt Brightness specified
+ MainVM->>MonitorVM: SetBrightnessAsync(value, immediate=true)
+ MonitorVM->>Controller: SetBrightnessAsync(monitor, value)
+ Controller->>Monitor: DDC/CI or WMI call
+ Monitor-->>Controller: Success
+ end
+
+ alt Contrast specified
+ MainVM->>MonitorVM: SetContrastAsync(value, immediate=true)
+ MonitorVM->>Controller: SetContrastAsync(monitor, value)
+ Controller->>Monitor: SetVCPFeature(0x12, value)
+ end
+
+ alt Volume specified
+ MainVM->>MonitorVM: SetVolumeAsync(value, immediate=true)
+ MonitorVM->>Controller: SetVolumeAsync(monitor, value)
+ Controller->>Monitor: SetVCPFeature(0x62, value)
+ end
+
+ alt ColorTemperature specified
+ MainVM->>MonitorVM: SetColorTemperatureAsync(vcpValue)
+ MonitorVM->>Controller: SetColorTemperatureAsync(monitor, vcpValue)
+ Controller->>Monitor: SetVCPFeature(0x14, vcpValue)
+ end
+
+ alt Orientation specified
+ MainVM->>MonitorVM: SetOrientationAsync(orientation)
+ MonitorVM->>Controller: SetRotationAsync(monitor, orientation)
+ Controller->>Monitor: ChangeDisplaySettingsEx
+ end
+ end
+
+ Note over MainVM: await Task.WhenAll(updateTasks)
+ MainVM->>MainVM: Log profile application complete
+```
+
+---
+
+### Sequence: UI Slider Adjustment (Brightness)
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Slider as Brightness Slider
+ participant MonitorVM as MonitorViewModel
+ participant Debouncer as SimpleDebouncer
+ participant MonitorManager
+ participant Controller as DdcCiController
+ participant StateManager as MonitorStateManager
+ participant Monitor as Physical Monitor
+
+ User->>Slider: Drags slider (continuous)
+
+ loop During drag (multiple events)
+ Slider->>MonitorVM: CurrentBrightness = value
+ MonitorVM->>MonitorVM: SetBrightnessAsync(value, immediate=false)
+ MonitorVM->>Debouncer: Debounce(300ms)
+ Note over Debouncer: Resets timer on each call
+ end
+
+ User->>Slider: Releases slider
+
+ Note over Debouncer: 300ms elapsed, no new input
+ Debouncer->>MonitorVM: Execute debounced action
+
+ MonitorVM->>MonitorVM: ApplyBrightnessToHardwareAsync()
+ MonitorVM->>MonitorManager: SetBrightnessAsync(monitor, finalValue)
+
+ MonitorManager->>Controller: SetBrightnessAsync(monitor, value)
+
+ Controller->>Controller: SetVcpFeatureAsync(VcpCodeBrightness)
+ Controller->>Monitor: SetVCPFeature(0x10, value)
+ Monitor-->>Controller: OK
+
+ Controller-->>MonitorManager: MonitorOperationResult
+ MonitorManager-->>MonitorVM: Success/Failure
+
+ MonitorVM->>StateManager: UpdateMonitorParameter("Brightness", value)
+
+ Note over StateManager: Debounced save (2 seconds)
+ StateManager->>StateManager: Schedule file write
+
+ Note over StateManager: After 2s idle
+ StateManager->>StateManager: SaveToFile(monitor_state.json)
+```
+
+---
+
+### Sequence: Module Enable/Disable Lifecycle
+
+```mermaid
+sequenceDiagram
+ participant Runner as PowerToys Runner
+ participant ModuleInterface as PowerDisplayModule (C++)
+ participant PowerDisplayApp as PowerDisplay.exe
+ participant MonitorManager
+ participant StateManager as MonitorStateManager
+ participant EventHandles as Windows Events
+
+ Note over Runner: User enables PowerDisplay
+ Runner->>ModuleInterface: enable()
+
+ ModuleInterface->>ModuleInterface: m_enabled = true
+ ModuleInterface->>ModuleInterface: Trace::EnablePowerDisplay(true)
+
+ ModuleInterface->>ModuleInterface: is_process_running()
+ alt Process not running
+ ModuleInterface->>PowerDisplayApp: ShellExecuteExW("PowerToys.PowerDisplay.exe", pid)
+ PowerDisplayApp->>PowerDisplayApp: Initialize WinUI 3 App
+ PowerDisplayApp->>PowerDisplayApp: RegisterSingletonInstance()
+ PowerDisplayApp->>MonitorManager: DiscoverMonitorsAsync()
+
+ alt RestoreSettingsOnStartup enabled
+ PowerDisplayApp->>StateManager: GetMonitorParameters(monitorId)
+ StateManager-->>PowerDisplayApp: Saved brightness, contrast, etc.
+ PowerDisplayApp->>MonitorManager: SetBrightnessAsync(savedValue)
+ PowerDisplayApp->>MonitorManager: SetContrastAsync(savedValue)
+ Note over PowerDisplayApp: Restore all saved settings to hardware
+ end
+
+ PowerDisplayApp->>PowerDisplayApp: Start event listeners
+ PowerDisplayApp->>EventHandles: SetEvent("Ready")
+ end
+
+ ModuleInterface->>ModuleInterface: m_hProcess = sei.hProcess
+
+ Note over Runner: User presses hotkey
+ Runner->>ModuleInterface: on_hotkey()
+ ModuleInterface->>EventHandles: SetEvent(ToggleEvent)
+ EventHandles->>PowerDisplayApp: Toggle visibility
+
+ Note over Runner: User disables PowerDisplay
+ Runner->>ModuleInterface: disable()
+
+ ModuleInterface->>EventHandles: ResetEvent(InvokeEvent)
+ ModuleInterface->>EventHandles: SetEvent(TerminateEvent)
+
+ PowerDisplayApp->>PowerDisplayApp: Receive terminate signal
+ PowerDisplayApp->>MonitorManager: Dispose()
+ PowerDisplayApp->>PowerDisplayApp: Application.Exit()
+
+ ModuleInterface->>ModuleInterface: CloseHandle(m_hProcess)
+ ModuleInterface->>ModuleInterface: m_enabled = false
+ ModuleInterface->>ModuleInterface: Trace::EnablePowerDisplay(false)
+```
+
+---
+
+## Future Considerations
+
+### Already Implemented
+
+- **Monitor Hot-Plug**: `DisplayChangeWatcher` uses WinRT DeviceWatcher + DisplayMonitor API with 1-second debouncing
+- **Display Rotation**: `DisplayRotationService` uses Windows ChangeDisplaySettingsEx API
+- **LightSwitch Integration**: Automatic profile application on theme changes via `LightSwitchService`
+- **Monitor Identification**: Overlay windows showing monitor numbers via `IdentifyWindow`
+- **Mirror Mode Support**: Correct orientation sync for multiple monitors sharing the same GDI device name
+
+### Potential Future Enhancements
+
+1. **Advanced Color Management**: Integration with Windows Color Management APIs (HDR, ICC profiles)
+2. **PIP/PBP Control**: Picture-in-Picture and Picture-by-Picture configuration (VcpCapabilities already parses window capabilities)
+3. **Power State Control**: Monitor power on/off via VCP code 0xD6
+
+---
+
+## References
+
+- [VESA DDC/CI Standard](https://vesa.org/vesa-standards/)
+- [MCCS (Monitor Control Command Set) Specification](https://vesa.org/vesa-standards/)
+- [Microsoft High-Level Monitor Configuration API](https://learn.microsoft.com/en-us/windows/win32/monitor/high-level-monitor-configuration-api)
+- [WMI Reference](https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-reference)
+- [WmiMonitorBrightness Class](https://learn.microsoft.com/en-us/windows/win32/wmicoreprov/wmimonitorbrightness)
+- [PowerToys Architecture Documentation](../../core/architecture.md)
diff --git a/doc/devdocs/modules/powerdisplay/mccsParserDesign.md b/doc/devdocs/modules/powerdisplay/mccsParserDesign.md
new file mode 100644
index 0000000000..128407308c
--- /dev/null
+++ b/doc/devdocs/modules/powerdisplay/mccsParserDesign.md
@@ -0,0 +1,223 @@
+# 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
+├── ApplySegment() → void
+│ ├── ParseHexList() → List
+│ ├── ParseVcpEntries() → Dictionary
+│ └── 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` 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 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)
diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
index 40c3d5b0e8..43919ecaf1 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
- std::array processesToTerminate = {
+ std::array processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe",
@@ -1565,6 +1565,7 @@ 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",
diff --git a/installer/PowerToysSetupVNext/PowerDisplay.wxs b/installer/PowerToysSetupVNext/PowerDisplay.wxs
new file mode 100644
index 0000000000..5cfe23661c
--- /dev/null
+++ b/installer/PowerToysSetupVNext/PowerDisplay.wxs
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
index a7a9744e87..4000503edf 100644
--- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
+++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
@@ -47,6 +47,7 @@ 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
@@ -123,6 +124,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
+
diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs
index a5615870f9..5256af42fd 100644
--- a/installer/PowerToysSetupVNext/Product.wxs
+++ b/installer/PowerToysSetupVNext/Product.wxs
@@ -53,6 +53,7 @@
+
diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
index 6724d95170..2ada2b17d1 100644
--- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
+++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
@@ -176,6 +176,10 @@ 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
diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs
index 5233c0d668..fedf5480e3 100644
--- a/src/common/Common.UI/SettingsDeepLink.cs
+++ b/src/common/Common.UI/SettingsDeepLink.cs
@@ -45,6 +45,7 @@ namespace Common.UI
NewPlus,
CmdPal,
ZoomIt,
+ PowerDisplay,
}
private static string SettingsWindowNameToString(SettingsWindow value)
@@ -115,6 +116,8 @@ namespace Common.UI
return "CmdPal";
case SettingsWindow.ZoomIt:
return "ZoomIt";
+ case SettingsWindow.PowerDisplay:
+ return "PowerDisplay";
default:
{
return string.Empty;
diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp
index 2b256cd926..1132df9599 100644
--- a/src/common/GPOWrapper/GPOWrapper.cpp
+++ b/src/common/GPOWrapper/GPOWrapper.cpp
@@ -32,6 +32,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast(powertoys_gpo::getConfiguredLightSwitchEnabledValue());
}
+ GpoRuleConfigured GPOWrapper::GetConfiguredPowerDisplayEnabledValue()
+ {
+ return static_cast(powertoys_gpo::getConfiguredPowerDisplayEnabledValue());
+ }
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
{
return static_cast(powertoys_gpo::getConfiguredFancyZonesEnabledValue());
diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h
index e57cccccd9..aceb3bf756 100644
--- a/src/common/GPOWrapper/GPOWrapper.h
+++ b/src/common/GPOWrapper/GPOWrapper.h
@@ -14,6 +14,7 @@ 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();
diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl
index 06d035aa35..58c35cd977 100644
--- a/src/common/GPOWrapper/GPOWrapper.idl
+++ b/src/common/GPOWrapper/GPOWrapper.idl
@@ -18,6 +18,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
+ static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs
index 8461b4a6d8..548276f725 100644
--- a/src/common/ManagedCommon/ModuleType.cs
+++ b/src/common/ManagedCommon/ModuleType.cs
@@ -30,6 +30,7 @@ namespace ManagedCommon
PowerRename,
PowerLauncher,
PowerAccent,
+ PowerDisplay,
RegistryPreview,
MeasureTool,
ShortcutGuide,
diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp
index fef43de566..67b4da51f2 100644
--- a/src/common/interop/Constants.cpp
+++ b/src/common/interop/Constants.cpp
@@ -251,4 +251,40 @@ 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::PowerDisplaySendSettingsTelemetryEvent()
+ {
+ return CommonSharedConstants::POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT;
+ }
+ hstring Constants::HotkeyUpdatedPowerDisplayEvent()
+ {
+ return CommonSharedConstants::HOTKEY_UPDATED_POWER_DISPLAY_EVENT;
+ }
+ hstring Constants::PowerDisplayToggleMessage()
+ {
+ return CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE;
+ }
+ hstring Constants::PowerDisplayApplyProfileMessage()
+ {
+ return CommonSharedConstants::POWER_DISPLAY_APPLY_PROFILE_MESSAGE;
+ }
+ hstring Constants::PowerDisplayTerminateAppMessage()
+ {
+ return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE;
+ }
}
diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h
index cdd883cc41..faa2a97379 100644
--- a/src/common/interop/Constants.h
+++ b/src/common/interop/Constants.h
@@ -66,6 +66,15 @@ 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 PowerDisplaySendSettingsTelemetryEvent();
+ static hstring HotkeyUpdatedPowerDisplayEvent();
+ static hstring PowerDisplayToggleMessage();
+ static hstring PowerDisplayApplyProfileMessage();
+ static hstring PowerDisplayTerminateAppMessage();
};
}
diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl
index abd642b197..042d790699 100644
--- a/src/common/interop/Constants.idl
+++ b/src/common/interop/Constants.idl
@@ -63,6 +63,15 @@ 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 PowerDisplaySendSettingsTelemetryEvent();
+ static String HotkeyUpdatedPowerDisplayEvent();
+ static String PowerDisplayToggleMessage();
+ static String PowerDisplayApplyProfileMessage();
+ static String PowerDisplayTerminateAppMessage();
}
}
}
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index 3dad776cf6..079f53c85c 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -153,6 +153,23 @@ 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 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";
+
+ // IPC Messages used in PowerDisplay (Named Pipe communication)
+ const wchar_t POWER_DISPLAY_TOGGLE_MESSAGE[] = L"Toggle";
+ const wchar_t POWER_DISPLAY_APPLY_PROFILE_MESSAGE[] = L"ApplyProfile";
+ const wchar_t POWER_DISPLAY_TERMINATE_APP_MESSAGE[] = L"TerminateApp";
+
+ // 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";
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index 881633e05e..6f0592ea53 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -83,6 +83,7 @@ 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();
diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h
index ab71d09d0b..0b2611b076 100644
--- a/src/common/utils/gpo.h
+++ b/src/common/utils/gpo.h
@@ -32,6 +32,7 @@ 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";
@@ -310,6 +311,11 @@ 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);
diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx
index eb8eb92b93..ddef3d95eb 100644
--- a/src/gpo/assets/PowerToys.admx
+++ b/src/gpo/assets/PowerToys.admx
@@ -149,6 +149,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml
index fe0611022a..ccd38d9934 100644
--- a/src/gpo/assets/en-US/PowerToys.adml
+++ b/src/gpo/assets/en-US/PowerToys.adml
@@ -248,6 +248,7 @@ If you don't configure this policy, the user will be able to control the setting
CmdPal: Configure enabled state
Crop And Lock: Configure enabled state
Light Switch: Configure enabled state
+ PowerDisplay: Configure enabled state
Environment Variables: Configure enabled state
FancyZones: Configure enabled state
File Locksmith: Configure enabled state
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
index 488142b95b..15e9f7c915 100644
--- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
@@ -248,6 +248,46 @@ void LightSwitchSettings::LoadSettings()
}
}
+ // 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;
+ }
+ }
+
// For ChangeSystem/ChangeApps changes, log telemetry
if (themeTargetChanged)
{
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h
index 1d1c7953fe..4fd9777c5e 100644
--- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h
@@ -67,6 +67,11 @@ struct LightSwitchConfig
bool changeSystem = false;
bool changeApps = false;
+
+ bool enableDarkModeProfile = false;
+ bool enableLightModeProfile = false;
+ std::wstring darkModeProfile = L"";
+ std::wstring lightModeProfile = L"";
};
class LightSwitchSettings
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp
index cc4f959881..28bcca6512 100644
--- a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp
@@ -4,6 +4,7 @@
#include
#include "ThemeScheduler.h"
#include
+#include
void ApplyTheme(bool shouldBeLight);
@@ -37,7 +38,7 @@ void LightSwitchStateManager::OnTick()
}
}
-// Called when manual override is triggered
+// Called when manual override is triggered (via hotkey)
void LightSwitchStateManager::OnManualOverride()
{
std::lock_guard lock(_stateMutex);
@@ -45,15 +46,19 @@ 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();
@@ -268,7 +273,61 @@ 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");
+ }
+}
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h
index 65d6f7ada7..b6c001fc64 100644
--- a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h
@@ -48,4 +48,7 @@ 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);
};
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/MccsCapabilitiesParserTests.cs b/src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/MccsCapabilitiesParserTests.cs
new file mode 100644
index 0000000000..55f5e574ff
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/MccsCapabilitiesParserTests.cs
@@ -0,0 +1,737 @@
+// 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;
+
+///
+/// Unit tests for MccsCapabilitiesParser class.
+///
+[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);
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj b/src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj
new file mode 100644
index 0000000000..fb7e2474db
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ false
+ true
+ PowerDisplay.UnitTests
+ x64;ARM64
+ false
+ false
+ $(SolutionDir)$(Platform)\$(Configuration)\tests\PowerDisplay.Lib.UnitTests\
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime
+
+
+
+ runtime
+
+
+
+
+
+
+
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs
new file mode 100644
index 0000000000..e268045624
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs
@@ -0,0 +1,725 @@
+// 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
+{
+ ///
+ /// DDC/CI monitor controller for controlling external monitors
+ ///
+ public partial class DdcCiController : IMonitorController, IDisposable
+ {
+ ///
+ /// Represents a candidate monitor discovered during Phase 1 of monitor enumeration.
+ ///
+ /// Physical monitor handle for DDC/CI communication
+ /// Native physical monitor structure with description
+ /// Display info from QueryDisplayConfig (EdidId, FriendlyName, MonitorNumber)
+ private readonly record struct CandidateMonitor(
+ IntPtr Handle,
+ PHYSICAL_MONITOR PhysicalMonitor,
+ MonitorDisplayInfo MonitorInfo);
+
+ ///
+ /// Delay between retry attempts for DDC/CI operations (in milliseconds)
+ ///
+ private const int RetryDelayMs = 100;
+
+ ///
+ /// Retry pipeline for getting capabilities string length (3 retries).
+ ///
+ private static readonly ResiliencePipeline CapabilitiesLengthRetryPipeline =
+ new ResiliencePipelineBuilder()
+ .AddRetry(new RetryStrategyOptions
+ {
+ MaxRetryAttempts = 2, // 2 retries = 3 total attempts
+ Delay = TimeSpan.FromMilliseconds(RetryDelayMs),
+ ShouldHandle = new PredicateBuilder().HandleResult(len => len == 0),
+ OnRetry = static args =>
+ {
+ Logger.LogWarning($"[Retry] GetCapabilitiesStringLength returned invalid result on attempt {args.AttemptNumber + 1}, retrying...");
+ return default;
+ },
+ })
+ .Build();
+
+ ///
+ /// Retry pipeline for getting capabilities string (5 retries).
+ ///
+ private static readonly ResiliencePipeline CapabilitiesStringRetryPipeline =
+ new ResiliencePipelineBuilder()
+ .AddRetry(new RetryStrategyOptions
+ {
+ MaxRetryAttempts = 4, // 4 retries = 5 total attempts
+ Delay = TimeSpan.FromMilliseconds(RetryDelayMs),
+ ShouldHandle = new PredicateBuilder().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";
+
+ ///
+ /// Get monitor brightness using VCP code 0x10
+ ///
+ public async Task GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(monitor);
+ return await GetVcpFeatureAsync(monitor, VcpCodeBrightness, cancellationToken);
+ }
+
+ ///
+ /// Set monitor brightness using VCP code 0x10
+ ///
+ public Task SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
+ => SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeBrightness, brightness, cancellationToken);
+
+ ///
+ /// Set monitor contrast
+ ///
+ public Task SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
+ => SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, contrast, cancellationToken);
+
+ ///
+ /// Set monitor volume
+ ///
+ public Task SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
+ => SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, cancellationToken);
+
+ ///
+ /// 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
+ ///
+ public async Task GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(monitor);
+ return await GetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, cancellationToken);
+ }
+
+ ///
+ /// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
+ ///
+ public Task SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
+ => SetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, colorTemperature, cancellationToken);
+
+ ///
+ /// Get current input source using VCP code 0x60
+ /// Returns the raw VCP value (e.g., 0x11 for HDMI-1)
+ ///
+ public async Task GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(monitor);
+ return await GetVcpFeatureAsync(monitor, VcpCodeInputSource, cancellationToken);
+ }
+
+ ///
+ /// Set input source using VCP code 0x60
+ ///
+ public Task SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default)
+ => SetVcpFeatureAsync(monitor, VcpCodeInputSource, inputSource, cancellationToken);
+
+ ///
+ /// Set power state using VCP code 0xD6 (Power Mode).
+ /// Values: 0x01=On, 0x02=Standby, 0x03=Suspend, 0x04=Off(DPM), 0x05=Off(Hard).
+ /// Note: Setting any value other than 0x01 (On) will turn off the display.
+ ///
+ public Task SetPowerStateAsync(Monitor monitor, int powerState, CancellationToken cancellationToken = default)
+ => SetVcpFeatureAsync(monitor, VcpCodePowerMode, powerState, cancellationToken);
+
+ ///
+ /// Get current power state using VCP code 0xD6 (Power Mode).
+ /// Returns the raw VCP value (0x01=On, 0x02=Standby, etc.)
+ ///
+ public async Task GetPowerStateAsync(Monitor monitor, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(monitor);
+ return await GetVcpFeatureAsync(monitor, VcpCodePowerMode, cancellationToken);
+ }
+
+ ///
+ /// Get monitor capabilities string with retry logic.
+ /// Uses cached CapabilitiesRaw if available to avoid slow I2C operations.
+ ///
+ public async Task 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);
+ }
+
+ ///
+ /// Try to get capabilities string from monitor handle.
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 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
+ ///
+ public async Task> 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();
+ }
+
+ var candidateMonitors = await CollectCandidateMonitorsAsync(
+ monitorHandles, allMonitorDisplayInfo, cancellationToken);
+
+ if (candidateMonitors.Count == 0)
+ {
+ return Enumerable.Empty();
+ }
+
+ // 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();
+ }
+ }
+
+ ///
+ /// Enumerate all logical monitor handles using Win32 API.
+ ///
+ private List EnumerateMonitorHandles()
+ {
+ var handles = new List();
+
+ 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;
+ }
+
+ ///
+ /// Get GDI device name for a monitor handle (e.g., "\\.\DISPLAY1").
+ ///
+ private unsafe string? GetGdiDeviceName(IntPtr hMonitor)
+ {
+ var monitorInfo = new MONITORINFOEX { CbSize = (uint)sizeof(MONITORINFOEX) };
+ if (GetMonitorInfo(hMonitor, &monitorInfo))
+ {
+ return monitorInfo.GetDeviceName();
+ }
+
+ return null;
+ }
+
+ ///
+ /// 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.
+ ///
+ private async Task> CollectCandidateMonitorsAsync(
+ List monitorHandles,
+ Dictionary allMonitorDisplayInfo,
+ CancellationToken cancellationToken)
+ {
+ var candidates = new List();
+
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ private async Task<(CandidateMonitor Candidate, DdcCiValidationResult Result)[]> FetchCapabilitiesInParallelAsync(
+ List 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;
+ }
+
+ ///
+ /// Phase 3: Create Monitor objects for valid DDC/CI monitors.
+ /// A monitor is valid if it has capabilities with brightness support.
+ ///
+ private List CreateValidMonitors(
+ (CandidateMonitor Candidate, DdcCiValidationResult Result)[] fetchResults)
+ {
+ var monitors = new List();
+ var newHandleMap = new Dictionary();
+
+ 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 power state if supported
+ if (monitor.SupportsPowerState)
+ {
+ InitializePowerState(monitor, candidate.Handle);
+ }
+
+ // Initialize contrast if supported
+ if (monitor.SupportsContrast)
+ {
+ InitializeContrast(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;
+ }
+
+ ///
+ /// Initialize input source value for a monitor using VCP 0x60.
+ ///
+ private static void InitializeInputSource(Monitor monitor, IntPtr handle)
+ {
+ if (TryGetVcpFeature(handle, VcpCodeInputSource, monitor.Id, out uint current, out uint _))
+ {
+ monitor.CurrentInputSource = (int)current;
+ }
+ }
+
+ ///
+ /// Initialize color temperature value for a monitor using VCP 0x14.
+ ///
+ private static void InitializeColorTemperature(Monitor monitor, IntPtr handle)
+ {
+ if (TryGetVcpFeature(handle, VcpCodeSelectColorPreset, monitor.Id, out uint current, out uint _))
+ {
+ monitor.CurrentColorTemperature = (int)current;
+ }
+ }
+
+ ///
+ /// Initialize power state value for a monitor using VCP 0xD6.
+ ///
+ private static void InitializePowerState(Monitor monitor, IntPtr handle)
+ {
+ if (TryGetVcpFeature(handle, VcpCodePowerMode, monitor.Id, out uint current, out uint _))
+ {
+ monitor.CurrentPowerState = (int)current;
+ }
+ }
+
+ ///
+ /// Initialize brightness value for a monitor using VCP 0x10.
+ ///
+ private static void InitializeBrightness(Monitor monitor, IntPtr handle)
+ {
+ if (TryGetVcpFeature(handle, VcpCodeBrightness, monitor.Id, out uint current, out uint max))
+ {
+ var brightnessInfo = new VcpFeatureValue((int)current, 0, (int)max);
+ monitor.CurrentBrightness = brightnessInfo.ToPercentage();
+ }
+ }
+
+ ///
+ /// Initialize contrast value for a monitor using VCP 0x12.
+ ///
+ private static void InitializeContrast(Monitor monitor, IntPtr handle)
+ {
+ if (TryGetVcpFeature(handle, VcpCodeContrast, monitor.Id, out uint current, out uint max))
+ {
+ var contrastInfo = new VcpFeatureValue((int)current, 0, (int)max);
+ monitor.CurrentContrast = contrastInfo.ToPercentage();
+ }
+ }
+
+ ///
+ /// Wrapper for GetVCPFeatureAndVCPFeatureReply that logs errors on failure.
+ ///
+ /// Physical monitor handle
+ /// VCP code to read
+ /// Monitor ID for logging (optional)
+ /// Output: current value
+ /// Output: maximum value
+ /// True if successful, false otherwise
+ private static bool TryGetVcpFeature(IntPtr handle, byte vcpCode, string? monitorId, out uint currentValue, out uint maxValue)
+ {
+ if (GetVCPFeatureAndVCPFeatureReply(handle, vcpCode, IntPtr.Zero, out currentValue, out maxValue))
+ {
+ return true;
+ }
+
+ var lastError = GetLastError();
+ var monitorPrefix = string.IsNullOrEmpty(monitorId) ? string.Empty : $"[{monitorId}] ";
+ Logger.LogError($"{monitorPrefix}Failed to read VCP 0x{vcpCode:X2}, error code: {lastError}");
+ return false;
+ }
+
+ ///
+ /// Update monitor capability flags based on parsed VCP capabilities.
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Handle to the monitor
+ /// Cancellation token
+ /// Array of valid physical monitors, or null if failed after retries
+ private async Task 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;
+ }
+
+ ///
+ /// Generic method to get VCP feature value.
+ ///
+ /// Monitor to query
+ /// VCP code to read
+ /// Cancellation token
+ private async Task GetVcpFeatureAsync(
+ Monitor monitor,
+ byte vcpCode,
+ CancellationToken cancellationToken = default)
+ {
+ return await Task.Run(
+ () =>
+ {
+ if (monitor.Handle == IntPtr.Zero)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+
+ if (TryGetVcpFeature(monitor.Handle, vcpCode, monitor.Id, out uint current, out uint max))
+ {
+ return new VcpFeatureValue((int)current, 0, (int)max);
+ }
+
+ return VcpFeatureValue.Invalid;
+ },
+ cancellationToken);
+ }
+
+ ///
+ /// Generic method to set VCP feature value directly.
+ ///
+ private Task 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;
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs
new file mode 100644
index 0000000000..8b5dbf49fb
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiNative.cs
@@ -0,0 +1,277 @@
+// 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
+{
+ ///
+ /// DDC/CI native API wrapper
+ ///
+ public static class DdcCiNative
+ {
+ ///
+ /// 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.
+ ///
+ /// Physical monitor handle
+ /// Validation result with capabilities data (or failure status)
+ 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;
+ }
+ }
+
+ ///
+ /// Try to get capabilities string from a physical monitor handle.
+ ///
+ /// Physical monitor handle
+ /// Capabilities string, or null if failed
+ 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;
+ }
+ }
+
+ ///
+ /// Gets GDI device name for a source (e.g., "\\.\DISPLAY1").
+ ///
+ /// Adapter ID
+ /// Source ID
+ /// GDI device name, or null if retrieval fails
+ 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;
+ }
+
+ ///
+ /// Gets friendly name, EDID ID, and device path for a monitor target.
+ ///
+ /// Adapter ID
+ /// Target ID
+ /// Tuple of (friendlyName, edidId, devicePath), any may be null if retrieval fails
+ private static unsafe (string? FriendlyName, string? EdidId, 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 EDID ID from EDID data
+ var manufacturerId = deviceName.EdidManufactureId;
+ var manufactureCode = ConvertManufactureIdToString(manufacturerId);
+ var productCode = deviceName.EdidProductCodeId.ToString("X4", System.Globalization.CultureInfo.InvariantCulture);
+ var edidId = $"{manufactureCode}{productCode}";
+
+ return (friendlyName, edidId, devicePath);
+ }
+ }
+ catch (Exception ex) when (ex is not OutOfMemoryException)
+ {
+ }
+
+ return (null, null, null);
+ }
+
+ ///
+ /// Converts manufacturer ID to 3-character manufacturer code
+ ///
+ /// Manufacturer ID
+ /// 3-character manufacturer code
+ 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}";
+ }
+
+ ///
+ /// Gets complete information for all monitors, keyed by GDI device name (e.g., "\\.\DISPLAY1").
+ /// This allows reliable matching with GetMonitorInfo results.
+ ///
+ /// Dictionary keyed by GDI device name containing monitor information
+ public static unsafe Dictionary GetAllMonitorDisplayInfo()
+ {
+ var monitorInfo = new Dictionary(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, EDID ID, device path)
+ var (friendlyName, edidId, 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,
+ EdidId = edidId ?? 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;
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiValidationResult.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiValidationResult.cs
new file mode 100644
index 0000000000..33cb3b7b5e
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiValidationResult.cs
@@ -0,0 +1,57 @@
+// 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
+{
+ ///
+ /// DDC/CI validation result containing both validation status and cached capabilities data.
+ /// This allows reusing capabilities data retrieved during validation, avoiding duplicate I2C calls.
+ ///
+ public struct DdcCiValidationResult
+ {
+ ///
+ /// Gets a value indicating whether the monitor has a valid DDC/CI connection with brightness support.
+ ///
+ public bool IsValid { get; }
+
+ ///
+ /// Gets the raw capabilities string retrieved during validation.
+ /// Null if retrieval failed.
+ ///
+ public string? CapabilitiesString { get; }
+
+ ///
+ /// Gets the parsed VCP capabilities info retrieved during validation.
+ /// Null if parsing failed.
+ ///
+ public Models.VcpCapabilities? VcpCapabilitiesInfo { get; }
+
+ ///
+ /// Gets a value indicating whether capabilities retrieval was attempted.
+ /// True means the result is from an actual attempt (success or failure).
+ ///
+ public bool WasAttempted { get; }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public DdcCiValidationResult(bool isValid, string? capabilitiesString = null, Models.VcpCapabilities? vcpCapabilitiesInfo = null, bool wasAttempted = true)
+ {
+ IsValid = isValid;
+ CapabilitiesString = capabilitiesString;
+ VcpCapabilitiesInfo = vcpCapabilitiesInfo;
+ WasAttempted = wasAttempted;
+ }
+
+ ///
+ /// Gets an invalid validation result with no cached data.
+ ///
+ public static DdcCiValidationResult Invalid => new(false, null, null, true);
+
+ ///
+ /// Gets a result indicating validation was not attempted yet.
+ ///
+ public static DdcCiValidationResult NotAttempted => new(false, null, null, false);
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDiscoveryHelper.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDiscoveryHelper.cs
new file mode 100644
index 0000000000..82d0240e80
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDiscoveryHelper.cs
@@ -0,0 +1,150 @@
+// 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
+{
+ ///
+ /// Helper class for discovering and creating monitor objects
+ ///
+ public class MonitorDiscoveryHelper
+ {
+ ///
+ /// Get physical monitors for a logical monitor.
+ /// Filters out any monitors with NULL handles (Windows API bug workaround).
+ ///
+ /// Handle to the logical monitor
+ /// Output: true if any NULL handles were filtered out
+ /// Array of valid physical monitors, or null if API call failed
+ 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();
+ 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;
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Physical monitor structure with handle and description
+ /// Display info from QueryDisplayConfig (EdidId, FriendlyName, MonitorNumber)
+ internal Monitor? CreateMonitorFromPhysical(
+ PHYSICAL_MONITOR physicalMonitor,
+ MonitorDisplayInfo monitorInfo)
+ {
+ try
+ {
+ // Get EDID ID and friendly name directly from MonitorDisplayInfo
+ string edidId = monitorInfo.EdidId ?? 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;
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDisplayInfo.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDisplayInfo.cs
new file mode 100644
index 0000000000..9faad9f18a
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/MonitorDisplayInfo.cs
@@ -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 Windows.Win32.Foundation;
+
+namespace PowerDisplay.Common.Drivers.DDC
+{
+ ///
+ /// Monitor display information structure
+ ///
+ public struct MonitorDisplayInfo
+ {
+ ///
+ /// Gets or sets the monitor device path (e.g., "\\?\DISPLAY#DELA1D8#...").
+ /// This is unique per target and used as the primary key.
+ ///
+ public string DevicePath { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public string GdiDeviceName { get; set; }
+
+ ///
+ /// Gets or sets the friendly display name from EDID.
+ ///
+ public string FriendlyName { get; set; }
+
+ ///
+ /// Gets or sets the EDID ID derived from manufacturer and product code.
+ /// Format: "{ManufacturerCode}{ProductCode}" (e.g., "GSM5C6D", "LEN4038").
+ /// Note: This is NOT unique - same model monitors have the same EdidId.
+ ///
+ public string EdidId { get; set; }
+
+ public LUID AdapterId { get; set; }
+
+ public uint TargetId { get; set; }
+
+ ///
+ /// 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.)
+ ///
+ public int MonitorNumber { get; set; }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/PhysicalMonitorHandleManager.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/PhysicalMonitorHandleManager.cs
new file mode 100644
index 0000000000..c9482673d2
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/PhysicalMonitorHandleManager.cs
@@ -0,0 +1,106 @@
+// 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
+{
+ ///
+ /// Manages physical monitor handles - reuse, cleanup, and validation
+ ///
+ public partial class PhysicalMonitorHandleManager : IDisposable
+ {
+ // Mapping: monitorId -> physical handle (thread-safe)
+ private readonly ConcurrentDictionary _monitorIdToHandleMap = new();
+ private readonly object _handleLock = new();
+ private bool _disposed;
+
+ ///
+ /// Update the handle mapping with new handles
+ ///
+ public void UpdateHandleMap(Dictionary 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;
+ }
+ }
+ }
+
+ ///
+ /// Clean up handles that are no longer in use.
+ /// Called within lock context. Optimized to O(n) using HashSet lookup.
+ ///
+ private void CleanupUnusedHandles(Dictionary newHandles)
+ {
+ if (_monitorIdToHandleMap.IsEmpty)
+ {
+ return;
+ }
+
+ // Build HashSet of handles that will be reused (O(m))
+ var reusedHandles = new HashSet(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 (Exception ex)
+ {
+ Logger.LogTrace($"Failed to destroy physical monitor handle 0x{handle:X}: {ex.Message}");
+ }
+ }
+ }
+
+ 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 (Exception ex)
+ {
+ Logger.LogTrace($"Failed to destroy physical monitor handle 0x{handle:X} during dispose: {ex.Message}");
+ }
+ }
+ }
+
+ _monitorIdToHandleMap.Clear();
+ _disposed = true;
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeConstants.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeConstants.cs
new file mode 100644
index 0000000000..7a3983cc4f
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeConstants.cs
@@ -0,0 +1,146 @@
+// 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
+{
+ ///
+ /// Windows API constant definitions
+ ///
+ public static class NativeConstants
+ {
+ ///
+ /// VCP code: Brightness (0x10)
+ /// Standard VESA MCCS brightness control.
+ /// This is the ONLY brightness code used by PowerDisplay.
+ ///
+ public const byte VcpCodeBrightness = 0x10;
+
+ ///
+ /// VCP code: Contrast (0x12)
+ /// Standard VESA MCCS contrast control.
+ ///
+ public const byte VcpCodeContrast = 0x12;
+
+ ///
+ /// VCP code: Audio Speaker Volume (0x62)
+ /// Standard VESA MCCS volume control for monitors with built-in speakers.
+ ///
+ public const byte VcpCodeVolume = 0x62;
+
+ ///
+ /// 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.
+ ///
+ public const byte VcpCodeSelectColorPreset = 0x14;
+
+ ///
+ /// 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.
+ ///
+ public const byte VcpCodeInputSource = 0x60;
+
+ ///
+ /// VCP code: Power Mode (0xD6)
+ /// Controls monitor power state via DPMS.
+ /// Values: 0x01=On, 0x02=Standby, 0x03=Suspend, 0x04=Off(DPM), 0x05=Off(Hard).
+ /// Note: Switching to any non-On state will turn off the display.
+ ///
+ public const byte VcpCodePowerMode = 0xD6;
+
+ ///
+ /// Query display config: only active paths
+ ///
+ public const uint QdcOnlyActivePaths = 0x00000002;
+
+ ///
+ /// Get source name (GDI device name like "\\.\DISPLAY1")
+ ///
+ public const uint DisplayconfigDeviceInfoGetSourceName = 1;
+
+ ///
+ /// Get target name (monitor friendly name and hardware ID)
+ ///
+ public const uint DisplayconfigDeviceInfoGetTargetName = 2;
+
+ ///
+ /// Retrieve the current settings for the display device.
+ ///
+ public const int EnumCurrentSettings = -1;
+
+ ///
+ /// The display is in the natural orientation of the device.
+ ///
+ public const int DmdoDefault = 0;
+
+ ///
+ /// The display is rotated 180 degrees (measured clockwise) from its natural orientation.
+ ///
+ public const int Dmdo180 = 2;
+
+ // ==================== DEVMODE field flags ====================
+
+ ///
+ /// DmDisplayOrientation field is valid.
+ ///
+ public const int DmDisplayOrientation = 0x00000080;
+
+ ///
+ /// DmPelsWidth field is valid.
+ ///
+ public const int DmPelsWidth = 0x00080000;
+
+ ///
+ /// DmPelsHeight field is valid.
+ ///
+ public const int DmPelsHeight = 0x00100000;
+
+ // ==================== ChangeDisplaySettings flags ====================
+
+ ///
+ /// Test the graphics mode but don't actually set it.
+ ///
+ public const uint CdsTest = 0x00000002;
+
+ // ==================== ChangeDisplaySettings result codes ====================
+
+ ///
+ /// The settings change was successful.
+ ///
+ public const int DispChangeSuccessful = 0;
+
+ ///
+ /// The computer must be restarted for the graphics mode to work.
+ ///
+ public const int DispChangeRestart = 1;
+
+ ///
+ /// The display driver failed the specified graphics mode.
+ ///
+ public const int DispChangeFailed = -1;
+
+ ///
+ /// The graphics mode is not supported.
+ ///
+ public const int DispChangeBadmode = -2;
+
+ ///
+ /// Unable to write settings to the registry.
+ ///
+ public const int DispChangeNotupdated = -3;
+
+ ///
+ /// An invalid set of flags was passed in.
+ ///
+ public const int DispChangeBadflags = -4;
+
+ ///
+ /// An invalid parameter was passed in.
+ ///
+ public const int DispChangeBadparam = -5;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeDelegates.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeDelegates.cs
new file mode 100644
index 0000000000..03f77535d0
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeDelegates.cs
@@ -0,0 +1,25 @@
+// 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;
+
+///
+/// Native delegate type definitions
+///
+public static class NativeDelegates
+{
+ ///
+ /// Monitor enumeration procedure delegate
+ ///
+ /// Monitor handle
+ /// Monitor device context
+ /// Pointer to monitor rectangle
+ /// User data
+ /// True to continue enumeration
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DevMode.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DevMode.cs
new file mode 100644
index 0000000000..7a600d5b7e
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DevMode.cs
@@ -0,0 +1,55 @@
+// 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
+{
+ ///
+ /// The DEVMODE structure contains information about the initialization and environment of a printer or a display device.
+ ///
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public unsafe struct DevMode
+ {
+ ///
+ /// Device name - fixed buffer for LibraryImport compatibility
+ ///
+ 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;
+
+ ///
+ /// Form name - fixed buffer for LibraryImport compatibility
+ ///
+ 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;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfig2DRegion.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfig2DRegion.cs
new file mode 100644
index 0000000000..27c7ea1c7f
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfig2DRegion.cs
@@ -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.Runtime.InteropServices;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration 2D region
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfig2DRegion
+ {
+ public uint Cx;
+ public uint Cy;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigDeviceInfoHeader.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigDeviceInfoHeader.cs
new file mode 100644
index 0000000000..48b2bbcde5
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigDeviceInfoHeader.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+using Windows.Win32.Foundation;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration device information header
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigDeviceInfoHeader
+ {
+ public uint Type;
+ public uint Size;
+ public LUID AdapterId;
+ public uint Id;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigModeInfo.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigModeInfo.cs
new file mode 100644
index 0000000000..9c63467659
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigModeInfo.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+using Windows.Win32.Foundation;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration mode information
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigModeInfo
+ {
+ public uint InfoType;
+ public uint Id;
+ public LUID AdapterId;
+ public DisplayConfigModeInfoUnion ModeInfo;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigModeInfoUnion.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigModeInfoUnion.cs
new file mode 100644
index 0000000000..aabe635a41
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigModeInfoUnion.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration mode information union
+ ///
+ [StructLayout(LayoutKind.Explicit)]
+ public struct DisplayConfigModeInfoUnion
+ {
+ [FieldOffset(0)]
+ public DisplayConfigTargetMode TargetMode;
+
+ [FieldOffset(0)]
+ public DisplayConfigSourceMode SourceMode;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathInfo.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathInfo.cs
new file mode 100644
index 0000000000..06880ec425
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathInfo.cs
@@ -0,0 +1,19 @@
+// 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
+{
+ ///
+ /// Display configuration path information
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigPathInfo
+ {
+ public DisplayConfigPathSourceInfo SourceInfo;
+ public DisplayConfigPathTargetInfo TargetInfo;
+ public uint Flags;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathSourceInfo.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathSourceInfo.cs
new file mode 100644
index 0000000000..ea38f3fade
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathSourceInfo.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+using Windows.Win32.Foundation;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration path source information
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigPathSourceInfo
+ {
+ public LUID AdapterId;
+ public uint Id;
+ public uint ModeInfoIdx;
+ public uint StatusFlags;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathTargetInfo.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathTargetInfo.cs
new file mode 100644
index 0000000000..739aef3357
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPathTargetInfo.cs
@@ -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 System.Runtime.InteropServices;
+
+using Windows.Win32.Foundation;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration path target information
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigPathTargetInfo
+ {
+ public LUID AdapterId;
+ public uint Id;
+ public uint ModeInfoIdx;
+ public uint OutputTechnology;
+ public uint Rotation;
+ public uint Scaling;
+ public DisplayConfigRational RefreshRate;
+ public uint ScanLineOrdering;
+ public bool TargetAvailable;
+ public uint StatusFlags;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPoint.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPoint.cs
new file mode 100644
index 0000000000..d2ad0a76f8
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigPoint.cs
@@ -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.Runtime.InteropServices;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration point
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigPoint
+ {
+ public int X;
+ public int Y;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigRational.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigRational.cs
new file mode 100644
index 0000000000..dde4497d73
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigRational.cs
@@ -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.Runtime.InteropServices;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration rational number
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigRational
+ {
+ public uint Numerator;
+ public uint Denominator;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigSourceDeviceName.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigSourceDeviceName.cs
new file mode 100644
index 0000000000..7af54f0609
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigSourceDeviceName.cs
@@ -0,0 +1,33 @@
+// 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
+{
+ ///
+ /// Display configuration source device name - contains GDI device name (e.g., "\\.\DISPLAY1")
+ ///
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public unsafe struct DisplayConfigSourceDeviceName
+ {
+ public DisplayConfigDeviceInfoHeader Header;
+
+ ///
+ /// GDI device name - fixed buffer for 32 wide characters (CCHDEVICENAME)
+ ///
+ public fixed ushort ViewGdiDeviceName[32];
+
+ ///
+ /// Helper method to get GDI device name as string
+ ///
+ public readonly string GetViewGdiDeviceName()
+ {
+ fixed (ushort* ptr = ViewGdiDeviceName)
+ {
+ return new string((char*)ptr);
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigSourceMode.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigSourceMode.cs
new file mode 100644
index 0000000000..a39b7a298d
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigSourceMode.cs
@@ -0,0 +1,20 @@
+// 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
+{
+ ///
+ /// Display configuration source mode
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigSourceMode
+ {
+ public uint Width;
+ public uint Height;
+ public uint PixelFormat;
+ public DisplayConfigPoint Position;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigTargetDeviceName.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigTargetDeviceName.cs
new file mode 100644
index 0000000000..9a38f82c30
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigTargetDeviceName.cs
@@ -0,0 +1,54 @@
+// 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
+{
+ ///
+ /// Display configuration target device name
+ ///
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public unsafe struct DisplayConfigTargetDeviceName
+ {
+ public DisplayConfigDeviceInfoHeader Header;
+ public uint Flags;
+ public uint OutputTechnology;
+ public ushort EdidManufactureId;
+ public ushort EdidProductCodeId;
+ public uint ConnectorInstance;
+
+ ///
+ /// Monitor friendly name - fixed buffer for LibraryImport compatibility
+ ///
+ public fixed ushort MonitorFriendlyDeviceName[64];
+
+ ///
+ /// Monitor device path - fixed buffer for LibraryImport compatibility
+ ///
+ public fixed ushort MonitorDevicePath[128];
+
+ ///
+ /// Helper method to get monitor friendly name as string
+ ///
+ public readonly string GetMonitorFriendlyDeviceName()
+ {
+ fixed (ushort* ptr = MonitorFriendlyDeviceName)
+ {
+ return new string((char*)ptr);
+ }
+ }
+
+ ///
+ /// Helper method to get monitor device path as string
+ ///
+ public readonly string GetMonitorDevicePath()
+ {
+ fixed (ushort* ptr = MonitorDevicePath)
+ {
+ return new string((char*)ptr);
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigTargetMode.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigTargetMode.cs
new file mode 100644
index 0000000000..9ea0f15867
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigTargetMode.cs
@@ -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.Runtime.InteropServices;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// Display configuration target mode
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigTargetMode
+ {
+ public DisplayConfigVideoSignalInfo TargetVideoSignalInfo;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigVideoSignalInfo.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigVideoSignalInfo.cs
new file mode 100644
index 0000000000..36c4907e5c
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/DisplayConfigVideoSignalInfo.cs
@@ -0,0 +1,23 @@
+// 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
+{
+ ///
+ /// Display configuration video signal information
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DisplayConfigVideoSignalInfo
+ {
+ public ulong PixelRate;
+ public DisplayConfigRational HSyncFreq;
+ public DisplayConfigRational VSyncFreq;
+ public DisplayConfig2DRegion ActiveSize;
+ public DisplayConfig2DRegion TotalSize;
+ public uint VideoStandard;
+ public uint ScanLineOrdering;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/MonitorInfoEx.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/MonitorInfoEx.cs
new file mode 100644
index 0000000000..1af97ed764
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/MonitorInfoEx.cs
@@ -0,0 +1,51 @@
+// 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
+{
+ ///
+ /// Monitor information extended structure
+ ///
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public unsafe struct MonitorInfoEx
+ {
+ ///
+ /// Structure size
+ ///
+ public uint CbSize;
+
+ ///
+ /// Monitor rectangle area
+ ///
+ public Rect RcMonitor;
+
+ ///
+ /// Work area rectangle
+ ///
+ public Rect RcWork;
+
+ ///
+ /// Flags
+ ///
+ public uint DwFlags;
+
+ ///
+ /// Device name (e.g., "\\.\DISPLAY1") - fixed buffer for LibraryImport compatibility
+ ///
+ public fixed ushort SzDevice[32];
+
+ ///
+ /// Helper property to get device name as string
+ ///
+ public readonly string GetDeviceName()
+ {
+ fixed (ushort* ptr = SzDevice)
+ {
+ return new string((char*)ptr);
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/PhysicalMonitor.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/PhysicalMonitor.cs
new file mode 100644
index 0000000000..a418dcc168
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/PhysicalMonitor.cs
@@ -0,0 +1,37 @@
+// 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
+{
+ ///
+ /// Physical monitor structure for DDC/CI
+ ///
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public unsafe struct PhysicalMonitor
+ {
+ ///
+ /// Physical monitor handle
+ ///
+ public IntPtr HPhysicalMonitor;
+
+ ///
+ /// Physical monitor description string - fixed buffer for LibraryImport compatibility
+ ///
+ public fixed ushort SzPhysicalMonitorDescription[128];
+
+ ///
+ /// Helper method to get description as string
+ ///
+ public readonly string GetDescription()
+ {
+ fixed (ushort* ptr = SzPhysicalMonitorDescription)
+ {
+ return new string((char*)ptr);
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/Rect.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/Rect.cs
new file mode 100644
index 0000000000..0af4d13dc5
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/NativeStructures/Rect.cs
@@ -0,0 +1,32 @@
+// 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
+{
+ ///
+ /// Rectangle structure
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct Rect
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+
+ public int Width => Right - Left;
+
+ public int Height => Bottom - Top;
+
+ public Rect(int left, int top, int right, int bottom)
+ {
+ Left = left;
+ Top = top;
+ Right = right;
+ Bottom = bottom;
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/PInvoke.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/PInvoke.cs
new file mode 100644
index 0000000000..1e1ab5185e
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/PInvoke.cs
@@ -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.Runtime.InteropServices;
+
+namespace PowerDisplay.Common.Drivers
+{
+ ///
+ /// P/Invoke declarations using LibraryImport source generator
+ ///
+ internal static partial class PInvoke
+ {
+ // ==================== User32.dll - Display Configuration ====================
+ [LibraryImport("user32.dll")]
+ internal static partial int GetDisplayConfigBufferSizes(
+ uint flags,
+ out uint numPathArrayElements,
+ out uint numModeInfoArrayElements);
+
+ // Use unsafe pointer to avoid runtime marshalling
+ [LibraryImport("user32.dll")]
+ internal static unsafe partial int QueryDisplayConfig(
+ uint flags,
+ ref uint numPathArrayElements,
+ DisplayConfigPathInfo* pathArray,
+ ref uint numModeInfoArrayElements,
+ DisplayConfigModeInfo* modeInfoArray,
+ IntPtr currentTopologyId);
+
+ [LibraryImport("user32.dll")]
+ internal static unsafe partial int DisplayConfigGetDeviceInfo(
+ DisplayConfigTargetDeviceName* deviceName);
+
+ [LibraryImport("user32.dll")]
+ internal static unsafe partial int DisplayConfigGetDeviceInfo(
+ DisplayConfigSourceDeviceName* sourceName);
+
+ // ==================== User32.dll - Monitor Enumeration ====================
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool EnumDisplayMonitors(
+ IntPtr hdc,
+ IntPtr lprcClip,
+ NativeDelegates.MonitorEnumProc lpfnEnum,
+ IntPtr dwData);
+
+ [LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW", StringMarshalling = StringMarshalling.Utf16)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static unsafe partial bool GetMonitorInfo(
+ IntPtr hMonitor,
+ MonitorInfoEx* lpmi);
+
+ [LibraryImport("user32.dll", EntryPoint = "EnumDisplaySettingsW", StringMarshalling = StringMarshalling.Utf16)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static unsafe partial bool EnumDisplaySettings(
+ [MarshalAs(UnmanagedType.LPWStr)] string? lpszDeviceName,
+ int iModeNum,
+ DevMode* lpDevMode);
+
+ [LibraryImport("user32.dll", EntryPoint = "ChangeDisplaySettingsExW", StringMarshalling = StringMarshalling.Utf16)]
+ internal static unsafe partial int ChangeDisplaySettingsEx(
+ [MarshalAs(UnmanagedType.LPWStr)] string? lpszDeviceName,
+ DevMode* lpDevMode,
+ IntPtr hwnd,
+ uint dwflags,
+ IntPtr lParam);
+
+ // ==================== Dxva2.dll - DDC/CI Monitor Control ====================
+ [LibraryImport("Dxva2.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool GetNumberOfPhysicalMonitorsFromHMONITOR(
+ IntPtr hMonitor,
+ out uint pdwNumberOfPhysicalMonitors);
+
+ // Use unsafe pointer to avoid ArraySubType limitation
+ [LibraryImport("Dxva2.dll", StringMarshalling = StringMarshalling.Utf16)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static unsafe partial bool GetPhysicalMonitorsFromHMONITOR(
+ IntPtr hMonitor,
+ uint dwPhysicalMonitorArraySize,
+ PhysicalMonitor* pPhysicalMonitorArray);
+
+ [LibraryImport("Dxva2.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool DestroyPhysicalMonitor(IntPtr hMonitor);
+
+ [LibraryImport("Dxva2.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool GetVCPFeatureAndVCPFeatureReply(
+ IntPtr hPhysicalMonitor,
+ byte bVCPCode,
+ IntPtr pvct,
+ out uint pdwCurrentValue,
+ out uint pdwMaximumValue);
+
+ [LibraryImport("Dxva2.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool SetVCPFeature(
+ IntPtr hPhysicalMonitor,
+ byte bVCPCode,
+ uint dwNewValue);
+
+ [LibraryImport("Dxva2.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool GetCapabilitiesStringLength(
+ IntPtr hPhysicalMonitor,
+ out uint pdwCapabilitiesStringLengthInCharacters);
+
+ [LibraryImport("Dxva2.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool CapabilitiesRequestAndCapabilitiesReply(
+ IntPtr hPhysicalMonitor,
+ IntPtr pszASCIICapabilitiesString,
+ uint dwCapabilitiesStringLengthInCharacters);
+
+ // ==================== Kernel32.dll ====================
+ [LibraryImport("kernel32.dll")]
+ internal static partial uint GetLastError();
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/WMI/WmiController.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/WMI/WmiController.cs
new file mode 100644
index 0000000000..4b464500da
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/WMI/WmiController.cs
@@ -0,0 +1,387 @@
+// 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.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using ManagedCommon;
+using PowerDisplay.Common.Interfaces;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Common.Utils;
+using WmiLight;
+using Monitor = PowerDisplay.Common.Models.Monitor;
+
+namespace PowerDisplay.Common.Drivers.WMI
+{
+ ///
+ /// WMI monitor controller for controlling internal laptop displays.
+ ///
+ public partial class WmiController : IMonitorController, IDisposable
+ {
+ private const string WmiNamespace = @"root\WMI";
+ private const string BrightnessQueryClass = "WmiMonitorBrightness";
+ private const string BrightnessMethodClass = "WmiMonitorBrightnessMethods";
+
+ // Common WMI error codes for classification
+ private const int WbemENotFound = unchecked((int)0x80041002);
+ private const int WbemEAccessDenied = unchecked((int)0x80041003);
+ private const int WbemEProviderFailure = unchecked((int)0x80041004);
+ private const int WbemEInvalidQuery = unchecked((int)0x80041017);
+ private const int WmiFeatureNotSupported = 0x1068;
+
+ ///
+ /// Classifies WMI exceptions into user-friendly error messages.
+ ///
+ private static MonitorOperationResult ClassifyWmiError(WmiException ex, string operation)
+ {
+ var hresult = ex.HResult;
+
+ return hresult switch
+ {
+ WbemENotFound => MonitorOperationResult.Failure($"WMI class not found during {operation}. This feature may not be supported on your system.", hresult),
+ WbemEAccessDenied => MonitorOperationResult.Failure($"Access denied during {operation}. Administrator privileges may be required.", hresult),
+ WbemEProviderFailure => MonitorOperationResult.Failure($"WMI provider failure during {operation}. The display driver may not support this feature.", hresult),
+ WbemEInvalidQuery => MonitorOperationResult.Failure($"Invalid WMI query during {operation}. This is likely a bug.", hresult),
+ WmiFeatureNotSupported => MonitorOperationResult.Failure($"WMI brightness control not supported on this system during {operation}.", hresult),
+ _ => MonitorOperationResult.Failure($"WMI error during {operation}: {ex.Message}", hresult),
+ };
+ }
+
+ ///
+ /// Escape special characters in WMI query strings.
+ /// WMI requires backslashes and single quotes to be escaped in WHERE clauses.
+ /// See: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wql-sql-for-wmi
+ ///
+ /// The string value to escape.
+ /// The escaped string safe for use in WMI queries.
+ private static string EscapeWmiString(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return value;
+ }
+
+ // WMI requires backslashes and single quotes to be escaped in WHERE clauses
+ // Backslash must be escaped first to avoid double-escaping the quote's backslash
+ return value.Replace("\\", "\\\\").Replace("'", "\\'");
+ }
+
+ ///
+ /// Extract hardware ID from WMI InstanceName.
+ /// InstanceName format: "DISPLAY\BOE0900\4&10fd3ab1&0&UID265988_0"
+ /// Returns the second segment (e.g., "BOE0900") which is the manufacturer+product code.
+ ///
+ /// The WMI InstanceName.
+ /// The EDID ID extracted from the InstanceName, or empty string if extraction fails.
+ private static string ExtractEdidIdFromInstanceName(string instanceName)
+ {
+ if (string.IsNullOrEmpty(instanceName))
+ {
+ return string.Empty;
+ }
+
+ // Split by backslash: ["DISPLAY", "BOE0900", "4&10fd3ab1&0&UID265988_0"]
+ var parts = instanceName.Split('\\');
+ if (parts.Length >= 2 && !string.IsNullOrEmpty(parts[1]))
+ {
+ // Return the second part (e.g., "BOE0900")
+ return parts[1];
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ /// Build a WMI query filtered by monitor instance name.
+ ///
+ /// The WMI class to query.
+ /// The monitor instance name to filter by.
+ /// Optional SELECT clause fields (defaults to "*").
+ /// The formatted WMI query string.
+ private static string BuildInstanceNameQuery(string wmiClass, string instanceName, string selectClause = "*")
+ {
+ var escapedInstanceName = EscapeWmiString(instanceName);
+ return $"SELECT {selectClause} FROM {wmiClass} WHERE InstanceName = '{escapedInstanceName}'";
+ }
+
+ ///
+ /// Get MonitorDisplayInfo from dictionary by matching EdidId.
+ /// Uses QueryDisplayConfig path index which matches Windows Display Settings "Identify" feature.
+ ///
+ /// The EDID ID to match (e.g., "LEN4038", "BOE0900").
+ /// Dictionary of monitor display info from QueryDisplayConfig.
+ /// MonitorDisplayInfo if found, or null if not found.
+ private static Drivers.DDC.MonitorDisplayInfo? GetMonitorDisplayInfoByEdidId(string edidId, Dictionary monitorDisplayInfos)
+ {
+ if (string.IsNullOrEmpty(edidId) || monitorDisplayInfos == null || monitorDisplayInfos.Count == 0)
+ {
+ return null;
+ }
+
+ var match = monitorDisplayInfos.Values.FirstOrDefault(
+ v => edidId.Equals(v.EdidId, StringComparison.OrdinalIgnoreCase));
+
+ // Check if match was found (struct default has null/empty EdidId)
+ if (!string.IsNullOrEmpty(match.EdidId))
+ {
+ return match;
+ }
+
+ Logger.LogWarning($"WMI: Could not find MonitorDisplayInfo for EdidId '{edidId}'");
+ return null;
+ }
+
+ public string Name => "WMI Monitor Controller";
+
+ ///
+ /// Get monitor brightness
+ ///
+ public async Task GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(monitor);
+
+ return await Task.Run(
+ () =>
+ {
+ try
+ {
+ using var connection = new WmiConnection(WmiNamespace);
+ var query = BuildInstanceNameQuery(BrightnessQueryClass, monitor.InstanceName, "CurrentBrightness");
+ var results = connection.CreateQuery(query);
+
+ foreach (var obj in results)
+ {
+ var currentBrightness = obj.GetPropertyValue("CurrentBrightness");
+ return new VcpFeatureValue(currentBrightness, 0, 100);
+ }
+
+ // No match found - monitor may have been disconnected
+ }
+ catch (WmiException ex)
+ {
+ Logger.LogWarning($"WMI GetBrightness failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"WMI GetBrightness failed: {ex.Message}");
+ }
+
+ return VcpFeatureValue.Invalid;
+ },
+ cancellationToken);
+ }
+
+ ///
+ /// Set monitor brightness
+ ///
+ public async Task SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(monitor);
+
+ // Validate brightness range
+ brightness = Math.Clamp(brightness, 0, 100);
+
+ return await Task.Run(
+ () =>
+ {
+ try
+ {
+ using var connection = new WmiConnection(WmiNamespace);
+ var query = BuildInstanceNameQuery(BrightnessMethodClass, monitor.InstanceName);
+ var results = connection.CreateQuery(query);
+
+ foreach (var obj in results)
+ {
+ // Call WmiSetBrightness method
+ // Parameters: Timeout (uint32), Brightness (uint8)
+ // Note: WmiLight requires string values for method parameters
+ using (WmiMethod method = obj.GetMethod("WmiSetBrightness"))
+ using (WmiMethodParameters inParams = method.CreateInParameters())
+ {
+ inParams.SetPropertyValue("Timeout", "0");
+ inParams.SetPropertyValue("Brightness", brightness.ToString(CultureInfo.InvariantCulture));
+
+ uint result = obj.ExecuteMethod(
+ method,
+ inParams,
+ out WmiMethodParameters outParams);
+
+ // Check return value (0 indicates success)
+ if (result == 0)
+ {
+ return MonitorOperationResult.Success();
+ }
+
+ return MonitorOperationResult.Failure($"WMI method returned error code: {result}", (int)result);
+ }
+ }
+
+ // No match found - monitor may have been disconnected
+ Logger.LogWarning($"WMI SetBrightness: No monitor found with InstanceName '{monitor.InstanceName}'");
+ return MonitorOperationResult.Failure($"No WMI brightness method found for monitor '{monitor.InstanceName}'");
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return MonitorOperationResult.Failure("Access denied. Administrator privileges may be required.", 5);
+ }
+ catch (WmiException ex)
+ {
+ return ClassifyWmiError(ex, "SetBrightness");
+ }
+ catch (Exception ex)
+ {
+ return MonitorOperationResult.Failure($"Unexpected error during SetBrightness: {ex.Message}");
+ }
+ },
+ cancellationToken);
+ }
+
+ ///
+ /// Discover supported monitors.
+ /// WMI brightness control is typically only available on internal laptop displays,
+ /// which don't have meaningful UserFriendlyName in WmiMonitorID, so we use "Built-in Display".
+ ///
+ public async Task> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
+ {
+ return await Task.Run(
+ () =>
+ {
+ var monitors = new List();
+
+ try
+ {
+ using var connection = new WmiConnection(WmiNamespace);
+
+ // Query WMI brightness support - only internal displays typically support this
+ var brightnessQuery = $"SELECT InstanceName, CurrentBrightness FROM {BrightnessQueryClass}";
+ var brightnessResults = connection.CreateQuery(brightnessQuery).ToList();
+
+ if (brightnessResults.Count == 0)
+ {
+ return monitors;
+ }
+
+ // Get MonitorDisplayInfo from QueryDisplayConfig - this provides the correct monitor numbers
+ var monitorDisplayInfos = Drivers.DDC.DdcCiNative.GetAllMonitorDisplayInfo();
+
+ // Create monitor objects for each supported brightness instance
+ foreach (var obj in brightnessResults)
+ {
+ try
+ {
+ var instanceName = obj.GetPropertyValue("InstanceName") ?? string.Empty;
+ var currentBrightness = obj.GetPropertyValue("CurrentBrightness");
+
+ // Extract EDID ID from InstanceName
+ // e.g., "DISPLAY\LEN4038\4&40f4dee&0&UID8388688_0" -> "LEN4038"
+ var edidId = ExtractEdidIdFromInstanceName(instanceName);
+
+ // Get MonitorDisplayInfo from QueryDisplayConfig by matching EDID ID
+ // This provides MonitorNumber and GdiDeviceName for display settings APIs
+ var displayInfo = GetMonitorDisplayInfoByEdidId(edidId, monitorDisplayInfos);
+ int monitorNumber = displayInfo?.MonitorNumber ?? 0;
+ string gdiDeviceName = displayInfo?.GdiDeviceName ?? string.Empty;
+
+ // Generate unique ID: "WMI_{EdidId}_{MonitorNumber}"
+ string uniqueId = !string.IsNullOrEmpty(edidId)
+ ? $"WMI_{edidId}_{monitorNumber}"
+ : $"WMI_Unknown_{monitorNumber}";
+
+ // Get display name from PnP manufacturer ID (e.g., "Lenovo Built-in Display")
+ var displayName = PnpIdHelper.GetBuiltInDisplayName(edidId);
+
+ var monitor = new Monitor
+ {
+ Id = uniqueId,
+ Name = displayName,
+ CurrentBrightness = currentBrightness,
+ MinBrightness = 0,
+ MaxBrightness = 100,
+ IsAvailable = true,
+ InstanceName = instanceName,
+ Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
+ CommunicationMethod = "WMI",
+ SupportsColorTemperature = false,
+ MonitorNumber = monitorNumber,
+ GdiDeviceName = gdiDeviceName,
+ };
+
+ monitors.Add(monitor);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"Failed to create monitor from WMI data: {ex.Message}");
+ }
+ }
+ }
+ catch (WmiException ex)
+ {
+ Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message}");
+ }
+
+ return monitors;
+ },
+ cancellationToken);
+ }
+
+ // Extended features not supported by WMI
+ public Task SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(MonitorOperationResult.Failure("Contrast control not supported via WMI"));
+ }
+
+ public Task SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(MonitorOperationResult.Failure("Volume control not supported via WMI"));
+ }
+
+ public Task GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(VcpFeatureValue.Invalid);
+ }
+
+ public Task SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(MonitorOperationResult.Failure("Color temperature control not supported via WMI"));
+ }
+
+ public Task GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default)
+ {
+ // Input source switching not supported for internal displays
+ return Task.FromResult(VcpFeatureValue.Invalid);
+ }
+
+ public Task SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default)
+ {
+ // Input source switching not supported for internal displays
+ return Task.FromResult(MonitorOperationResult.Failure("Input source switching not supported via WMI"));
+ }
+
+ public Task SetPowerStateAsync(Monitor monitor, int powerState, CancellationToken cancellationToken = default)
+ {
+ // Power state control not supported for internal displays via WMI
+ return Task.FromResult(MonitorOperationResult.Failure("Power state control not supported via WMI"));
+ }
+
+ public Task GetPowerStateAsync(Monitor monitor, CancellationToken cancellationToken = default)
+ {
+ // Power state control not supported for internal displays via WMI
+ return Task.FromResult(VcpFeatureValue.Invalid);
+ }
+
+ public void Dispose()
+ {
+ // WmiLight objects are created per-operation and disposed immediately via using statements.
+ // No instance-level resources require cleanup.
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IMonitorController.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IMonitorController.cs
new file mode 100644
index 0000000000..cd3a6fe15d
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IMonitorController.cs
@@ -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.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using PowerDisplay.Common.Models;
+using Monitor = PowerDisplay.Common.Models.Monitor;
+
+namespace PowerDisplay.Common.Interfaces
+{
+ ///
+ /// Monitor controller interface
+ ///
+ public interface IMonitorController
+ {
+ ///
+ /// Gets controller name
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets monitor brightness
+ ///
+ /// Monitor object
+ /// Cancellation token
+ /// Brightness information
+ Task GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default);
+
+ ///
+ /// Sets monitor brightness
+ ///
+ /// Monitor object
+ /// Brightness value (0-100)
+ /// Cancellation token
+ /// Operation result
+ Task SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default);
+
+ ///
+ /// Discovers supported monitors
+ ///
+ /// Cancellation token
+ /// List of monitors
+ Task> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Sets monitor contrast
+ ///
+ /// Monitor object
+ /// Contrast value (0-100)
+ /// Cancellation token
+ /// Operation result
+ Task SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default);
+
+ ///
+ /// Sets monitor volume
+ ///
+ /// Monitor object
+ /// Volume value (0-100)
+ /// Cancellation token
+ /// Operation result
+ Task SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets monitor color temperature using VCP 0x14 (Select Color Preset)
+ ///
+ /// Monitor object
+ /// Cancellation token
+ /// VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
+ Task GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default);
+
+ ///
+ /// Sets monitor color temperature using VCP 0x14 preset value
+ ///
+ /// Monitor object
+ /// VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
+ /// Cancellation token
+ /// Operation result
+ Task SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets current input source using VCP 0x60
+ ///
+ /// Monitor object
+ /// Cancellation token
+ /// VCP input source value (e.g., 0x11 for HDMI-1)
+ Task GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default);
+
+ ///
+ /// Sets input source using VCP 0x60
+ ///
+ /// Monitor object
+ /// VCP input source value (e.g., 0x11 for HDMI-1)
+ /// Cancellation token
+ /// Operation result
+ Task SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default);
+
+ ///
+ /// Sets power state using VCP 0xD6 (Power Mode)
+ ///
+ /// Monitor object
+ /// VCP power state value: 0x01=On, 0x02=Standby, 0x03=Suspend, 0x04=Off(DPM), 0x05=Off(Hard)
+ /// Cancellation token
+ /// Operation result
+ Task SetPowerStateAsync(Monitor monitor, int powerState, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets current power state using VCP 0xD6 (Power Mode)
+ ///
+ /// Monitor object
+ /// Cancellation token
+ /// VCP power state value: 0x01=On, 0x02=Standby, 0x03=Suspend, 0x04=Off(DPM), 0x05=Off(Hard)
+ Task GetPowerStateAsync(Monitor monitor, CancellationToken cancellationToken = default);
+
+ ///
+ /// Releases resources
+ ///
+ void Dispose();
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IMonitorData.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IMonitorData.cs
new file mode 100644
index 0000000000..26f156b97c
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IMonitorData.cs
@@ -0,0 +1,56 @@
+// 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.Interfaces
+{
+ ///
+ /// Core interface representing monitor hardware data.
+ /// This interface defines the actual hardware values for a monitor.
+ /// Implementations can add UI-specific properties and use converters for display formatting.
+ ///
+ public interface IMonitorData
+ {
+ ///
+ /// Gets or sets the unique identifier for the monitor.
+ ///
+ string Id { get; set; }
+
+ ///
+ /// Gets or sets the display name of the monitor.
+ ///
+ string Name { get; set; }
+
+ ///
+ /// Gets or sets the current brightness value (0-100).
+ ///
+ int Brightness { get; set; }
+
+ ///
+ /// Gets or sets the current contrast value (0-100).
+ ///
+ int Contrast { get; set; }
+
+ ///
+ /// Gets or sets the current volume value (0-100).
+ ///
+ int Volume { get; set; }
+
+ ///
+ /// Gets or sets the color temperature VCP preset value (raw DDC/CI value from VCP code 0x14).
+ /// This stores the raw VCP value (e.g., 0x05 for 6500K preset), not the Kelvin temperature.
+ /// Use ColorTemperatureHelper to convert to/from human-readable display names.
+ ///
+ int ColorTemperatureVcp { get; set; }
+
+ ///
+ /// Gets or sets the monitor number (1, 2, 3...) as assigned by the OS.
+ ///
+ int MonitorNumber { get; set; }
+
+ ///
+ /// Gets or sets the monitor orientation (0=0, 1=90, 2=180, 3=270).
+ ///
+ int Orientation { get; set; }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IProfileService.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IProfileService.cs
new file mode 100644
index 0000000000..3562346b94
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Interfaces/IProfileService.cs
@@ -0,0 +1,62 @@
+// 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 PowerDisplay.Common.Models;
+
+namespace PowerDisplay.Common.Interfaces
+{
+ ///
+ /// Interface for profile management service.
+ /// Provides abstraction for loading, saving, and managing PowerDisplay profiles.
+ /// Enables dependency injection and unit testing.
+ ///
+ public interface IProfileService
+ {
+ ///
+ /// Loads PowerDisplay profiles from disk.
+ ///
+ /// PowerDisplayProfiles object, or a new empty instance if file doesn't exist or load fails.
+ PowerDisplayProfiles LoadProfiles();
+
+ ///
+ /// Saves PowerDisplay profiles to disk.
+ ///
+ /// The profiles collection to save.
+ /// True if save was successful, false otherwise.
+ bool SaveProfiles(PowerDisplayProfiles profiles);
+
+ ///
+ /// Adds or updates a profile in the collection and persists to disk.
+ ///
+ /// The profile to add or update.
+ /// True if operation was successful, false otherwise.
+ bool AddOrUpdateProfile(PowerDisplayProfile profile);
+
+ ///
+ /// Removes a profile by name and persists to disk.
+ ///
+ /// The name of the profile to remove.
+ /// True if profile was found and removed, false otherwise.
+ bool RemoveProfile(string profileName);
+
+ ///
+ /// Gets a profile by name.
+ ///
+ /// The name of the profile to retrieve.
+ /// The profile if found, null otherwise.
+ PowerDisplayProfile? GetProfile(string profileName);
+
+ ///
+ /// Checks if the profiles file exists.
+ ///
+ /// True if profiles file exists, false otherwise.
+ bool ProfilesFileExists();
+
+ ///
+ /// Gets the path to the profiles file.
+ ///
+ /// The full path to the profiles file.
+ string GetProfilesFilePath();
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/ColorPresetItem.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/ColorPresetItem.cs
new file mode 100644
index 0000000000..13baa45ed2
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/ColorPresetItem.cs
@@ -0,0 +1,86 @@
+// 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.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Represents a color temperature preset item for VCP code 0x14.
+ /// Used to display available color temperature presets in UI components.
+ ///
+ public partial class ColorPresetItem : INotifyPropertyChanged
+ {
+ private int _vcpValue;
+ private string _displayName = string.Empty;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorPresetItem()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The VCP value for the color temperature preset.
+ /// The display name for UI.
+ public ColorPresetItem(int vcpValue, string displayName)
+ {
+ _vcpValue = vcpValue;
+ _displayName = displayName;
+ }
+
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ ///
+ /// Gets or sets the VCP value for this color temperature preset.
+ ///
+ [JsonPropertyName("vcpValue")]
+ public int VcpValue
+ {
+ get => _vcpValue;
+ set
+ {
+ if (_vcpValue != value)
+ {
+ _vcpValue = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the display name for UI.
+ ///
+ [JsonPropertyName("displayName")]
+ public string DisplayName
+ {
+ get => _displayName;
+ set
+ {
+ if (_displayName != value)
+ {
+ _displayName = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Raises the PropertyChanged event.
+ ///
+ /// The name of the property that changed.
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs
new file mode 100644
index 0000000000..2b72890803
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/Monitor.cs
@@ -0,0 +1,382 @@
+// 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.ComponentModel;
+using System.Runtime.CompilerServices;
+using PowerDisplay.Common.Interfaces;
+using PowerDisplay.Common.Utils;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Monitor model that implements property change notification.
+ /// Implements IMonitorData to provide a common interface for monitor hardware values.
+ ///
+ ///
+ /// is the unique identifier used for all purposes: UI lookups, IPC, persistent storage, and handle management.
+ /// Format: "{Source}_{EdidId}_{MonitorNumber}" (e.g., "DDC_GSM5C6D_1", "WMI_BOE0900_2").
+ ///
+ public partial class Monitor : INotifyPropertyChanged, IMonitorData
+ {
+ private int _currentBrightness;
+ private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value)
+ private int _currentInputSource; // VCP 0x60 value
+ private int _currentPowerState = 0x01; // Default to On (VCP 0xD6 value)
+ private bool _isAvailable = true;
+ private int _orientation;
+
+ ///
+ /// Gets or sets unique identifier for all purposes: UI lookups, IPC, persistent storage, and handle management.
+ ///
+ ///
+ /// Format: "{Source}_{EdidId}_{MonitorNumber}" where Source is "DDC" or "WMI".
+ /// Examples: "DDC_GSM5C6D_1", "WMI_BOE0900_2".
+ /// Stable across reboots and unique even for multiple identical monitors.
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets display name
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets current brightness (0-100)
+ ///
+ public int CurrentBrightness
+ {
+ get => _currentBrightness;
+ set
+ {
+ var clamped = Math.Clamp(value, MinBrightness, MaxBrightness);
+ if (_currentBrightness != clamped)
+ {
+ _currentBrightness = clamped;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets minimum brightness value
+ ///
+ public int MinBrightness { get; set; }
+
+ ///
+ /// Gets or sets maximum brightness value
+ ///
+ public int MaxBrightness { get; set; } = 100;
+
+ ///
+ /// Gets or sets current color temperature VCP preset value (from VCP code 0x14).
+ /// This stores the raw VCP value (e.g., 0x05 for 6500K), not Kelvin temperature.
+ /// Use ColorTemperaturePresetName to get human-readable name.
+ ///
+ public int CurrentColorTemperature
+ {
+ get => _currentColorTemperature;
+ set
+ {
+ if (_currentColorTemperature != value)
+ {
+ _currentColorTemperature = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(ColorTemperaturePresetName));
+ }
+ }
+ }
+
+ ///
+ /// Gets human-readable color temperature preset name (e.g., "6500K (0x05)", "sRGB (0x01)")
+ ///
+ public string ColorTemperaturePresetName =>
+ VcpNames.GetFormattedValueName(0x14, CurrentColorTemperature);
+
+ ///
+ /// Gets or sets a value indicating whether the monitor supports color temperature adjustment via VCP 0x14
+ ///
+ public bool SupportsColorTemperature { get; set; }
+
+ ///
+ /// Gets or sets current input source VCP value (from VCP code 0x60).
+ /// This stores the raw VCP value (e.g., 0x11 for HDMI-1).
+ /// Use InputSourceName to get human-readable name.
+ ///
+ public int CurrentInputSource
+ {
+ get => _currentInputSource;
+ set
+ {
+ if (_currentInputSource != value)
+ {
+ _currentInputSource = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(InputSourceName));
+ }
+ }
+ }
+
+ ///
+ /// Gets human-readable input source name (e.g., "HDMI-1", "DisplayPort-1")
+ /// Returns just the name without hex value for cleaner UI display.
+ ///
+ public string InputSourceName =>
+ VcpNames.GetValueName(0x60, CurrentInputSource) ?? $"Source 0x{CurrentInputSource:X2}";
+
+ ///
+ /// Gets a value indicating whether the monitor supports input source switching via VCP 0x60
+ ///
+ public bool SupportsInputSource => VcpCapabilitiesInfo?.SupportsVcpCode(0x60) ?? false;
+
+ ///
+ /// Gets get supported input sources from capabilities (as list of VCP values)
+ ///
+ public System.Collections.Generic.IReadOnlyList? SupportedInputSources =>
+ VcpCapabilitiesInfo?.GetSupportedValues(0x60);
+
+ ///
+ /// Gets a value indicating whether the monitor supports power state control via VCP 0xD6
+ ///
+ public bool SupportsPowerState => VcpCapabilitiesInfo?.SupportsVcpCode(0xD6) ?? false;
+
+ ///
+ /// Gets supported power states from capabilities (as list of VCP values)
+ /// Values: 0x01=On, 0x02=Standby, 0x03=Suspend, 0x04=Off(DPM), 0x05=Off(Hard)
+ ///
+ public System.Collections.Generic.IReadOnlyList? SupportedPowerStates =>
+ VcpCapabilitiesInfo?.GetSupportedValues(0xD6);
+
+ ///
+ /// Gets or sets current power state VCP value (from VCP code 0xD6).
+ /// Values: 0x01=On, 0x02=Standby, 0x03=Suspend, 0x04=Off(DPM), 0x05=Off(Hard).
+ ///
+ public int CurrentPowerState
+ {
+ get => _currentPowerState;
+ set
+ {
+ if (_currentPowerState != value)
+ {
+ _currentPowerState = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the monitor supports contrast adjustment
+ ///
+ public bool SupportsContrast => Capabilities.HasFlag(MonitorCapabilities.Contrast);
+
+ ///
+ /// Gets a value indicating whether the monitor supports volume adjustment (for audio-capable monitors)
+ ///
+ public bool SupportsVolume => Capabilities.HasFlag(MonitorCapabilities.Volume);
+
+ private int _currentContrast = 50;
+ private int _currentVolume = 50;
+
+ ///
+ /// Gets or sets current contrast (0-100)
+ ///
+ public int CurrentContrast
+ {
+ get => _currentContrast;
+ set
+ {
+ var clamped = Math.Clamp(value, MinContrast, MaxContrast);
+ if (_currentContrast != clamped)
+ {
+ _currentContrast = clamped;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets minimum contrast value
+ ///
+ public int MinContrast { get; set; }
+
+ ///
+ /// Gets or sets maximum contrast value
+ ///
+ public int MaxContrast { get; set; } = 100;
+
+ ///
+ /// Gets or sets current volume (0-100)
+ ///
+ public int CurrentVolume
+ {
+ get => _currentVolume;
+ set
+ {
+ var clamped = Math.Clamp(value, MinVolume, MaxVolume);
+ if (_currentVolume != clamped)
+ {
+ _currentVolume = clamped;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets minimum volume value
+ ///
+ public int MinVolume { get; set; }
+
+ ///
+ /// Gets or sets maximum volume value
+ ///
+ public int MaxVolume { get; set; } = 100;
+
+ ///
+ /// Gets or sets a value indicating whether the monitor is available/online
+ ///
+ public bool IsAvailable
+ {
+ get => _isAvailable;
+ set
+ {
+ if (_isAvailable != value)
+ {
+ _isAvailable = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets physical monitor handle (for DDC/CI)
+ ///
+ public IntPtr Handle { get; set; } = IntPtr.Zero;
+
+ ///
+ /// Gets or sets instance name (used by WMI)
+ ///
+ public string InstanceName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets communication method (DDC/CI, WMI, HDR API, etc.)
+ ///
+ public string CommunicationMethod { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets supported control methods
+ ///
+ public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
+
+ ///
+ /// Gets or sets raw DDC/CI capabilities string (MCCS format)
+ ///
+ public string? CapabilitiesRaw { get; set; }
+
+ ///
+ /// Gets or sets parsed VCP capabilities information
+ ///
+ public VcpCapabilities? VcpCapabilitiesInfo { get; set; }
+
+ ///
+ /// Gets or sets last update time
+ ///
+ public DateTime LastUpdate { get; set; } = DateTime.Now;
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public override string ToString()
+ {
+ return $"{Name} ({CommunicationMethod}) - {CurrentBrightness}%";
+ }
+
+ ///
+ /// Update monitor status
+ ///
+ public void UpdateStatus(int brightness, bool isAvailable = true)
+ {
+ IsAvailable = isAvailable;
+ if (isAvailable)
+ {
+ CurrentBrightness = brightness;
+ LastUpdate = DateTime.Now;
+ }
+ }
+
+ ///
+ int IMonitorData.Brightness
+ {
+ get => CurrentBrightness;
+ set => CurrentBrightness = value;
+ }
+
+ ///
+ int IMonitorData.Contrast
+ {
+ get => CurrentContrast;
+ set => CurrentContrast = value;
+ }
+
+ ///
+ int IMonitorData.Volume
+ {
+ get => CurrentVolume;
+ set => CurrentVolume = value;
+ }
+
+ ///
+ int IMonitorData.ColorTemperatureVcp
+ {
+ get => CurrentColorTemperature;
+ set => CurrentColorTemperature = value;
+ }
+
+ ///
+ /// Gets or sets monitor number (1, 2, 3...)
+ ///
+ public int MonitorNumber { get; set; }
+
+ ///
+ /// Gets or sets the GDI device name (e.g., "\\.\DISPLAY1").
+ /// This is obtained from QueryDisplayConfig during discovery and should be used
+ /// for display settings APIs (EnumDisplaySettings, ChangeDisplaySettingsEx).
+ ///
+ public string GdiDeviceName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets monitor orientation (0=0, 1=90, 2=180, 3=270).
+ /// Fires PropertyChanged when value changes.
+ ///
+ public int Orientation
+ {
+ get => _orientation;
+ set
+ {
+ if (_orientation != value)
+ {
+ _orientation = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ int IMonitorData.MonitorNumber
+ {
+ get => MonitorNumber;
+ set => MonitorNumber = value;
+ }
+
+ ///
+ int IMonitorData.Orientation
+ {
+ get => Orientation;
+ set => Orientation = value;
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorCapabilities.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorCapabilities.cs
new file mode 100644
index 0000000000..e961f32038
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorCapabilities.cs
@@ -0,0 +1,52 @@
+// 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;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Monitor control capabilities flags
+ ///
+ [Flags]
+ public enum MonitorCapabilities
+ {
+ None = 0,
+
+ ///
+ /// Supports brightness control
+ ///
+ Brightness = 1 << 0,
+
+ ///
+ /// Supports contrast control
+ ///
+ Contrast = 1 << 1,
+
+ ///
+ /// Supports DDC/CI protocol
+ ///
+ DdcCi = 1 << 2,
+
+ ///
+ /// Supports WMI control
+ ///
+ Wmi = 1 << 3,
+
+ ///
+ /// Supports HDR
+ ///
+ Hdr = 1 << 4,
+
+ ///
+ /// Supports high-level monitor API
+ ///
+ HighLevel = 1 << 5,
+
+ ///
+ /// Supports volume control
+ ///
+ Volume = 1 << 6,
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorOperationResult.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorOperationResult.cs
new file mode 100644
index 0000000000..6905d7be44
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorOperationResult.cs
@@ -0,0 +1,58 @@
+// 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;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Monitor operation result
+ ///
+ public readonly struct MonitorOperationResult
+ {
+ ///
+ /// Gets a value indicating whether the operation was successful
+ ///
+ public bool IsSuccess { get; }
+
+ ///
+ /// Gets error message
+ ///
+ public string? ErrorMessage { get; }
+
+ ///
+ /// Gets system error code
+ ///
+ public int? ErrorCode { get; }
+
+ ///
+ /// Gets operation timestamp
+ ///
+ public DateTime Timestamp { get; }
+
+ private MonitorOperationResult(bool isSuccess, string? errorMessage = null, int? errorCode = null)
+ {
+ IsSuccess = isSuccess;
+ ErrorMessage = errorMessage;
+ ErrorCode = errorCode;
+ Timestamp = DateTime.Now;
+ }
+
+ ///
+ /// Creates a successful result
+ ///
+ public static MonitorOperationResult Success() => new(true);
+
+ ///
+ /// Creates a failed result
+ ///
+ public static MonitorOperationResult Failure(string errorMessage, int? errorCode = null)
+ => new(false, errorMessage, errorCode);
+
+ public override string ToString()
+ {
+ return IsSuccess ? "Success" : $"Failed: {ErrorMessage}";
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorStateEntry.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorStateEntry.cs
new file mode 100644
index 0000000000..8b1ade54a2
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorStateEntry.cs
@@ -0,0 +1,52 @@
+// 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.Text.Json.Serialization;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Individual monitor state entry for JSON persistence.
+ /// Stores the current state of a monitor's adjustable parameters.
+ ///
+ public sealed class MonitorStateEntry
+ {
+ ///
+ /// Gets or sets the brightness level (0-100).
+ ///
+ [JsonPropertyName("brightness")]
+ public int Brightness { get; set; }
+
+ ///
+ /// Gets or sets the color temperature VCP value.
+ ///
+ [JsonPropertyName("colorTemperature")]
+ public int ColorTemperatureVcp { get; set; }
+
+ ///
+ /// Gets or sets the contrast level (0-100).
+ ///
+ [JsonPropertyName("contrast")]
+ public int Contrast { get; set; }
+
+ ///
+ /// Gets or sets the volume level (0-100).
+ ///
+ [JsonPropertyName("volume")]
+ public int Volume { get; set; }
+
+ ///
+ /// Gets or sets the raw capabilities string from DDC/CI.
+ ///
+ [JsonPropertyName("capabilitiesRaw")]
+ public string? CapabilitiesRaw { get; set; }
+
+ ///
+ /// Gets or sets when this entry was last updated.
+ ///
+ [JsonPropertyName("lastUpdated")]
+ public DateTime LastUpdated { get; set; }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorStateFile.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorStateFile.cs
new file mode 100644
index 0000000000..e761503649
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/MonitorStateFile.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Monitor state file structure for JSON persistence.
+ /// Contains all monitor states indexed by Monitor.Id.
+ ///
+ public sealed class MonitorStateFile
+ {
+ ///
+ /// Gets or sets the monitor states dictionary.
+ /// Key is the monitor's unique Id (e.g., "DDC_GSM5C6D_1", "WMI_BOE0900_2").
+ ///
+ [JsonPropertyName("monitors")]
+ public Dictionary Monitors { get; set; } = new();
+
+ ///
+ /// Gets or sets when the file was last updated.
+ ///
+ [JsonPropertyName("lastUpdated")]
+ public DateTime LastUpdated { get; set; }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/PowerDisplayProfile.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/PowerDisplayProfile.cs
new file mode 100644
index 0000000000..8944569201
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/PowerDisplayProfile.cs
@@ -0,0 +1,60 @@
+// 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.Text.Json.Serialization;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Represents a PowerDisplay profile containing monitor settings
+ ///
+ public class PowerDisplayProfile
+ {
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ [JsonPropertyName("monitorSettings")]
+ public List MonitorSettings { get; set; }
+
+ [JsonPropertyName("createdDate")]
+ public DateTime CreatedDate { get; set; }
+
+ [JsonPropertyName("lastModified")]
+ public DateTime LastModified { get; set; }
+
+ public PowerDisplayProfile()
+ {
+ Name = string.Empty;
+ MonitorSettings = new List();
+ CreatedDate = DateTime.UtcNow;
+ LastModified = DateTime.UtcNow;
+ }
+
+ public PowerDisplayProfile(string name, List monitorSettings)
+ {
+ Name = name;
+ MonitorSettings = monitorSettings ?? new List();
+ CreatedDate = DateTime.UtcNow;
+ LastModified = DateTime.UtcNow;
+ }
+
+ ///
+ /// Validates that the profile has at least one monitor configured
+ ///
+ public bool IsValid()
+ {
+ return !string.IsNullOrWhiteSpace(Name) && MonitorSettings != null && MonitorSettings.Count > 0;
+ }
+
+ ///
+ /// Updates the last modified timestamp
+ ///
+ public void Touch()
+ {
+ LastModified = DateTime.UtcNow;
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/PowerDisplayProfiles.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/PowerDisplayProfiles.cs
new file mode 100644
index 0000000000..6813089943
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/PowerDisplayProfiles.cs
@@ -0,0 +1,100 @@
+// 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.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Serialization;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Container for all PowerDisplay profiles
+ ///
+ public class PowerDisplayProfiles
+ {
+ // NOTE: Custom profile concept has been removed. Profiles are now templates, not states.
+ // This constant is kept for backward compatibility (cleaning up legacy Custom profiles).
+ public const string CustomProfileName = "Custom";
+
+ [JsonPropertyName("profiles")]
+ public List Profiles { get; set; }
+
+ [JsonPropertyName("lastUpdated")]
+ public DateTime LastUpdated { get; set; }
+
+ public PowerDisplayProfiles()
+ {
+ Profiles = new List();
+ LastUpdated = DateTime.UtcNow;
+ }
+
+ ///
+ /// Gets the profile by name
+ ///
+ public PowerDisplayProfile? GetProfile(string name)
+ {
+ return Profiles.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+ }
+
+ ///
+ /// Adds or updates a profile
+ ///
+ public void SetProfile(PowerDisplayProfile profile)
+ {
+ if (profile == null || !profile.IsValid())
+ {
+ throw new ArgumentException("Profile is invalid");
+ }
+
+ var existing = GetProfile(profile.Name);
+ if (existing != null)
+ {
+ Profiles.Remove(existing);
+ }
+
+ profile.Touch();
+ Profiles.Add(profile);
+ LastUpdated = DateTime.UtcNow;
+ }
+
+ ///
+ /// Removes a profile by name
+ ///
+ public bool RemoveProfile(string name)
+ {
+ var profile = GetProfile(name);
+ if (profile != null)
+ {
+ Profiles.Remove(profile);
+ LastUpdated = DateTime.UtcNow;
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if a profile name is valid and available
+ ///
+ public bool IsNameAvailable(string name, string? excludeName = null)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return false;
+ }
+
+ // Check if name is already used (excluding the profile being renamed)
+ var existing = GetProfile(name);
+ if (existing != null && (excludeName == null || !existing.Name.Equals(excludeName, StringComparison.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/ProfileMonitorSetting.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/ProfileMonitorSetting.cs
new file mode 100644
index 0000000000..d346657d7c
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/ProfileMonitorSetting.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Monitor settings for a specific profile
+ ///
+ public class ProfileMonitorSetting
+ {
+ ///
+ /// Gets or sets the monitor's unique identifier.
+ /// Format: "{Source}_{EdidId}_{MonitorNumber}" (e.g., "DDC_GSM5C6D_1").
+ ///
+ [JsonPropertyName("monitorId")]
+ public string MonitorId { get; set; }
+
+ [JsonPropertyName("brightness")]
+ public int? Brightness { get; set; }
+
+ [JsonPropertyName("contrast")]
+ public int? Contrast { get; set; }
+
+ [JsonPropertyName("volume")]
+ public int? Volume { get; set; }
+
+ ///
+ /// Gets or sets the color temperature VCP preset value.
+ /// JSON property name kept as "colorTemperature" for backward compatibility.
+ ///
+ [JsonPropertyName("colorTemperature")]
+ public int? ColorTemperatureVcp { get; set; }
+
+ public ProfileMonitorSetting()
+ {
+ MonitorId = string.Empty;
+ }
+
+ public ProfileMonitorSetting(string monitorId, int? brightness = null, int? colorTemperatureVcp = null, int? contrast = null, int? volume = null)
+ {
+ MonitorId = monitorId;
+ Brightness = brightness;
+ ColorTemperatureVcp = colorTemperatureVcp;
+ Contrast = contrast;
+ Volume = volume;
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/VcpCapabilities.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/VcpCapabilities.cs
new file mode 100644
index 0000000000..c30eccefe2
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/VcpCapabilities.cs
@@ -0,0 +1,314 @@
+// 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;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// DDC/CI VCP capabilities information
+ ///
+ public class VcpCapabilities
+ {
+ ///
+ /// Gets or sets raw capabilities string (MCCS format)
+ ///
+ public string Raw { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets monitor model name from capabilities
+ ///
+ public string? Model { get; set; }
+
+ ///
+ /// Gets or sets monitor type from capabilities (e.g., "LCD")
+ ///
+ public string? Type { get; set; }
+
+ ///
+ /// Gets or sets mCCS protocol version
+ ///
+ public string? Protocol { get; set; }
+
+ ///
+ /// Gets or sets mCCS version (e.g., "2.2", "2.1")
+ ///
+ public string? MccsVersion { get; set; }
+
+ ///
+ /// Gets or sets supported command codes
+ ///
+ public List SupportedCommands { get; set; } = new();
+
+ ///
+ /// Gets or sets supported VCP codes with their information
+ ///
+ public Dictionary SupportedVcpCodes { get; set; } = new();
+
+ ///
+ /// Gets or sets window capabilities for PIP/PBP support
+ ///
+ public List Windows { get; set; } = new();
+
+ ///
+ /// Gets a value indicating whether check if display supports PIP/PBP windows
+ ///
+ public bool HasWindowSupport => Windows.Count > 0;
+
+ ///
+ /// Check if a specific VCP code is supported
+ ///
+ public bool SupportsVcpCode(byte code) => SupportedVcpCodes.ContainsKey(code);
+
+ ///
+ /// Get VCP code information
+ ///
+ public VcpCodeInfo? GetVcpCodeInfo(byte code)
+ {
+ return SupportedVcpCodes.TryGetValue(code, out var info) ? info : null;
+ }
+
+ ///
+ /// Check if a VCP code supports discrete values
+ ///
+ public bool HasDiscreteValues(byte code)
+ {
+ var info = GetVcpCodeInfo(code);
+ return info?.HasDiscreteValues ?? false;
+ }
+
+ ///
+ /// Get supported values for a VCP code
+ ///
+ public IReadOnlyList? GetSupportedValues(byte code)
+ {
+ return GetVcpCodeInfo(code)?.SupportedValues;
+ }
+
+ ///
+ /// Get all VCP codes as hex strings, sorted by code value.
+ ///
+ /// List of hex strings like ["0x10", "0x12", "0x14"]
+ public List GetVcpCodesAsHexStrings()
+ {
+ var result = new List(SupportedVcpCodes.Count);
+ foreach (var kvp in SupportedVcpCodes)
+ {
+ result.Add($"0x{kvp.Key:X2}");
+ }
+
+ result.Sort(StringComparer.Ordinal);
+ return result;
+ }
+
+ ///
+ /// Get all VCP codes sorted by code value.
+ ///
+ /// Sorted list of VcpCodeInfo
+ public IEnumerable GetSortedVcpCodes()
+ {
+ var sortedKeys = new List(SupportedVcpCodes.Keys);
+ sortedKeys.Sort();
+
+ foreach (var key in sortedKeys)
+ {
+ yield return SupportedVcpCodes[key];
+ }
+ }
+
+ ///
+ /// Gets creates an empty capabilities object
+ ///
+ public static VcpCapabilities Empty => new();
+
+ public override string ToString()
+ {
+ return $"Model: {Model}, VCP Codes: {SupportedVcpCodes.Count}";
+ }
+ }
+
+ ///
+ /// Information about a single VCP code
+ ///
+ public readonly struct VcpCodeInfo
+ {
+ ///
+ /// Gets vCP code (e.g., 0x10 for brightness)
+ ///
+ public byte Code { get; }
+
+ ///
+ /// Gets human-readable name of the VCP code
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets supported discrete values (empty if continuous range)
+ ///
+ public IReadOnlyList SupportedValues { get; }
+
+ ///
+ /// Gets a value indicating whether this VCP code has discrete values
+ ///
+ public bool HasDiscreteValues => SupportedValues.Count > 0;
+
+ ///
+ /// Gets a value indicating whether this VCP code supports a continuous range
+ ///
+ public bool IsContinuous => SupportedValues.Count == 0;
+
+ ///
+ /// Gets the VCP code formatted as a hex string (e.g., "0x10").
+ ///
+ public string FormattedCode => $"0x{Code:X2}";
+
+ ///
+ /// Gets the VCP code formatted with its name (e.g., "Brightness (0x10)").
+ ///
+ public string FormattedTitle => $"{Name} ({FormattedCode})";
+
+ public VcpCodeInfo(byte code, string name, IReadOnlyList? supportedValues = null)
+ {
+ Code = code;
+ Name = name;
+ SupportedValues = supportedValues ?? Array.Empty();
+ }
+
+ public override string ToString()
+ {
+ if (HasDiscreteValues)
+ {
+ return $"0x{Code:X2} ({Name}): {string.Join(", ", SupportedValues)}";
+ }
+
+ return $"0x{Code:X2} ({Name}): Continuous";
+ }
+ }
+
+ ///
+ /// Window size (width and height)
+ ///
+ public readonly struct WindowSize
+ {
+ ///
+ /// Gets width in pixels
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets height in pixels
+ ///
+ public int Height { get; }
+
+ public WindowSize(int width, int height)
+ {
+ Width = width;
+ Height = height;
+ }
+
+ public override string ToString() => $"{Width}x{Height}";
+ }
+
+ ///
+ /// Window area coordinates (top-left and bottom-right)
+ ///
+ public readonly struct WindowArea
+ {
+ ///
+ /// Gets top-left X coordinate
+ ///
+ public int X1 { get; }
+
+ ///
+ /// Gets top-left Y coordinate
+ ///
+ public int Y1 { get; }
+
+ ///
+ /// Gets bottom-right X coordinate
+ ///
+ public int X2 { get; }
+
+ ///
+ /// Gets bottom-right Y coordinate
+ ///
+ public int Y2 { get; }
+
+ ///
+ /// Gets width of the area
+ ///
+ public int Width => X2 - X1;
+
+ ///
+ /// Gets height of the area
+ ///
+ public int Height => Y2 - Y1;
+
+ public WindowArea(int x1, int y1, int x2, int y2)
+ {
+ X1 = x1;
+ Y1 = y1;
+ X2 = x2;
+ Y2 = y2;
+ }
+
+ public override string ToString() => $"({X1},{Y1})-({X2},{Y2})";
+ }
+
+ ///
+ /// Window capability information for PIP/PBP displays
+ ///
+ public readonly struct WindowCapability
+ {
+ ///
+ /// Gets window number (1, 2, 3, etc.)
+ ///
+ public int WindowNumber { get; }
+
+ ///
+ /// Gets window type (e.g., "PIP", "PBP")
+ ///
+ public string Type { get; }
+
+ ///
+ /// Gets window area coordinates
+ ///
+ public WindowArea Area { get; }
+
+ ///
+ /// Gets maximum window size
+ ///
+ public WindowSize MaxSize { get; }
+
+ ///
+ /// Gets minimum window size
+ ///
+ public WindowSize MinSize { get; }
+
+ ///
+ /// Gets window identifier
+ ///
+ public int WindowId { get; }
+
+ public WindowCapability(
+ int windowNumber,
+ string type,
+ WindowArea area,
+ WindowSize maxSize,
+ WindowSize minSize,
+ int windowId)
+ {
+ WindowNumber = windowNumber;
+ Type = type ?? string.Empty;
+ Area = area;
+ MaxSize = maxSize;
+ MinSize = minSize;
+ WindowId = windowId;
+ }
+
+ public override string ToString() =>
+ $"Window{WindowNumber}: Type={Type}, Area={Area}, Max={MaxSize}, Min={MinSize}";
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/VcpFeatureValue.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/VcpFeatureValue.cs
new file mode 100644
index 0000000000..64c4b2d801
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/VcpFeatureValue.cs
@@ -0,0 +1,77 @@
+// 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;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// VCP feature value information structure.
+ /// Represents the current, minimum, and maximum values for a VCP (Virtual Control Panel) feature.
+ ///
+ public readonly struct VcpFeatureValue
+ {
+ ///
+ /// Gets current value
+ ///
+ public int Current { get; }
+
+ ///
+ /// Gets minimum value
+ ///
+ public int Minimum { get; }
+
+ ///
+ /// Gets maximum value
+ ///
+ public int Maximum { get; }
+
+ ///
+ /// Gets a value indicating whether the value information is valid
+ ///
+ public bool IsValid { get; }
+
+ ///
+ /// Gets timestamp when the value information was obtained
+ ///
+ public DateTime Timestamp { get; }
+
+ public VcpFeatureValue(int current, int minimum, int maximum)
+ {
+ Current = current;
+ Minimum = minimum;
+ Maximum = maximum;
+ IsValid = current >= minimum && current <= maximum && maximum > minimum;
+ Timestamp = DateTime.Now;
+ }
+
+ public VcpFeatureValue(int current, int maximum)
+ : this(current, 0, maximum)
+ {
+ }
+
+ ///
+ /// Gets creates invalid value information
+ ///
+ public static VcpFeatureValue Invalid => new(-1, -1, -1);
+
+ ///
+ /// Converts value to percentage (0-100)
+ ///
+ public int ToPercentage()
+ {
+ if (!IsValid || Maximum == Minimum)
+ {
+ return 0;
+ }
+
+ return (int)Math.Round((double)(Current - Minimum) * 100 / (Maximum - Minimum));
+ }
+
+ public override string ToString()
+ {
+ return IsValid ? $"{Current}/{Maximum} ({ToPercentage()}%)" : "Invalid";
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/NativeMethods.json b/src/modules/powerdisplay/PowerDisplay.Lib/NativeMethods.json
new file mode 100644
index 0000000000..450ecacafd
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/NativeMethods.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://aka.ms/CsWin32.schema.json",
+ "public": true,
+ "allowMarshaling": false
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/NativeMethods.txt b/src/modules/powerdisplay/PowerDisplay.Lib/NativeMethods.txt
new file mode 100644
index 0000000000..769c001fed
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/NativeMethods.txt
@@ -0,0 +1,9 @@
+// Structs and constants only - functions use LibraryImport for AOT compatibility
+// CsWin32 generates blittable types when allowMarshaling: false
+
+// Note: All structs are manually defined in NativeStructures.cs with proper blittable layouts
+// This file is intentionally left with only LUID as a minimal test
+// Full DISPLAYCONFIG_* types need helper methods like GetViewGdiDeviceName() which CsWin32 doesn't provide
+
+// Only request LUID from CsWin32 (other types manually defined in NativeStructures.cs)
+LUID
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/PathConstants.cs b/src/modules/powerdisplay/PowerDisplay.Lib/PathConstants.cs
new file mode 100644
index 0000000000..4dd92d9666
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/PathConstants.cs
@@ -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.IO;
+
+namespace PowerDisplay.Common
+{
+ ///
+ /// Centralized path constants for PowerDisplay module.
+ /// Provides unified access to all file and folder paths used by PowerDisplay and related integrations.
+ ///
+ public static class PathConstants
+ {
+ private static readonly Lazy _localAppDataPath = new Lazy(
+ () => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
+
+ private static readonly Lazy _powerToysBasePath = new Lazy(
+ () => Path.Combine(_localAppDataPath.Value, "Microsoft", "PowerToys"));
+
+ ///
+ /// Gets the base PowerToys settings folder path.
+ /// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys
+ ///
+ public static string PowerToysBasePath => _powerToysBasePath.Value;
+
+ ///
+ /// Gets the PowerDisplay module folder path.
+ /// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay
+ ///
+ public static string PowerDisplayFolderPath => Path.Combine(PowerToysBasePath, "PowerDisplay");
+
+ ///
+ /// Gets the PowerDisplay profiles file path.
+ /// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay\profiles.json
+ ///
+ public static string ProfilesFilePath => Path.Combine(PowerDisplayFolderPath, ProfilesFileName);
+
+ ///
+ /// Gets the PowerDisplay settings file path.
+ /// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay\settings.json
+ ///
+ public static string SettingsFilePath => Path.Combine(PowerDisplayFolderPath, SettingsFileName);
+
+ ///
+ /// Gets the LightSwitch module folder path.
+ /// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\LightSwitch
+ ///
+ public static string LightSwitchFolderPath => Path.Combine(PowerToysBasePath, "LightSwitch");
+
+ ///
+ /// Gets the LightSwitch settings file path.
+ /// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\LightSwitch\settings.json
+ ///
+ public static string LightSwitchSettingsFilePath => Path.Combine(LightSwitchFolderPath, SettingsFileName);
+
+ ///
+ /// The name of the profiles file.
+ ///
+ public const string ProfilesFileName = "profiles.json";
+
+ ///
+ /// The name of the settings file.
+ ///
+ public const string SettingsFileName = "settings.json";
+
+ ///
+ /// The name of the monitor state file.
+ ///
+ public const string MonitorStateFileName = "monitor_state.json";
+
+ ///
+ /// Gets the monitor state file path.
+ /// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay\monitor_state.json
+ ///
+ public static string MonitorStateFilePath => Path.Combine(PowerDisplayFolderPath, MonitorStateFileName);
+
+ ///
+ /// Event name for LightSwitch light theme change notifications.
+ /// Signaled when LightSwitch switches to light mode.
+ /// Must match CommonSharedConstants::LIGHT_SWITCH_LIGHT_THEME_EVENT in shared_constants.h.
+ ///
+ public const string LightSwitchLightThemeEventName = "Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca";
+
+ ///
+ /// Event name for LightSwitch dark theme change notifications.
+ /// Signaled when LightSwitch switches to dark mode.
+ /// Must match CommonSharedConstants::LIGHT_SWITCH_DARK_THEME_EVENT in shared_constants.h.
+ ///
+ public const string LightSwitchDarkThemeEventName = "Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368";
+
+ ///
+ /// Ensures the PowerDisplay folder exists. Creates it if necessary.
+ ///
+ /// The PowerDisplay folder path
+ public static string EnsurePowerDisplayFolderExists()
+ => EnsureFolderExists(PowerDisplayFolderPath);
+
+ ///
+ /// Ensures the LightSwitch folder exists. Creates it if necessary.
+ ///
+ /// The LightSwitch folder path
+ public static string EnsureLightSwitchFolderExists()
+ => EnsureFolderExists(LightSwitchFolderPath);
+
+ ///
+ /// Ensures the specified folder exists. Creates it if necessary.
+ ///
+ /// The folder path to ensure exists
+ /// The folder path
+ private static string EnsureFolderExists(string folderPath)
+ {
+ if (!Directory.Exists(folderPath))
+ {
+ Directory.CreateDirectory(folderPath);
+ }
+
+ return folderPath;
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj b/src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj
new file mode 100644
index 0000000000..e9f8cd3f05
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+ Library
+ PowerDisplay.Common
+ x64;ARM64
+ false
+ false
+ true
+ enable
+ PowerDisplay.Lib
+
+
+
+ false
+ false
+ true
+ false
+ false
+
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Serialization/ProfileSerializationContext.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Serialization/ProfileSerializationContext.cs
new file mode 100644
index 0000000000..198829f93e
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Serialization/ProfileSerializationContext.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using PowerDisplay.Common.Models;
+
+namespace PowerDisplay.Common.Serialization
+{
+ ///
+ /// JSON serialization context for PowerDisplay Profile types.
+ /// Provides source-generated serialization for Native AOT compatibility.
+ ///
+ [JsonSourceGenerationOptions(
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.Never,
+ IncludeFields = true)]
+
+ // Profile Types
+ [JsonSerializable(typeof(ProfileMonitorSetting))]
+ [JsonSerializable(typeof(List))]
+ [JsonSerializable(typeof(PowerDisplayProfile))]
+ [JsonSerializable(typeof(List))]
+ [JsonSerializable(typeof(PowerDisplayProfiles))]
+
+ // Monitor State Types
+ [JsonSerializable(typeof(MonitorStateEntry))]
+ [JsonSerializable(typeof(MonitorStateFile))]
+ [JsonSerializable(typeof(Dictionary))]
+ public partial class ProfileSerializationContext : JsonSerializerContext
+ {
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Services/DisplayRotationService.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Services/DisplayRotationService.cs
new file mode 100644
index 0000000000..2b3c6bc31d
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Services/DisplayRotationService.cs
@@ -0,0 +1,171 @@
+// 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 ManagedCommon;
+using PowerDisplay.Common.Models;
+using static PowerDisplay.Common.Drivers.NativeConstants;
+using static PowerDisplay.Common.Drivers.PInvoke;
+
+using DevMode = PowerDisplay.Common.Drivers.DevMode;
+
+namespace PowerDisplay.Common.Services
+{
+ ///
+ /// Service for controlling display rotation/orientation.
+ /// Uses ChangeDisplaySettingsEx API to change display orientation.
+ ///
+ public class DisplayRotationService
+ {
+ ///
+ /// Set display rotation for a specific monitor.
+ /// Uses GdiDeviceName from the Monitor object for accurate adapter targeting.
+ ///
+ /// Monitor object with GdiDeviceName
+ /// New orientation: 0=normal, 1=90°, 2=180°, 3=270°
+ /// Operation result
+ public MonitorOperationResult SetRotation(Monitor monitor, int newOrientation)
+ {
+ ArgumentNullException.ThrowIfNull(monitor);
+
+ if (newOrientation < 0 || newOrientation > 3)
+ {
+ return MonitorOperationResult.Failure($"Invalid orientation value: {newOrientation}. Must be 0-3.");
+ }
+
+ if (string.IsNullOrEmpty(monitor.GdiDeviceName))
+ {
+ return MonitorOperationResult.Failure("Monitor has no GdiDeviceName");
+ }
+
+ return SetRotationByGdiDeviceName(monitor.GdiDeviceName, newOrientation);
+ }
+
+ ///
+ /// Set display rotation by GDI device name.
+ ///
+ /// GDI device name (e.g., "\\.\DISPLAY1")
+ /// New orientation: 0=normal, 1=90°, 2=180°, 3=270°
+ /// Operation result
+ public unsafe MonitorOperationResult SetRotationByGdiDeviceName(string gdiDeviceName, int newOrientation)
+ {
+ if (string.IsNullOrEmpty(gdiDeviceName))
+ {
+ return MonitorOperationResult.Failure("GDI device name is required");
+ }
+
+ try
+ {
+ // 1. Get current display settings
+ DevMode devMode = default;
+ devMode.DmSize = (short)sizeof(DevMode);
+
+ if (!EnumDisplaySettings(gdiDeviceName, EnumCurrentSettings, &devMode))
+ {
+ var error = GetLastError();
+ Logger.LogError($"SetRotation: EnumDisplaySettings failed for {gdiDeviceName}, error: {error}");
+ return MonitorOperationResult.Failure($"Failed to get current display settings for {gdiDeviceName}", (int)error);
+ }
+
+ int currentOrientation = devMode.DmDisplayOrientation;
+
+ // If already at target orientation, return success
+ if (currentOrientation == newOrientation)
+ {
+ return MonitorOperationResult.Success();
+ }
+
+ // 2. Determine if we need to swap width and height
+ // When switching between landscape (0°/180°) and portrait (90°/270°), swap dimensions
+ bool currentIsLandscape = currentOrientation == DmdoDefault || currentOrientation == Dmdo180;
+ bool newIsLandscape = newOrientation == DmdoDefault || newOrientation == Dmdo180;
+
+ if (currentIsLandscape != newIsLandscape)
+ {
+ // Swap width and height
+ int temp = devMode.DmPelsWidth;
+ devMode.DmPelsWidth = devMode.DmPelsHeight;
+ devMode.DmPelsHeight = temp;
+ }
+
+ // 3. Set new orientation
+ devMode.DmDisplayOrientation = newOrientation;
+ devMode.DmFields = DmDisplayOrientation | DmPelsWidth | DmPelsHeight;
+
+ // 4. Test the settings first using CDS_TEST flag
+ int testResult = ChangeDisplaySettingsEx(gdiDeviceName, &devMode, IntPtr.Zero, CdsTest, IntPtr.Zero);
+ if (testResult != DispChangeSuccessful)
+ {
+ string errorMsg = GetChangeDisplaySettingsErrorMessage(testResult);
+ Logger.LogError($"SetRotation: Test failed for {gdiDeviceName}: {errorMsg}");
+ return MonitorOperationResult.Failure($"Display settings test failed: {errorMsg}", testResult);
+ }
+
+ // 5. Apply the settings (without CDS_UPDATEREGISTRY to make it temporary)
+ int result = ChangeDisplaySettingsEx(gdiDeviceName, &devMode, IntPtr.Zero, 0, IntPtr.Zero);
+ if (result != DispChangeSuccessful)
+ {
+ string errorMsg = GetChangeDisplaySettingsErrorMessage(result);
+ Logger.LogError($"SetRotation: Apply failed for {gdiDeviceName}: {errorMsg}");
+ return MonitorOperationResult.Failure($"Failed to apply display settings: {errorMsg}", result);
+ }
+
+ return MonitorOperationResult.Success();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"SetRotation: Exception for {gdiDeviceName}: {ex.Message}");
+ return MonitorOperationResult.Failure($"Exception while setting rotation: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Get current orientation for a GDI device name.
+ ///
+ /// GDI device name (e.g., "\\.\DISPLAY1")
+ /// Current orientation (0-3), or -1 if query failed
+ public unsafe int GetCurrentOrientation(string gdiDeviceName)
+ {
+ if (string.IsNullOrEmpty(gdiDeviceName))
+ {
+ return -1;
+ }
+
+ try
+ {
+ DevMode devMode = default;
+ devMode.DmSize = (short)sizeof(DevMode);
+
+ if (!EnumDisplaySettings(gdiDeviceName, EnumCurrentSettings, &devMode))
+ {
+ return -1;
+ }
+
+ return devMode.DmDisplayOrientation;
+ }
+ catch
+ {
+ return -1;
+ }
+ }
+
+ ///
+ /// Get human-readable error message for ChangeDisplaySettings result code.
+ ///
+ private static string GetChangeDisplaySettingsErrorMessage(int resultCode)
+ {
+ return resultCode switch
+ {
+ DispChangeSuccessful => "Success",
+ DispChangeRestart => "Computer must be restarted",
+ DispChangeFailed => "Display driver failed the specified graphics mode",
+ DispChangeBadmode => "Graphics mode is not supported",
+ DispChangeNotupdated => "Unable to write settings to registry",
+ DispChangeBadflags => "Invalid flags",
+ DispChangeBadparam => "Invalid parameter",
+ _ => $"Unknown error code: {resultCode}",
+ };
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Services/MonitorStateManager.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Services/MonitorStateManager.cs
new file mode 100644
index 0000000000..807102cffb
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Services/MonitorStateManager.cs
@@ -0,0 +1,289 @@
+// 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.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using ManagedCommon;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Common.Serialization;
+using PowerDisplay.Common.Utils;
+
+namespace PowerDisplay.Common.Services
+{
+ ///
+ /// Manages monitor parameter state in a separate file from main settings.
+ /// This avoids FileSystemWatcher feedback loops by separating read-only config (settings.json)
+ /// from frequently updated state (monitor_state.json).
+ /// Simplified to use direct save strategy for reliability and simplicity (KISS principle).
+ ///
+ public partial class MonitorStateManager : IDisposable
+ {
+ private readonly string _stateFilePath;
+ private readonly ConcurrentDictionary _states = new();
+ private readonly SimpleDebouncer _saveDebouncer;
+
+ private bool _disposed;
+ private bool _isDirty; // Track pending changes for flush on dispose
+ private const int SaveDebounceMs = 2000; // Save 2 seconds after last update
+
+ ///
+ /// Monitor state data (internal tracking, not serialized)
+ ///
+ private sealed class MonitorState
+ {
+ public int Brightness { get; set; }
+
+ public int ColorTemperatureVcp { get; set; }
+
+ public int Contrast { get; set; }
+
+ public int Volume { get; set; }
+
+ public string? CapabilitiesRaw { get; set; }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Uses PathConstants for consistent path management.
+ ///
+ public MonitorStateManager()
+ {
+ // Use PathConstants for consistent path management
+ PathConstants.EnsurePowerDisplayFolderExists();
+ _stateFilePath = PathConstants.MonitorStateFilePath;
+
+ // Initialize debouncer for batching rapid updates (e.g., slider drag)
+ _saveDebouncer = new SimpleDebouncer(SaveDebounceMs);
+
+ // Load existing state if available
+ LoadStateFromDisk();
+ }
+
+ ///
+ /// Update monitor parameter and schedule debounced save to disk.
+ /// Uses Monitor.Id as the stable key (e.g., "DDC_GSM5C6D_1", "WMI_BOE0900_2").
+ /// Debounced-save strategy reduces disk I/O by batching rapid updates (e.g., during slider drag).
+ ///
+ /// The monitor's unique Id (e.g., "DDC_GSM5C6D_1").
+ /// The property name to update (Brightness, ColorTemperature, Contrast, or Volume).
+ /// The new value.
+ public void UpdateMonitorParameter(string monitorId, string property, int value)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(monitorId))
+ {
+ Logger.LogWarning($"Cannot update monitor parameter: monitorId is empty");
+ return;
+ }
+
+ var state = _states.GetOrAdd(monitorId, _ => new MonitorState());
+
+ // Update the specific property
+ bool shouldSave = true;
+ switch (property)
+ {
+ case "Brightness":
+ state.Brightness = value;
+ break;
+ case "ColorTemperature":
+ state.ColorTemperatureVcp = value;
+ break;
+ case "Contrast":
+ state.Contrast = value;
+ break;
+ case "Volume":
+ state.Volume = value;
+ break;
+ default:
+ Logger.LogWarning($"Unknown property: {property}");
+ shouldSave = false;
+ break;
+ }
+
+ if (shouldSave)
+ {
+ // Mark dirty for flush on dispose
+ _isDirty = true;
+ }
+
+ // Schedule debounced save (SimpleDebouncer handles cancellation of previous calls)
+ if (shouldSave)
+ {
+ _saveDebouncer.Debounce(SaveStateToDiskAsync);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to update monitor parameter: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Get saved parameters for a monitor using Monitor.Id.
+ ///
+ /// The monitor's unique Id (e.g., "DDC_GSM5C6D_1").
+ /// A tuple of (Brightness, ColorTemperatureVcp, Contrast, Volume) or null if not found.
+ public (int Brightness, int ColorTemperatureVcp, int Contrast, int Volume)? GetMonitorParameters(string monitorId)
+ {
+ if (string.IsNullOrEmpty(monitorId))
+ {
+ return null;
+ }
+
+ if (_states.TryGetValue(monitorId, out var state))
+ {
+ return (state.Brightness, state.ColorTemperatureVcp, state.Contrast, state.Volume);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Load state from disk.
+ ///
+ private void LoadStateFromDisk()
+ {
+ try
+ {
+ if (!File.Exists(_stateFilePath))
+ {
+ return;
+ }
+
+ var json = File.ReadAllText(_stateFilePath);
+ var stateFile = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.MonitorStateFile);
+
+ if (stateFile?.Monitors != null)
+ {
+ foreach (var kvp in stateFile.Monitors)
+ {
+ var monitorKey = kvp.Key; // Should be MonitorId (e.g., "GSM5C6D")
+ var entry = kvp.Value;
+
+ _states[monitorKey] = new MonitorState
+ {
+ Brightness = entry.Brightness,
+ ColorTemperatureVcp = entry.ColorTemperatureVcp,
+ Contrast = entry.Contrast,
+ Volume = entry.Volume,
+ CapabilitiesRaw = entry.CapabilitiesRaw,
+ };
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to load monitor state: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Save current state to disk immediately (async).
+ /// Called by timer after debounce period.
+ ///
+ private async Task SaveStateToDiskAsync()
+ {
+ try
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ var json = BuildStateJson();
+
+ // Write to disk asynchronously
+ await File.WriteAllTextAsync(_stateFilePath, json);
+
+ // Clear dirty flag after successful save
+ _isDirty = false;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to save monitor state: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Save current state to disk synchronously.
+ /// Called during Dispose to flush pending changes without risk of deadlock.
+ ///
+ private void SaveStateToDiskSync()
+ {
+ try
+ {
+ var json = BuildStateJson();
+
+ // Write to disk synchronously - safe for Dispose
+ File.WriteAllText(_stateFilePath, json);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to save monitor state (sync): {ex.Message}");
+ }
+ }
+
+ ///
+ /// Build the JSON string for state file.
+ /// Shared logic between async and sync save methods.
+ ///
+ /// JSON string for state file
+ private string BuildStateJson()
+ {
+ var now = DateTime.Now;
+ var stateFile = new MonitorStateFile
+ {
+ LastUpdated = now,
+ };
+
+ foreach (var kvp in _states)
+ {
+ var monitorId = kvp.Key;
+ var state = kvp.Value;
+
+ stateFile.Monitors[monitorId] = new MonitorStateEntry
+ {
+ Brightness = state.Brightness,
+ ColorTemperatureVcp = state.ColorTemperatureVcp,
+ Contrast = state.Contrast,
+ Volume = state.Volume,
+ CapabilitiesRaw = state.CapabilitiesRaw,
+ LastUpdated = now,
+ };
+ }
+
+ return JsonSerializer.Serialize(stateFile, ProfileSerializationContext.Default.MonitorStateFile);
+ }
+
+ ///
+ /// Disposes the MonitorStateManager, flushing any pending state changes.
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ bool wasDirty = _isDirty;
+ _disposed = true;
+ _isDirty = false;
+
+ // Dispose debouncer first to cancel any pending saves
+ _saveDebouncer?.Dispose();
+
+ // Flush any pending changes before disposing using sync method to avoid deadlock
+ if (wasDirty)
+ {
+ SaveStateToDiskSync();
+ }
+
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Services/ProfileService.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Services/ProfileService.cs
new file mode 100644
index 0000000000..094cb554fb
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Services/ProfileService.cs
@@ -0,0 +1,250 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Text.Json;
+using ManagedCommon;
+using PowerDisplay.Common.Interfaces;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Common.Serialization;
+
+namespace PowerDisplay.Common.Services
+{
+ ///
+ /// Centralized service for managing PowerDisplay profiles storage and retrieval.
+ /// Provides unified access to profile data for PowerDisplay, Settings UI, and LightSwitch modules.
+ /// Thread-safe and AOT-compatible.
+ ///
+ public class ProfileService : IProfileService
+ {
+ private const string LogPrefix = "[ProfileService]";
+ private static readonly object _lock = new object();
+
+ ///
+ /// Gets the singleton instance of the ProfileService.
+ /// Use this for dependency injection or when interface-based access is needed.
+ ///
+ public static IProfileService Instance { get; } = new ProfileService();
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Private constructor to enforce singleton pattern for instance-based access.
+ /// Static methods remain available for backward compatibility.
+ ///
+ private ProfileService()
+ {
+ }
+
+ ///
+ /// Loads PowerDisplay profiles from disk.
+ /// Thread-safe operation with automatic legacy profile cleanup.
+ ///
+ /// PowerDisplayProfiles object, or a new empty instance if file doesn't exist or load fails
+ public static PowerDisplayProfiles LoadProfiles()
+ {
+ lock (_lock)
+ {
+ var (profiles, _) = LoadProfilesInternal();
+ return profiles;
+ }
+ }
+
+ ///
+ /// Saves PowerDisplay profiles to disk.
+ /// Thread-safe operation with automatic timestamp update and legacy profile cleanup.
+ ///
+ /// The profiles collection to save
+ /// True if save was successful, false otherwise
+ public static bool SaveProfiles(PowerDisplayProfiles profiles)
+ {
+ lock (_lock)
+ {
+ if (profiles == null)
+ {
+ Logger.LogWarning($"{LogPrefix} Cannot save null profiles");
+ return false;
+ }
+
+ var (success, _) = SaveProfilesInternal(profiles);
+ return success;
+ }
+ }
+
+ ///
+ /// Adds or updates a profile in the collection and persists to disk.
+ /// Thread-safe operation.
+ ///
+ /// The profile to add or update
+ /// True if operation was successful, false otherwise
+ public static bool AddOrUpdateProfile(PowerDisplayProfile profile)
+ {
+ lock (_lock)
+ {
+ if (profile == null || !profile.IsValid())
+ {
+ Logger.LogWarning($"{LogPrefix} Cannot add invalid profile");
+ return false;
+ }
+
+ var (profiles, _) = LoadProfilesInternal();
+ profiles.SetProfile(profile);
+
+ var (success, _) = SaveProfilesInternal(profiles);
+ return success;
+ }
+ }
+
+ ///
+ /// Removes a profile by name and persists to disk.
+ /// Thread-safe operation.
+ ///
+ /// The name of the profile to remove
+ /// True if profile was found and removed, false otherwise
+ public static bool RemoveProfile(string profileName)
+ {
+ lock (_lock)
+ {
+ var (profiles, _) = LoadProfilesInternal();
+ bool removed = profiles.RemoveProfile(profileName);
+
+ if (removed)
+ {
+ SaveProfilesInternal(profiles);
+ }
+
+ return removed;
+ }
+ }
+
+ ///
+ /// Gets a profile by name.
+ /// Thread-safe operation.
+ ///
+ /// The name of the profile to retrieve
+ /// The profile if found, null otherwise
+ public static PowerDisplayProfile? GetProfile(string profileName)
+ {
+ lock (_lock)
+ {
+ var (profiles, _) = LoadProfilesInternal();
+ return profiles.GetProfile(profileName);
+ }
+ }
+
+ ///
+ /// Checks if the profiles file exists.
+ ///
+ /// True if profiles file exists, false otherwise
+ public static bool ProfilesFileExists()
+ {
+ try
+ {
+ return File.Exists(PathConstants.ProfilesFilePath);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"{LogPrefix} Error checking if profiles file exists: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Gets the path to the profiles file.
+ ///
+ /// The full path to the profiles file
+ public static string GetProfilesFilePath()
+ {
+ return PathConstants.ProfilesFilePath;
+ }
+
+ // Internal methods without lock for use within already-locked contexts
+ // Returns tuple with result and optional log message
+ private static (PowerDisplayProfiles Profiles, string? Message) LoadProfilesInternal()
+ {
+ try
+ {
+ var filePath = PathConstants.ProfilesFilePath;
+
+ PathConstants.EnsurePowerDisplayFolderExists();
+
+ if (File.Exists(filePath))
+ {
+ var json = File.ReadAllText(filePath);
+ var profiles = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.PowerDisplayProfiles);
+
+ if (profiles != null)
+ {
+ profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
+ return (profiles, $"Loaded {profiles.Profiles.Count} profiles from {filePath}");
+ }
+ }
+ else
+ {
+ return (new PowerDisplayProfiles(), $"No profiles file found at {filePath}, returning empty collection");
+ }
+
+ return (new PowerDisplayProfiles(), null);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"{LogPrefix} Failed to load profiles: {ex.Message}");
+ return (new PowerDisplayProfiles(), null);
+ }
+ }
+
+ // Returns tuple with success status and optional log message
+ private static (bool Success, string? Message) SaveProfilesInternal(PowerDisplayProfiles profiles)
+ {
+ try
+ {
+ if (profiles == null)
+ {
+ return (false, null);
+ }
+
+ PathConstants.EnsurePowerDisplayFolderExists();
+
+ profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
+ profiles.LastUpdated = DateTime.UtcNow;
+
+ var json = JsonSerializer.Serialize(profiles, ProfileSerializationContext.Default.PowerDisplayProfiles);
+ var filePath = PathConstants.ProfilesFilePath;
+ File.WriteAllText(filePath, json);
+
+ return (true, $"Saved {profiles.Profiles.Count} profiles to {filePath}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"{LogPrefix} Failed to save profiles: {ex.Message}");
+ return (false, null);
+ }
+ }
+
+ // IProfileService Implementation
+ // Explicit interface implementation to satisfy IProfileService
+ // These methods delegate to the static methods for backward compatibility
+
+ ///
+ PowerDisplayProfiles IProfileService.LoadProfiles() => LoadProfiles();
+
+ ///
+ bool IProfileService.SaveProfiles(PowerDisplayProfiles profiles) => SaveProfiles(profiles);
+
+ ///
+ bool IProfileService.AddOrUpdateProfile(PowerDisplayProfile profile) => AddOrUpdateProfile(profile);
+
+ ///
+ bool IProfileService.RemoveProfile(string profileName) => RemoveProfile(profileName);
+
+ ///
+ PowerDisplayProfile? IProfileService.GetProfile(string profileName) => GetProfile(profileName);
+
+ ///
+ bool IProfileService.ProfilesFileExists() => ProfilesFileExists();
+
+ ///
+ string IProfileService.GetProfilesFilePath() => GetProfilesFilePath();
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/ColorTemperatureHelper.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/ColorTemperatureHelper.cs
new file mode 100644
index 0000000000..43dc8c044f
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/ColorTemperatureHelper.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Linq;
+using PowerDisplay.Common.Drivers;
+using PowerDisplay.Common.Models;
+
+namespace PowerDisplay.Common.Utils
+{
+ ///
+ /// Helper class for color temperature preset computation.
+ /// Provides shared logic for computing available color presets from VCP capabilities.
+ ///
+ public static class ColorTemperatureHelper
+ {
+ ///
+ /// Computes available color temperature presets from VCP value data.
+ ///
+ ///
+ /// Collection of tuples containing (VcpValue, Name) for each color temperature preset.
+ /// The VcpValue is the VCP value, Name is the name from capabilities string if available.
+ ///
+ /// Sorted list of ColorPresetItem objects.
+ public static List ComputeColorPresets(IEnumerable<(int VcpValue, string? Name)> colorTemperatureValues)
+ {
+ if (colorTemperatureValues == null)
+ {
+ return new List();
+ }
+
+ var presetList = new List();
+
+ foreach (var item in colorTemperatureValues)
+ {
+ var displayName = FormatColorTemperatureDisplayName(item.VcpValue, item.Name);
+ presetList.Add(new ColorPresetItem(item.VcpValue, displayName));
+ }
+
+ // Sort by VCP value for consistent ordering
+ return presetList.OrderBy(p => p.VcpValue).ToList();
+ }
+
+ ///
+ /// Formats a color temperature display name.
+ /// Uses VcpNames for standard VCP value mappings if no custom name is provided.
+ ///
+ /// The VCP value.
+ /// Optional custom name from capabilities string.
+ /// Formatted display name.
+ public static string FormatColorTemperatureDisplayName(int vcpValue, string? customName = null)
+ {
+ // Priority: use name from VCP capabilities if available
+ if (!string.IsNullOrEmpty(customName))
+ {
+ return customName;
+ }
+
+ // Fall back to standard VCP value name from shared library
+ return VcpNames.GetValueName(NativeConstants.VcpCodeSelectColorPreset, vcpValue)
+ ?? "Manufacturer Defined";
+ }
+
+ ///
+ /// Formats a display name for a custom (non-preset) color temperature value.
+ /// Used when the current value is not in the available preset list.
+ ///
+ /// The VCP value.
+ /// Formatted display name with "Custom" indicator.
+ public static string FormatCustomColorTemperatureDisplayName(int vcpValue)
+ {
+ var standardName = VcpNames.GetValueName(NativeConstants.VcpCodeSelectColorPreset, vcpValue);
+ return string.IsNullOrEmpty(standardName)
+ ? "Custom"
+ : $"{standardName} (Custom)";
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/EventHelper.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/EventHelper.cs
new file mode 100644
index 0000000000..0b3b82b8f7
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/EventHelper.cs
@@ -0,0 +1,46 @@
+// 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.Threading;
+using ManagedCommon;
+
+namespace PowerDisplay.Common.Utils
+{
+ ///
+ /// Helper class for Windows named event operations.
+ /// Provides unified event signaling with consistent error handling and logging.
+ ///
+ public static class EventHelper
+ {
+ ///
+ /// Signals a named event. Creates the event if it doesn't exist.
+ ///
+ /// The name of the event to signal.
+ /// True if the event was signaled successfully, false otherwise.
+ public static bool SignalEvent(string eventName)
+ {
+ if (string.IsNullOrEmpty(eventName))
+ {
+ Logger.LogWarning("[EventHelper] SignalEvent called with null or empty event name");
+ return false;
+ }
+
+ try
+ {
+ using var eventHandle = new EventWaitHandle(
+ false,
+ EventResetMode.AutoReset,
+ eventName);
+ eventHandle.Set();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[EventHelper] Failed to signal event '{eventName}': {ex.Message}");
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/MccsCapabilitiesParser.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/MccsCapabilitiesParser.cs
new file mode 100644
index 0000000000..201ead3965
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/MccsCapabilitiesParser.cs
@@ -0,0 +1,860 @@
+// 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.Globalization;
+using System.Runtime.CompilerServices;
+using ManagedCommon;
+using PowerDisplay.Common.Models;
+
+namespace PowerDisplay.Common.Utils
+{
+ ///
+ /// Recursive descent parser for DDC/CI MCCS capabilities strings.
+ ///
+ /// MCCS Capabilities String Grammar (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_]+
+ /// text ::= [^()]+
+ ///
+ ///
+ /// Example input:
+ /// (prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03)vcp(10 12 14(04 05) 60(11 12))mccs_ver(2.2))
+ ///
+ public ref struct MccsCapabilitiesParser
+ {
+ private readonly List _errors;
+ private ReadOnlySpan _input;
+ private int _position;
+
+ ///
+ /// Parse a capabilities string into structured VcpCapabilities.
+ ///
+ /// Raw MCCS capabilities string
+ /// Parsed capabilities object with any parse errors
+ public static MccsParseResult Parse(string? capabilitiesString)
+ {
+ if (string.IsNullOrWhiteSpace(capabilitiesString))
+ {
+ return new MccsParseResult(VcpCapabilities.Empty, new List());
+ }
+
+ var parser = new MccsCapabilitiesParser(capabilitiesString);
+ return parser.ParseCapabilities();
+ }
+
+ private MccsCapabilitiesParser(string input)
+ {
+ _input = input.AsSpan();
+ _position = 0;
+ _errors = new List();
+ }
+
+ ///
+ /// Main entry point: parse the entire capabilities string.
+ /// capabilities ::= '(' segment* ')' | segment*
+ ///
+ private MccsParseResult ParseCapabilities()
+ {
+ var capabilities = new VcpCapabilities
+ {
+ Raw = _input.ToString(),
+ };
+
+ SkipWhitespace();
+
+ // Handle optional outer parentheses (some monitors omit them)
+ bool hasOuterParens = Peek() == '(';
+ if (hasOuterParens)
+ {
+ Advance(); // consume '('
+ }
+
+ // Parse segments until end or closing paren
+ while (!IsAtEnd())
+ {
+ SkipWhitespace();
+
+ if (IsAtEnd())
+ {
+ break;
+ }
+
+ if (Peek() == ')')
+ {
+ if (hasOuterParens)
+ {
+ Advance(); // consume closing ')'
+ }
+
+ break;
+ }
+
+ // Parse a segment: identifier(content)
+ var segment = ParseSegment();
+ if (segment.HasValue)
+ {
+ ApplySegment(capabilities, segment.Value);
+ }
+ }
+
+ return new MccsParseResult(capabilities, _errors);
+ }
+
+ ///
+ /// Parse a single segment: identifier '(' content ')'
+ ///
+ private ParsedSegment? ParseSegment()
+ {
+ SkipWhitespace();
+
+ int startPos = _position;
+
+ // Parse identifier
+ var identifier = ParseIdentifier();
+ if (identifier.IsEmpty)
+ {
+ // Not a valid segment start - skip this character and continue
+ if (!IsAtEnd())
+ {
+ Advance();
+ }
+
+ return null;
+ }
+
+ SkipWhitespace();
+
+ // Expect '('
+ if (Peek() != '(')
+ {
+ AddError($"Expected '(' after identifier '{identifier.ToString()}' at position {_position}");
+ return null;
+ }
+
+ Advance(); // consume '('
+
+ // Parse content until matching ')'
+ var content = ParseBalancedContent();
+
+ // Expect ')'
+ if (Peek() != ')')
+ {
+ AddError($"Expected ')' to close segment '{identifier.ToString()}' at position {_position}");
+ }
+ else
+ {
+ Advance(); // consume ')'
+ }
+
+ return new ParsedSegment(identifier.ToString(), content);
+ }
+
+ ///
+ /// Parse content between balanced parentheses.
+ /// Handles nested parentheses correctly.
+ ///
+ private string ParseBalancedContent()
+ {
+ int start = _position;
+ int depth = 1;
+
+ while (!IsAtEnd() && depth > 0)
+ {
+ char c = Peek();
+ if (c == '(')
+ {
+ depth++;
+ }
+ else if (c == ')')
+ {
+ depth--;
+ if (depth == 0)
+ {
+ break; // Don't consume the closing paren
+ }
+ }
+
+ Advance();
+ }
+
+ return _input.Slice(start, _position - start).ToString();
+ }
+
+ ///
+ /// Parse an identifier (letters, digits, and underscores).
+ /// identifier ::= [a-zA-Z0-9_]+
+ /// Note: MCCS uses identifiers like window1, window2, etc.
+ ///
+ private ReadOnlySpan ParseIdentifier()
+ {
+ int start = _position;
+
+ while (!IsAtEnd() && IsIdentifierChar(Peek()))
+ {
+ Advance();
+ }
+
+ return _input.Slice(start, _position - start);
+ }
+
+ ///
+ /// Apply a parsed segment to the capabilities object.
+ ///
+ private void ApplySegment(VcpCapabilities capabilities, ParsedSegment segment)
+ {
+ switch (segment.Name.ToLowerInvariant())
+ {
+ case "prot":
+ capabilities.Protocol = segment.Content.Trim();
+ break;
+
+ case "type":
+ capabilities.Type = segment.Content.Trim();
+ break;
+
+ case "model":
+ capabilities.Model = segment.Content.Trim();
+ break;
+
+ case "mccs_ver":
+ capabilities.MccsVersion = segment.Content.Trim();
+ break;
+
+ case "cmds":
+ capabilities.SupportedCommands = ParseHexList(segment.Content);
+ break;
+
+ case "vcp":
+ capabilities.SupportedVcpCodes = ParseVcpEntries(segment.Content);
+ break;
+
+ case "vcpname":
+ ParseVcpNames(segment.Content, capabilities);
+ break;
+
+ default:
+ // Check for windowN pattern (window1, window2, etc.)
+ if (segment.Name.Length > 6 &&
+ segment.Name.StartsWith("window", StringComparison.OrdinalIgnoreCase) &&
+ int.TryParse(segment.Name.AsSpan(6), out int windowNum))
+ {
+ var windowParser = new WindowParser(segment.Content);
+ var windowCap = windowParser.Parse(windowNum);
+ capabilities.Windows.Add(windowCap);
+ }
+ else
+ {
+ // Unknown segments are silently ignored
+ }
+
+ break;
+ }
+ }
+
+ ///
+ /// Parse VCP entries: vcp_entry*
+ /// vcp_entry ::= hex_byte [ '(' hex_list ')' ]
+ ///
+ private Dictionary ParseVcpEntries(string content)
+ {
+ var vcpCodes = new Dictionary();
+ var parser = new VcpEntryParser(content);
+
+ while (parser.TryParseEntry(out var entry))
+ {
+ var name = VcpNames.GetCodeName(entry.Code);
+ vcpCodes[entry.Code] = new VcpCodeInfo(entry.Code, name, entry.Values);
+ }
+
+ return vcpCodes;
+ }
+
+ ///
+ /// Parse a hex byte list: hex_byte*
+ /// Handles both space-separated (01 02 03) and concatenated (010203) formats.
+ ///
+ private static List ParseHexList(string content)
+ {
+ var result = new List();
+ var span = content.AsSpan();
+ int i = 0;
+
+ while (i < span.Length)
+ {
+ // Skip whitespace
+ while (i < span.Length && char.IsWhiteSpace(span[i]))
+ {
+ i++;
+ }
+
+ if (i >= span.Length)
+ {
+ break;
+ }
+
+ // Try to read two hex digits
+ if (i + 1 < span.Length && IsHexDigit(span[i]) && IsHexDigit(span[i + 1]))
+ {
+ if (byte.TryParse(span.Slice(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value))
+ {
+ result.Add(value);
+ }
+
+ i += 2;
+ }
+ else
+ {
+ i++; // Skip invalid character
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Parse vcpname entries: hex_byte '(' name ')'
+ ///
+ private void ParseVcpNames(string content, VcpCapabilities capabilities)
+ {
+ // vcpname format: F0(Custom Name 1) F1(Custom Name 2)
+ var parser = new VcpNameParser(content);
+
+ while (parser.TryParseEntry(out var code, out var name))
+ {
+ if (capabilities.SupportedVcpCodes.TryGetValue(code, out var existingInfo))
+ {
+ // Update existing entry with custom name
+ capabilities.SupportedVcpCodes[code] = new VcpCodeInfo(code, name, existingInfo.SupportedValues);
+ }
+ else
+ {
+ // Add new entry with custom name
+ capabilities.SupportedVcpCodes[code] = new VcpCodeInfo(code, name, Array.Empty());
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private char Peek() => IsAtEnd() ? '\0' : _input[_position];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void Advance() => _position++;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsAtEnd() => _position >= _input.Length;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SkipWhitespace()
+ {
+ while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
+ {
+ Advance();
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsIdentifierChar(char c) =>
+ (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsHexDigit(char c) =>
+ (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+
+ private void AddError(string message)
+ {
+ _errors.Add(new ParseError(_position, message));
+ Logger.LogWarning($"[MccsParser] {message}");
+ }
+ }
+
+ ///
+ /// Sub-parser for VCP entries within the vcp() segment.
+ ///
+ internal ref struct VcpEntryParser
+ {
+ private ReadOnlySpan _content;
+ private int _position;
+
+ public VcpEntryParser(string content)
+ {
+ _content = content.AsSpan();
+ _position = 0;
+ }
+
+ ///
+ /// Try to parse the next VCP entry.
+ /// vcp_entry ::= hex_byte [ '(' hex_list ')' ]
+ ///
+ public bool TryParseEntry(out VcpEntry entry)
+ {
+ entry = default;
+ SkipWhitespace();
+
+ if (IsAtEnd())
+ {
+ return false;
+ }
+
+ // Parse hex byte (VCP code)
+ if (!TryParseHexByte(out var code))
+ {
+ // Skip invalid character and try again
+ _position++;
+ return TryParseEntry(out entry);
+ }
+
+ var values = new List();
+
+ SkipWhitespace();
+
+ // Check for optional value list
+ if (!IsAtEnd() && Peek() == '(')
+ {
+ _position++; // consume '('
+
+ // Parse values until ')'
+ while (!IsAtEnd() && Peek() != ')')
+ {
+ SkipWhitespace();
+
+ if (Peek() == ')')
+ {
+ break;
+ }
+
+ if (TryParseHexByte(out var value))
+ {
+ values.Add(value);
+ }
+ else
+ {
+ _position++; // Skip invalid character
+ }
+ }
+
+ if (!IsAtEnd() && Peek() == ')')
+ {
+ _position++; // consume ')'
+ }
+ }
+
+ entry = new VcpEntry(code, values);
+ return true;
+ }
+
+ private bool TryParseHexByte(out byte value)
+ {
+ value = 0;
+
+ if (_position + 1 >= _content.Length)
+ {
+ return false;
+ }
+
+ if (!IsHexDigit(_content[_position]) || !IsHexDigit(_content[_position + 1]))
+ {
+ return false;
+ }
+
+ if (byte.TryParse(_content.Slice(_position, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value))
+ {
+ _position += 2;
+ return true;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private char Peek() => IsAtEnd() ? '\0' : _content[_position];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsAtEnd() => _position >= _content.Length;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SkipWhitespace()
+ {
+ while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
+ {
+ _position++;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsHexDigit(char c) =>
+ (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+ }
+
+ ///
+ /// Sub-parser for vcpname entries.
+ ///
+ internal ref struct VcpNameParser
+ {
+ private ReadOnlySpan _content;
+ private int _position;
+
+ public VcpNameParser(string content)
+ {
+ _content = content.AsSpan();
+ _position = 0;
+ }
+
+ ///
+ /// Try to parse the next vcpname entry.
+ /// vcpname_entry ::= hex_byte '(' name ')'
+ ///
+ public bool TryParseEntry(out byte code, out string name)
+ {
+ code = 0;
+ name = string.Empty;
+
+ SkipWhitespace();
+
+ if (IsAtEnd())
+ {
+ return false;
+ }
+
+ // Parse hex byte
+ if (!TryParseHexByte(out code))
+ {
+ _position++;
+ return TryParseEntry(out code, out name);
+ }
+
+ SkipWhitespace();
+
+ // Expect '('
+ if (IsAtEnd() || Peek() != '(')
+ {
+ return false;
+ }
+
+ _position++; // consume '('
+
+ // Parse name until ')'
+ int start = _position;
+ while (!IsAtEnd() && Peek() != ')')
+ {
+ _position++;
+ }
+
+ name = _content.Slice(start, _position - start).ToString().Trim();
+
+ if (!IsAtEnd() && Peek() == ')')
+ {
+ _position++; // consume ')'
+ }
+
+ return true;
+ }
+
+ private bool TryParseHexByte(out byte value)
+ {
+ value = 0;
+
+ if (_position + 1 >= _content.Length)
+ {
+ return false;
+ }
+
+ if (!IsHexDigit(_content[_position]) || !IsHexDigit(_content[_position + 1]))
+ {
+ return false;
+ }
+
+ if (byte.TryParse(_content.Slice(_position, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value))
+ {
+ _position += 2;
+ return true;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private char Peek() => IsAtEnd() ? '\0' : _content[_position];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsAtEnd() => _position >= _content.Length;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SkipWhitespace()
+ {
+ while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
+ {
+ _position++;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsHexDigit(char c) =>
+ (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+ }
+
+ ///
+ /// Sub-parser for window segment content.
+ /// Parses: type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)
+ ///
+ internal ref struct WindowParser
+ {
+ private ReadOnlySpan _content;
+ private int _position;
+
+ public WindowParser(string content)
+ {
+ _content = content.AsSpan();
+ _position = 0;
+ }
+
+ ///
+ /// Parse window segment content into a WindowCapability.
+ ///
+ public WindowCapability Parse(int windowNumber)
+ {
+ string type = string.Empty;
+ var area = default(WindowArea);
+ var maxSize = default(WindowSize);
+ var minSize = default(WindowSize);
+ int windowId = 0;
+
+ // Parse sub-segments: type(...) area(...) max(...) min(...) window(...)
+ while (!IsAtEnd())
+ {
+ SkipWhitespace();
+ if (IsAtEnd())
+ {
+ break;
+ }
+
+ var subSegment = ParseSubSegment();
+ if (subSegment.HasValue)
+ {
+ switch (subSegment.Value.Name.ToLowerInvariant())
+ {
+ case "type":
+ type = subSegment.Value.Content.Trim();
+ break;
+ case "area":
+ area = ParseArea(subSegment.Value.Content);
+ break;
+ case "max":
+ maxSize = ParseSize(subSegment.Value.Content);
+ break;
+ case "min":
+ minSize = ParseSize(subSegment.Value.Content);
+ break;
+ case "window":
+ _ = int.TryParse(subSegment.Value.Content.Trim(), out windowId);
+ break;
+ }
+ }
+ }
+
+ return new WindowCapability(windowNumber, type, area, maxSize, minSize, windowId);
+ }
+
+ private (string Name, string Content)? ParseSubSegment()
+ {
+ int start = _position;
+
+ // Parse identifier
+ while (!IsAtEnd() && IsIdentifierChar(Peek()))
+ {
+ _position++;
+ }
+
+ if (_position == start)
+ {
+ // No identifier found, skip character
+ if (!IsAtEnd())
+ {
+ _position++;
+ }
+
+ return null;
+ }
+
+ var name = _content.Slice(start, _position - start).ToString();
+
+ SkipWhitespace();
+
+ // Expect '('
+ if (IsAtEnd() || Peek() != '(')
+ {
+ return null;
+ }
+
+ _position++; // consume '('
+
+ // Parse content with balanced parentheses
+ int contentStart = _position;
+ int depth = 1;
+
+ while (!IsAtEnd() && depth > 0)
+ {
+ char c = Peek();
+ if (c == '(')
+ {
+ depth++;
+ }
+ else if (c == ')')
+ {
+ depth--;
+ if (depth == 0)
+ {
+ break;
+ }
+ }
+
+ _position++;
+ }
+
+ var content = _content.Slice(contentStart, _position - contentStart).ToString();
+
+ if (!IsAtEnd() && Peek() == ')')
+ {
+ _position++; // consume ')'
+ }
+
+ return (name, content);
+ }
+
+ private static WindowArea ParseArea(string content)
+ {
+ var values = ParseIntList(content);
+ if (values.Length >= 4)
+ {
+ return new WindowArea(values[0], values[1], values[2], values[3]);
+ }
+
+ return default;
+ }
+
+ private static WindowSize ParseSize(string content)
+ {
+ var values = ParseIntList(content);
+ if (values.Length >= 2)
+ {
+ return new WindowSize(values[0], values[1]);
+ }
+
+ return default;
+ }
+
+ private static int[] ParseIntList(string content)
+ {
+ var parts = content.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ var result = new List(parts.Length);
+
+ foreach (var part in parts)
+ {
+ if (int.TryParse(part.Trim(), out int value))
+ {
+ result.Add(value);
+ }
+ }
+
+ return result.ToArray();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private char Peek() => IsAtEnd() ? '\0' : _content[_position];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsAtEnd() => _position >= _content.Length;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SkipWhitespace()
+ {
+ while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
+ {
+ _position++;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsIdentifierChar(char c) =>
+ (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
+ }
+
+ ///
+ /// Represents a parsed segment from the capabilities string.
+ ///
+ internal readonly struct ParsedSegment
+ {
+ public string Name { get; }
+
+ public string Content { get; }
+
+ public ParsedSegment(string name, string content)
+ {
+ Name = name;
+ Content = content;
+ }
+ }
+
+ ///
+ /// Represents a parsed VCP entry.
+ ///
+ internal readonly struct VcpEntry
+ {
+ public byte Code { get; }
+
+ public IReadOnlyList Values { get; }
+
+ public VcpEntry(byte code, IReadOnlyList values)
+ {
+ Code = code;
+ Values = values;
+ }
+ }
+
+ ///
+ /// Represents a parse error with position information.
+ ///
+ public readonly struct ParseError
+ {
+ public int Position { get; }
+
+ public string Message { get; }
+
+ public ParseError(int position, string message)
+ {
+ Position = position;
+ Message = message;
+ }
+
+ public override string ToString() => $"[{Position}] {Message}";
+ }
+
+ ///
+ /// Result of parsing MCCS capabilities string.
+ ///
+ public sealed class MccsParseResult
+ {
+ public VcpCapabilities Capabilities { get; }
+
+ public IReadOnlyList Errors { get; }
+
+ public bool HasErrors => Errors.Count > 0;
+
+ public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
+
+ public MccsParseResult(VcpCapabilities capabilities, IReadOnlyList errors)
+ {
+ Capabilities = capabilities;
+ Errors = errors;
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/PnpIdHelper.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/PnpIdHelper.cs
new file mode 100644
index 0000000000..dd1665d0f5
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/PnpIdHelper.cs
@@ -0,0 +1,86 @@
+// 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.Frozen;
+using System.Collections.Generic;
+
+namespace PowerDisplay.Common.Utils;
+
+///
+/// Helper class for mapping PnP (Plug and Play) manufacturer IDs to display names.
+/// PnP IDs are 3-character codes assigned by Microsoft to hardware manufacturers.
+/// See: https://uefi.org/pnp_id_list
+///
+public static class PnpIdHelper
+{
+ ///
+ /// Map of common laptop/monitor manufacturer PnP IDs to display names.
+ /// Only includes manufacturers known to produce laptops with internal displays.
+ ///
+ private static readonly FrozenDictionary ManufacturerNames = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // Major laptop manufacturers
+ { "ACR", "Acer" },
+ { "AUO", "AU Optronics" },
+ { "BOE", "BOE" },
+ { "CMN", "Chi Mei Innolux" },
+ { "DEL", "Dell" },
+ { "HWP", "HP" },
+ { "IVO", "InfoVision" },
+ { "LEN", "Lenovo" },
+ { "LGD", "LG Display" },
+ { "NCP", "Nanjing CEC Panda" },
+ { "SAM", "Samsung" },
+ { "SDC", "Samsung Display" },
+ { "SEC", "Samsung Electronics" },
+ { "SHP", "Sharp" },
+ { "AUS", "ASUS" },
+ { "MSI", "MSI" },
+ { "APP", "Apple" },
+ { "SNY", "Sony" },
+ { "PHL", "Philips" },
+ { "HSD", "HannStar" },
+ { "CPT", "Chunghwa Picture Tubes" },
+ { "QDS", "Quanta Display" },
+ { "TMX", "Tianma Microelectronics" },
+ { "CSO", "CSOT" },
+
+ // Microsoft Surface
+ { "MSF", "Microsoft" },
+ }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Extract the 3-character PnP manufacturer ID from an EDID ID.
+ ///
+ /// EDID ID like "LEN4038" or "BOE0900".
+ /// The 3-character PnP ID (e.g., "LEN"), or null if invalid.
+ public static string? ExtractPnpId(string? edidId)
+ {
+ if (string.IsNullOrEmpty(edidId) || edidId.Length < 3)
+ {
+ return null;
+ }
+
+ // PnP ID is the first 3 characters
+ return edidId.Substring(0, 3).ToUpperInvariant();
+ }
+
+ ///
+ /// Get a user-friendly display name for an internal display based on its EDID ID.
+ ///
+ /// EDID ID like "LEN4038" or "BOE0900".
+ /// Display name like "Lenovo Built-in Display" or "Built-in Display" as fallback.
+ public static string GetBuiltInDisplayName(string? edidId)
+ {
+ var pnpId = ExtractPnpId(edidId);
+
+ if (pnpId != null && ManufacturerNames.TryGetValue(pnpId, out var manufacturer))
+ {
+ return $"{manufacturer} Built-in Display";
+ }
+
+ return "Built-in Display";
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/ProfileHelper.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/ProfileHelper.cs
new file mode 100644
index 0000000000..d7274824d7
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/ProfileHelper.cs
@@ -0,0 +1,66 @@
+// 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.Globalization;
+
+namespace PowerDisplay.Common.Utils
+{
+ ///
+ /// Helper class for profile management.
+ /// Provides shared logic for generating unique profile names and other profile-related operations.
+ ///
+ public static class ProfileHelper
+ {
+ ///
+ /// Default base name for new profiles.
+ ///
+ public const string DefaultProfileBaseName = "Profile";
+
+ ///
+ /// Maximum counter value when generating unique profile names.
+ ///
+ private const int MaxProfileCounter = 1000;
+
+ ///
+ /// Generates a unique profile name that doesn't conflict with existing names.
+ /// Uses the format "Profile N" where N is an incrementing number.
+ ///
+ /// Set of existing profile names to avoid conflicts.
+ /// Optional base name to use (defaults to "Profile").
+ /// A unique profile name.
+ public static string GenerateUniqueProfileName(ISet existingNames, string? baseName = null)
+ {
+ if (existingNames == null)
+ {
+ existingNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+
+ var nameBase = string.IsNullOrEmpty(baseName) ? DefaultProfileBaseName : baseName;
+
+ // Start with base name without number
+ if (!existingNames.Contains(nameBase))
+ {
+ return nameBase;
+ }
+
+ // Try "Profile 2", "Profile 3", etc.
+ int counter = 2;
+ while (counter < MaxProfileCounter)
+ {
+ var candidateName = string.Format(CultureInfo.InvariantCulture, "{0} {1}", nameBase, counter);
+ if (!existingNames.Contains(candidateName))
+ {
+ return candidateName;
+ }
+
+ counter++;
+ }
+
+ // Fallback with timestamp if somehow we hit the limit
+ return string.Format(CultureInfo.InvariantCulture, "{0} {1}", nameBase, DateTime.Now.Ticks);
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/SimpleDebouncer.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/SimpleDebouncer.cs
new file mode 100644
index 0000000000..3c55a5d992
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/SimpleDebouncer.cs
@@ -0,0 +1,127 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using ManagedCommon;
+
+namespace PowerDisplay.Common.Utils
+{
+ ///
+ /// Simple debouncer that delays execution of an action until a quiet period.
+ /// Replaces the complex PropertyUpdateQueue with a much simpler approach (KISS principle).
+ ///
+ public partial class SimpleDebouncer : IDisposable
+ {
+ private readonly int _delayMs;
+ private readonly object _lock = new object();
+ private CancellationTokenSource? _cts;
+ private bool _disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Create a debouncer with specified delay
+ ///
+ /// Delay in milliseconds before executing action
+ public SimpleDebouncer(int delayMs = 300)
+ {
+ _delayMs = delayMs;
+ }
+
+ ///
+ /// Debounce an async action. Cancels previous invocation if still pending.
+ ///
+ /// Async action to execute after delay
+ public void Debounce(Func action)
+ {
+ _ = DebounceAsync(action);
+ }
+
+ ///
+ /// Debounce a synchronous action
+ ///
+ public void Debounce(Action action)
+ {
+ _ = DebounceAsync(() =>
+ {
+ action();
+ return Task.CompletedTask;
+ });
+ }
+
+ private async Task DebounceAsync(Func action)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ CancellationTokenSource cts;
+ CancellationTokenSource? oldCts = null;
+
+ lock (_lock)
+ {
+ // Store old CTS to dispose later
+ oldCts = _cts;
+
+ // Create new CTS
+ _cts = new CancellationTokenSource();
+ cts = _cts;
+ }
+
+ // Dispose old CTS outside the lock to avoid blocking
+ if (oldCts != null)
+ {
+ try
+ {
+ oldCts.Cancel();
+ oldCts.Dispose();
+ }
+ catch (ObjectDisposedException)
+ {
+ // Expected if CTS was already disposed
+ }
+ }
+
+ try
+ {
+ // Wait for quiet period
+ await Task.Delay(_delayMs, cts.Token).ConfigureAwait(false);
+
+ // Execute action if not cancelled
+ if (!cts.Token.IsCancellationRequested)
+ {
+ await action().ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when debouncing - a newer call cancelled this one
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Debounced action failed: {ex.Message}");
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ lock (_lock)
+ {
+ _disposed = true;
+ _cts?.Cancel();
+ _cts?.Dispose();
+ _cts = null;
+ }
+
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/VcpNames.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/VcpNames.cs
new file mode 100644
index 0000000000..2d4fed19c6
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/VcpNames.cs
@@ -0,0 +1,428 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+
+namespace PowerDisplay.Common.Utils
+{
+ ///
+ /// Provides human-readable names for VCP codes and their values based on MCCS v2.2a specification.
+ /// Combines VCP code names (e.g., 0x10 = "Brightness") and VCP value names (e.g., 0x14:0x05 = "6500K").
+ ///
+ public static class VcpNames
+ {
+ ///
+ /// VCP code to name mapping
+ ///
+ private static readonly Dictionary CodeNames = new()
+ {
+ // Control codes (0x00-0x0F)
+ { 0x00, "Code Page" },
+ { 0x01, "Degauss" },
+ { 0x02, "New Control Value" },
+ { 0x03, "Soft Controls" },
+
+ // Preset operations (0x04-0x0A)
+ { 0x04, "Restore Factory Defaults" },
+ { 0x05, "Restore Brightness and Contrast" },
+ { 0x06, "Restore Factory Geometry" },
+ { 0x08, "Restore Color Defaults" },
+ { 0x0A, "Restore Factory TV Defaults" },
+
+ // Color temperature codes
+ { 0x0B, "Color Temperature Increment" },
+ { 0x0C, "Color Temperature Request" },
+ { 0x0E, "Clock" },
+ { 0x0F, "Color Saturation" },
+
+ // Image adjustment codes
+ { 0x10, "Brightness" },
+ { 0x11, "Flesh Tone Enhancement" },
+ { 0x12, "Contrast" },
+ { 0x13, "Backlight Control" },
+ { 0x14, "Select Color Preset" },
+ { 0x16, "Video Gain: Red" },
+ { 0x17, "User Color Vision Compensation" },
+ { 0x18, "Video Gain: Green" },
+ { 0x1A, "Video Gain: Blue" },
+ { 0x1C, "Focus" },
+ { 0x1E, "Auto Setup" },
+ { 0x1F, "Auto Color Setup" },
+
+ // Geometry controls (0x20-0x4C)
+ { 0x20, "Horizontal Position" },
+ { 0x22, "Horizontal Size" },
+ { 0x24, "Horizontal Pincushion" },
+ { 0x26, "Horizontal Pincushion Balance" },
+ { 0x28, "Horizontal Convergence R/B" },
+ { 0x29, "Horizontal Convergence M/G" },
+ { 0x2A, "Horizontal Linearity" },
+ { 0x2C, "Horizontal Linearity Balance" },
+ { 0x2E, "Gray Scale Expansion" },
+ { 0x30, "Vertical Position" },
+ { 0x32, "Vertical Size" },
+ { 0x34, "Vertical Pincushion" },
+ { 0x36, "Vertical Pincushion Balance" },
+ { 0x38, "Vertical Convergence R/B" },
+ { 0x39, "Vertical Convergence M/G" },
+ { 0x3A, "Vertical Linearity" },
+ { 0x3C, "Vertical Linearity Balance" },
+ { 0x3E, "Clock Phase" },
+
+ // Miscellaneous codes
+ { 0x40, "Horizontal Parallelogram" },
+ { 0x41, "Vertical Parallelogram" },
+ { 0x42, "Horizontal Keystone" },
+ { 0x43, "Vertical Keystone" },
+ { 0x44, "Rotation" },
+ { 0x46, "Top Corner Flare" },
+ { 0x48, "Top Corner Hook" },
+ { 0x4A, "Bottom Corner Flare" },
+ { 0x4C, "Bottom Corner Hook" },
+
+ // Advanced codes
+ { 0x52, "Active Control" },
+ { 0x54, "Performance Preservation" },
+ { 0x56, "Horizontal Moire" },
+ { 0x58, "Vertical Moire" },
+ { 0x59, "6 Axis Saturation: Red" },
+ { 0x5A, "6 Axis Saturation: Yellow" },
+ { 0x5B, "6 Axis Saturation: Green" },
+ { 0x5C, "6 Axis Saturation: Cyan" },
+ { 0x5D, "6 Axis Saturation: Blue" },
+ { 0x5E, "6 Axis Saturation: Magenta" },
+
+ // Input source codes
+ { 0x60, "Input Source" },
+ { 0x62, "Audio Speaker Volume" },
+ { 0x63, "Speaker Select" },
+ { 0x64, "Audio: Microphone Volume" },
+ { 0x66, "Ambient Light Sensor" },
+ { 0x6B, "Backlight Level: White" },
+ { 0x6C, "Video Black Level: Red" },
+ { 0x6D, "Backlight Level: Red" },
+ { 0x6E, "Video Black Level: Green" },
+ { 0x6F, "Backlight Level: Green" },
+ { 0x70, "Video Black Level: Blue" },
+ { 0x71, "Backlight Level: Blue" },
+ { 0x72, "Gamma" },
+ { 0x73, "LUT Size" },
+ { 0x74, "Single Point LUT Operation" },
+ { 0x75, "Block LUT Operation" },
+ { 0x76, "Remote Procedure Call" },
+ { 0x78, "Display Identification Data Operation" },
+ { 0x7A, "Adjust Focal Plane" },
+ { 0x7C, "Adjust Zoom" },
+ { 0x7E, "Trapezoid" },
+ { 0x80, "Keystone" },
+ { 0x82, "Horizontal Mirror (Flip)" },
+ { 0x84, "Vertical Mirror (Flip)" },
+
+ // Image adjustment codes (0x86-0x9F)
+ { 0x86, "Display Scaling" },
+ { 0x87, "Sharpness" },
+ { 0x88, "Velocity Scan Modulation" },
+ { 0x8A, "Color Saturation" },
+ { 0x8B, "TV Channel Up/Down" },
+ { 0x8C, "TV Sharpness" },
+ { 0x8D, "Audio Mute/Screen Blank" },
+ { 0x8E, "TV Contrast" },
+ { 0x8F, "Audio Treble" },
+ { 0x90, "Hue" },
+ { 0x91, "Audio Bass" },
+ { 0x92, "TV Black Level/Luminance" },
+ { 0x93, "Audio Balance L/R" },
+ { 0x94, "Audio Processor Mode" },
+ { 0x95, "Window Position(TL_X)" },
+ { 0x96, "Window Position(TL_Y)" },
+ { 0x97, "Window Position(BR_X)" },
+ { 0x98, "Window Position(BR_Y)" },
+ { 0x99, "Window Background" },
+ { 0x9A, "6 Axis Hue Control: Red" },
+ { 0x9B, "6 Axis Hue Control: Yellow" },
+ { 0x9C, "6 Axis Hue Control: Green" },
+ { 0x9D, "6 Axis Hue Control: Cyan" },
+ { 0x9E, "6 Axis Hue Control: Blue" },
+ { 0x9F, "6 Axis Hue Control: Magenta" },
+
+ // Window control codes
+ { 0xA0, "Auto Setup On/Off" },
+ { 0xA2, "Auto Color Setup On/Off" },
+ { 0xA4, "Window Mask Control" },
+ { 0xA5, "Window Select" },
+ { 0xA6, "Window Size" },
+ { 0xA7, "Window Transparency" },
+ { 0xA8, "Window Control" },
+ { 0xAA, "Screen Orientation" },
+ { 0xAC, "Horizontal Frequency" },
+ { 0xAE, "Vertical Frequency" },
+
+ // Misc advanced codes
+ { 0xB0, "Settings" },
+ { 0xB2, "Flat Panel Sub-Pixel Layout" },
+ { 0xB4, "Source Timing Mode" },
+ { 0xB6, "Display Technology Type" },
+ { 0xB7, "Monitor Status" },
+ { 0xB8, "Packet Count" },
+ { 0xB9, "Monitor X Origin" },
+ { 0xBA, "Monitor Y Origin" },
+ { 0xBB, "Header Error Count" },
+ { 0xBC, "Body CRC Error Count" },
+ { 0xBD, "Client ID" },
+ { 0xBE, "Link Control" },
+
+ // Display controller codes
+ { 0xC0, "Display Usage Time" },
+ { 0xC2, "Display Firmware Level" },
+ { 0xC4, "Display Descriptor Length" },
+ { 0xC5, "Transmit Display Descriptor" },
+ { 0xC6, "Enable Display of 'Display Descriptor'" },
+ { 0xC8, "Display Controller Type" },
+ { 0xC9, "Display Firmware Level" },
+ { 0xCA, "OSD" },
+ { 0xCC, "OSD Language" },
+ { 0xCD, "Status Indicators" },
+ { 0xCE, "Auxiliary Display Size" },
+ { 0xCF, "Auxiliary Display Data" },
+ { 0xD0, "Output Select" },
+ { 0xD2, "Asset Tag" },
+ { 0xD4, "Stereo Video Mode" },
+ { 0xD6, "Power Mode" },
+ { 0xD7, "Auxiliary Power Output" },
+ { 0xD8, "Scan Mode" },
+ { 0xD9, "Image Mode" },
+ { 0xDA, "On Screen Display" },
+ { 0xDB, "Backlight Level: White" },
+ { 0xDC, "Display Application" },
+ { 0xDD, "Application Enable Key" },
+ { 0xDE, "Scratch Pad" },
+ { 0xDF, "VCP Version" },
+
+ // Manufacturer specific codes (0xE0-0xFF)
+ // Per MCCS 2.2a: "The 32 control codes E0h through FFh have been
+ // allocated to allow manufacturers to issue their own specific controls."
+ { 0xE0, "Manufacturer Specific" },
+ { 0xE1, "Manufacturer Specific" },
+ { 0xE2, "Manufacturer Specific" },
+ { 0xE3, "Manufacturer Specific" },
+ { 0xE4, "Manufacturer Specific" },
+ { 0xE5, "Manufacturer Specific" },
+ { 0xE6, "Manufacturer Specific" },
+ { 0xE7, "Manufacturer Specific" },
+ { 0xE8, "Manufacturer Specific" },
+ { 0xE9, "Manufacturer Specific" },
+ { 0xEA, "Manufacturer Specific" },
+ { 0xEB, "Manufacturer Specific" },
+ { 0xEC, "Manufacturer Specific" },
+ { 0xED, "Manufacturer Specific" },
+ { 0xEE, "Manufacturer Specific" },
+ { 0xEF, "Manufacturer Specific" },
+ { 0xF0, "Manufacturer Specific" },
+ { 0xF1, "Manufacturer Specific" },
+ { 0xF2, "Manufacturer Specific" },
+ { 0xF3, "Manufacturer Specific" },
+ { 0xF4, "Manufacturer Specific" },
+ { 0xF5, "Manufacturer Specific" },
+ { 0xF6, "Manufacturer Specific" },
+ { 0xF7, "Manufacturer Specific" },
+ { 0xF8, "Manufacturer Specific" },
+ { 0xF9, "Manufacturer Specific" },
+ { 0xFA, "Manufacturer Specific" },
+ { 0xFB, "Manufacturer Specific" },
+ { 0xFC, "Manufacturer Specific" },
+ { 0xFD, "Manufacturer Specific" },
+ { 0xFE, "Manufacturer Specific" },
+ { 0xFF, "Manufacturer Specific" },
+ };
+
+ ///
+ /// Get the friendly name for a VCP code
+ ///
+ /// VCP code (e.g., 0x10)
+ /// Friendly name, or hex representation if unknown
+ public static string GetCodeName(byte code)
+ {
+ return CodeNames.TryGetValue(code, out var name) ? name : $"Unknown (0x{code:X2})";
+ }
+
+ // Dictionary>
+ private static readonly Dictionary> ValueNames = new()
+ {
+ // 0x14: Select Color Preset
+ [0x14] = new Dictionary
+ {
+ [0x01] = "sRGB",
+ [0x02] = "Display Native",
+ [0x03] = "4000K",
+ [0x04] = "5000K",
+ [0x05] = "6500K",
+ [0x06] = "7500K",
+ [0x07] = "8200K",
+ [0x08] = "9300K",
+ [0x09] = "10000K",
+ [0x0A] = "11500K",
+ [0x0B] = "User 1",
+ [0x0C] = "User 2",
+ [0x0D] = "User 3",
+ },
+
+ // 0x60: Input Source
+ [0x60] = new Dictionary
+ {
+ [0x01] = "VGA-1",
+ [0x02] = "VGA-2",
+ [0x03] = "DVI-1",
+ [0x04] = "DVI-2",
+ [0x05] = "Composite Video 1",
+ [0x06] = "Composite Video 2",
+ [0x07] = "S-Video-1",
+ [0x08] = "S-Video-2",
+ [0x09] = "Tuner-1",
+ [0x0A] = "Tuner-2",
+ [0x0B] = "Tuner-3",
+ [0x0C] = "Component Video 1",
+ [0x0D] = "Component Video 2",
+ [0x0E] = "Component Video 3",
+ [0x0F] = "DisplayPort-1",
+ [0x10] = "DisplayPort-2",
+ [0x11] = "HDMI-1",
+ [0x12] = "HDMI-2",
+ [0x1B] = "USB-C",
+ },
+
+ // 0xD6: Power Mode
+ [0xD6] = new Dictionary
+ {
+ [0x01] = "On",
+ [0x02] = "Standby",
+ [0x03] = "Suspend",
+ [0x04] = "Off (DPM)",
+ [0x05] = "Off (Hard)",
+ },
+
+ // 0x8D: Audio Mute
+ [0x8D] = new Dictionary
+ {
+ [0x01] = "Muted",
+ [0x02] = "Unmuted",
+ },
+
+ // 0xDC: Display Application
+ [0xDC] = new Dictionary
+ {
+ [0x00] = "Standard/Default",
+ [0x01] = "Productivity",
+ [0x02] = "Mixed",
+ [0x03] = "Movie",
+ [0x04] = "User Defined",
+ [0x05] = "Games",
+ [0x06] = "Sports",
+ [0x07] = "Professional (calibration)",
+ [0x08] = "Standard/Default with intermediate power consumption",
+ [0x09] = "Standard/Default with low power consumption",
+ [0x0A] = "Demonstration",
+ [0xF0] = "Dynamic Contrast",
+ },
+
+ // 0xCC: OSD Language
+ [0xCC] = new Dictionary
+ {
+ [0x01] = "Chinese (traditional, Hantai)",
+ [0x02] = "English",
+ [0x03] = "French",
+ [0x04] = "German",
+ [0x05] = "Italian",
+ [0x06] = "Japanese",
+ [0x07] = "Korean",
+ [0x08] = "Portuguese (Portugal)",
+ [0x09] = "Russian",
+ [0x0A] = "Spanish",
+ [0x0B] = "Swedish",
+ [0x0C] = "Turkish",
+ [0x0D] = "Chinese (simplified, Kantai)",
+ [0x0E] = "Portuguese (Brazil)",
+ [0x0F] = "Arabic",
+ [0x10] = "Bulgarian",
+ [0x11] = "Croatian",
+ [0x12] = "Czech",
+ [0x13] = "Danish",
+ [0x14] = "Dutch",
+ [0x15] = "Estonian",
+ [0x16] = "Finnish",
+ [0x17] = "Greek",
+ [0x18] = "Hebrew",
+ [0x19] = "Hindi",
+ [0x1A] = "Hungarian",
+ [0x1B] = "Latvian",
+ [0x1C] = "Lithuanian",
+ [0x1D] = "Norwegian",
+ [0x1E] = "Polish",
+ [0x1F] = "Romanian",
+ [0x20] = "Serbian",
+ [0x21] = "Slovak",
+ [0x22] = "Slovenian",
+ [0x23] = "Thai",
+ [0x24] = "Ukrainian",
+ [0x25] = "Vietnamese",
+ },
+
+ // 0x62: Audio Speaker Volume
+ [0x62] = new Dictionary
+ {
+ [0x00] = "Mute",
+
+ // Other values are continuous
+ },
+
+ // 0xDB: Image Mode (Dell monitors)
+ [0xDB] = new Dictionary
+ {
+ [0x00] = "Standard",
+ [0x01] = "Multimedia",
+ [0x02] = "Movie",
+ [0x03] = "Game",
+ [0x04] = "Sports",
+ [0x05] = "Color Temperature",
+ [0x06] = "Custom Color",
+ [0x07] = "ComfortView",
+ },
+ };
+
+ ///
+ /// Get human-readable name for a VCP value
+ ///
+ /// VCP code (e.g., 0x14)
+ /// Value to translate
+ /// Name string like "sRGB" or null if unknown
+ public static string? GetValueName(byte vcpCode, int value)
+ {
+ if (ValueNames.TryGetValue(vcpCode, out var codeValues))
+ {
+ if (codeValues.TryGetValue(value, out var name))
+ {
+ return name;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Get formatted display name for a VCP value (with hex value in parentheses)
+ ///
+ /// VCP code (e.g., 0x14)
+ /// Value to translate
+ /// Formatted string like "sRGB (0x01)" or "0x01" if unknown
+ public static string GetFormattedValueName(byte vcpCode, int value)
+ {
+ var name = GetValueName(vcpCode, value);
+ if (name != null)
+ {
+ return $"{name} (0x{value:X2})";
+ }
+
+ return $"0x{value:X2}";
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay/PowerDisplay.ico b/src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay/PowerDisplay.ico
new file mode 100644
index 0000000000..a9f170a8bd
Binary files /dev/null and b/src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay/PowerDisplay.ico differ
diff --git a/src/modules/powerdisplay/PowerDisplay/Configuration/AppConstants.cs b/src/modules/powerdisplay/PowerDisplay/Configuration/AppConstants.cs
new file mode 100644
index 0000000000..94ab75444a
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Configuration/AppConstants.cs
@@ -0,0 +1,34 @@
+// 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.Configuration
+{
+ ///
+ /// Application-wide constants and configuration values
+ ///
+ public static class AppConstants
+ {
+ ///
+ /// UI layout and timing constants
+ ///
+ public static class UI
+ {
+ // Window dimensions
+ public const int WindowWidth = 362;
+ public const int MinWindowHeight = 100;
+ public const int MaxWindowHeight = 650;
+ public const int WindowRightMargin = 12;
+
+ ///
+ /// Icon glyph for internal/laptop displays (WMI)
+ ///
+ public const string InternalMonitorGlyph = "\uE7F8";
+
+ ///
+ /// Icon glyph for external monitors (DDC/CI)
+ ///
+ public const string ExternalMonitorGlyph = "\uE7F4";
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/GlobalUsings.cs b/src/modules/powerdisplay/PowerDisplay/GlobalUsings.cs
new file mode 100644
index 0000000000..d6c14983d5
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/GlobalUsings.cs
@@ -0,0 +1,9 @@
+// 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.CompilerServices;
+
+// Enable compile-time marshalling for all P/Invoke declarations
+// This allows LibraryImport to handle array marshalling and achieve 100% coverage
+[assembly: DisableRuntimeMarshalling]
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/DisplayChangeWatcher.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/DisplayChangeWatcher.cs
new file mode 100644
index 0000000000..74650a4373
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/DisplayChangeWatcher.cs
@@ -0,0 +1,333 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using ManagedCommon;
+using Microsoft.UI.Dispatching;
+using Microsoft.Win32;
+using Windows.Devices.Display;
+using Windows.Devices.Enumeration;
+
+namespace PowerDisplay.Helpers;
+
+///
+/// Watches for display/monitor connection changes using WinRT DeviceWatcher.
+/// Triggers DisplayChanged event when monitors are added, removed, or updated.
+///
+public sealed partial class DisplayChangeWatcher : IDisposable
+{
+ private readonly DispatcherQueue _dispatcherQueue;
+ private readonly TimeSpan _debounceDelay;
+
+ private DeviceWatcher? _deviceWatcher;
+ private CancellationTokenSource? _debounceCts;
+ private bool _isRunning;
+ private bool _disposed;
+ private bool _initialEnumerationComplete;
+
+ ///
+ /// Event triggered when display configuration changes (after debounce period).
+ ///
+ public event EventHandler? DisplayChanged;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dispatcher queue for UI thread marshalling.
+ /// Delay before triggering DisplayChanged event. This allows hardware to stabilize after monitor plug/unplug.
+ public DisplayChangeWatcher(DispatcherQueue dispatcherQueue, TimeSpan debounceDelay)
+ {
+ _dispatcherQueue = dispatcherQueue ?? throw new ArgumentNullException(nameof(dispatcherQueue));
+ _debounceDelay = debounceDelay;
+ SystemEvents.PowerModeChanged += OnPowerModeChanged;
+ }
+
+ ///
+ /// Gets a value indicating whether the watcher is currently running.
+ ///
+ public bool IsRunning => _isRunning;
+
+ ///
+ /// Starts watching for display changes.
+ ///
+ public void Start()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ if (_isRunning)
+ {
+ return;
+ }
+
+ try
+ {
+ // Get the device selector for display monitors
+ string selector = DisplayMonitor.GetDeviceSelector();
+
+ // Create the device watcher
+ _deviceWatcher = DeviceInformation.CreateWatcher(selector);
+
+ // Subscribe to events
+ _deviceWatcher.Added += OnDeviceAdded;
+ _deviceWatcher.Removed += OnDeviceRemoved;
+ _deviceWatcher.Updated += OnDeviceUpdated;
+ _deviceWatcher.EnumerationCompleted += OnEnumerationCompleted;
+ _deviceWatcher.Stopped += OnWatcherStopped;
+
+ // Reset state before starting (must be before Start() to avoid race)
+ _initialEnumerationComplete = false;
+ _isRunning = true;
+
+ // Start watching
+ _deviceWatcher.Start();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[DisplayChangeWatcher] Failed to start: {ex.Message}");
+ _isRunning = false;
+ }
+ }
+
+ ///
+ /// Stops watching for display changes.
+ ///
+ public void Stop()
+ {
+ if (!_isRunning || _deviceWatcher == null)
+ {
+ return;
+ }
+
+ try
+ {
+ // Cancel any pending debounce
+ CancelDebounce();
+
+ // Stop the watcher
+ _deviceWatcher.Stop();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[DisplayChangeWatcher] Error stopping watcher: {ex.Message}");
+ }
+ }
+
+ private void OnDeviceAdded(DeviceWatcher sender, DeviceInformation args)
+ {
+ // Dispatch to UI thread to ensure thread-safe state access
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ // Ignore events during initial enumeration or after disposal
+ if (_disposed || !_initialEnumerationComplete)
+ {
+ return;
+ }
+
+ ScheduleDisplayChanged();
+ });
+ }
+
+ private void OnDeviceRemoved(DeviceWatcher sender, DeviceInformationUpdate args)
+ {
+ // Dispatch to UI thread to ensure thread-safe state access
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ // Ignore events during initial enumeration or after disposal
+ if (_disposed || !_initialEnumerationComplete)
+ {
+ return;
+ }
+
+ ScheduleDisplayChanged();
+ });
+ }
+
+ private void OnDeviceUpdated(DeviceWatcher sender, DeviceInformationUpdate args)
+ {
+ // Only trigger refresh for significant updates, not every property change.
+ // For now, we'll skip updates to avoid excessive refreshes.
+ // The Added and Removed events are the primary triggers for monitor changes.
+ }
+
+ private void OnEnumerationCompleted(DeviceWatcher sender, object args)
+ {
+ // Dispatch to UI thread to ensure thread-safe state access
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ _initialEnumerationComplete = true;
+ });
+ }
+
+ private void OnWatcherStopped(DeviceWatcher sender, object args)
+ {
+ // Dispatch to UI thread to ensure thread-safe state access
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ _isRunning = false;
+
+ // If not disposed, this is an unexpected stop (e.g., during sleep/wake)
+ // Try to auto-restart the watcher
+ if (!_disposed)
+ {
+ Logger.LogInfo("[DisplayChangeWatcher] Watcher stopped unexpectedly, attempting restart");
+
+ // Clean up the old watcher
+ CleanupDeviceWatcher();
+
+ // Restart after a short delay to allow system to stabilize
+ Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromSeconds(1));
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ if (!_disposed && !_isRunning)
+ {
+ Start();
+ }
+ });
+ });
+ }
+ else
+ {
+ _initialEnumerationComplete = false;
+ }
+ });
+ }
+
+ ///
+ /// Handles system power mode changes (suspend/resume).
+ ///
+ private void OnPowerModeChanged(object? sender, PowerModeChangedEventArgs e)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (e.Mode == PowerModes.Resume)
+ {
+ Logger.LogInfo("[DisplayChangeWatcher] System resumed from sleep, scheduling display refresh");
+
+ // Schedule a display refresh after system resumes
+ // Use a longer delay to allow hardware to fully reinitialize
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ if (!_disposed)
+ {
+ // Trigger a display changed event after wake-up
+ // The debounce mechanism will handle rapid successive events
+ ScheduleDisplayChanged();
+ }
+ });
+ }
+ }
+
+ ///
+ /// Cleans up the current device watcher and unsubscribes from events.
+ ///
+ private void CleanupDeviceWatcher()
+ {
+ if (_deviceWatcher != null)
+ {
+ try
+ {
+ _deviceWatcher.Added -= OnDeviceAdded;
+ _deviceWatcher.Removed -= OnDeviceRemoved;
+ _deviceWatcher.Updated -= OnDeviceUpdated;
+ _deviceWatcher.EnumerationCompleted -= OnEnumerationCompleted;
+ _deviceWatcher.Stopped -= OnWatcherStopped;
+ }
+ catch
+ {
+ // Ignore errors during cleanup
+ }
+
+ _deviceWatcher = null;
+ }
+ }
+
+ ///
+ /// Schedules a DisplayChanged event with debouncing.
+ /// Multiple rapid changes will only trigger one event after the debounce period.
+ ///
+ private void ScheduleDisplayChanged()
+ {
+ // Cancel any pending debounce
+ CancelDebounce();
+
+ // Create new cancellation token
+ _debounceCts = new CancellationTokenSource();
+ var token = _debounceCts.Token;
+
+ // Schedule the event after debounce delay
+ Task.Run(async () =>
+ {
+ try
+ {
+ await Task.Delay(_debounceDelay, token);
+
+ if (!token.IsCancellationRequested)
+ {
+ // Dispatch to UI thread
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ if (!_disposed)
+ {
+ DisplayChanged?.Invoke(this, EventArgs.Empty);
+ }
+ });
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Debounce was cancelled by a newer event, this is expected
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[DisplayChangeWatcher] Error in debounce task: {ex.Message}");
+ }
+ });
+ }
+
+ private void CancelDebounce()
+ {
+ try
+ {
+ _debounceCts?.Cancel();
+ _debounceCts?.Dispose();
+ _debounceCts = null;
+ }
+ catch (ObjectDisposedException)
+ {
+ // Already disposed, ignore
+ }
+ }
+
+ ///
+ /// Disposes resources used by the watcher.
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ // Unsubscribe from power mode changes
+ SystemEvents.PowerModeChanged -= OnPowerModeChanged;
+
+ // Stop watching
+ Stop();
+
+ // Unsubscribe from device watcher events
+ CleanupDeviceWatcher();
+
+ // Cancel debounce
+ CancelDebounce();
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/HotkeyService.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/HotkeyService.cs
new file mode 100644
index 0000000000..63ef27c054
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/HotkeyService.cs
@@ -0,0 +1,177 @@
+// 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;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using WinRT.Interop;
+
+namespace PowerDisplay.Helpers
+{
+ ///
+ /// Service for handling hotkey registration in-process.
+ /// Uses RegisterHotKey Win32 API instead of Runner's centralized mechanism
+ /// to avoid IPC timing issues (CmdPal pattern).
+ ///
+ internal sealed partial class HotkeyService : IDisposable
+ {
+ private const int HotkeyId = 9001;
+
+ private readonly SettingsUtils _settingsUtils;
+ private readonly Action _hotkeyAction;
+
+ private nint _hwnd;
+ private nint _originalWndProc;
+
+ // Must keep delegate reference to prevent GC collection
+ private WndProcDelegate? _hotkeyWndProc;
+ private bool _isRegistered;
+ private bool _disposed;
+
+ public HotkeyService(SettingsUtils settingsUtils, Action hotkeyAction)
+ {
+ _settingsUtils = settingsUtils;
+ _hotkeyAction = hotkeyAction;
+ }
+
+ ///
+ /// Initialize the hotkey service with a window handle.
+ /// Must be called after window is created.
+ ///
+ /// The WinUI window to attach to.
+ public void Initialize(Microsoft.UI.Xaml.Window window)
+ {
+ _hwnd = WindowNative.GetWindowHandle(window);
+
+ // LOAD BEARING: If you don't stick the pointer to the WndProc into a
+ // member (and instead use a local), then the pointer we marshal
+ // into the WindowLongPtr will be useless after we leave this function,
+ // and our WndProc will explode.
+ _hotkeyWndProc = HotkeyWndProc;
+ var wndProcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
+ _originalWndProc = SetWindowLongPtrNative(_hwnd, GwlWndProc, wndProcPointer);
+
+ // Register hotkey based on current settings
+ ReloadSettings();
+ }
+
+ ///
+ /// Reload settings and re-register hotkey.
+ /// Call this when settings change.
+ ///
+ public void ReloadSettings()
+ {
+ UnregisterHotkey();
+
+ var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
+ var hotkey = settings?.Properties?.ActivationShortcut;
+
+ if (hotkey == null || !hotkey.IsValid())
+ {
+ return;
+ }
+
+ RegisterHotkey(hotkey);
+ }
+
+ private void RegisterHotkey(HotkeySettings hotkey)
+ {
+ if (_hwnd == 0)
+ {
+ Logger.LogWarning("[HotkeyService] Cannot register hotkey: window handle not set");
+ return;
+ }
+
+ // Build modifiers using bit flags
+ uint modifiers = ModNoRepeat
+ | (hotkey.Win ? ModWin : 0)
+ | (hotkey.Ctrl ? ModControl : 0)
+ | (hotkey.Alt ? ModAlt : 0)
+ | (hotkey.Shift ? ModShift : 0);
+
+ if (RegisterHotKeyNative(_hwnd, HotkeyId, modifiers, (uint)hotkey.Code))
+ {
+ _isRegistered = true;
+ }
+ else
+ {
+ Logger.LogError($"[HotkeyService] Failed to register hotkey: {hotkey}, error={Marshal.GetLastWin32Error()}");
+ }
+ }
+
+ private void UnregisterHotkey()
+ {
+ if (!_isRegistered || _hwnd == 0)
+ {
+ return;
+ }
+
+ bool success = UnregisterHotKeyNative(_hwnd, HotkeyId);
+
+ if (!success)
+ {
+ var error = Marshal.GetLastWin32Error();
+ Logger.LogWarning($"[HotkeyService] Failed to unregister hotkey, error={error}");
+ }
+
+ _isRegistered = false;
+ }
+
+ private nint HotkeyWndProc(nint hwnd, uint uMsg, nuint wParam, nint lParam)
+ {
+ if (uMsg == WmHotkey && (int)wParam == HotkeyId)
+ {
+ try
+ {
+ _hotkeyAction?.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[HotkeyService] Hotkey action failed: {ex.Message}");
+ }
+
+ return 0;
+ }
+
+ return CallWindowProcNative(_originalWndProc, hwnd, uMsg, wParam, lParam);
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ UnregisterHotkey();
+ _disposed = true;
+ }
+
+ // P/Invoke constants
+ private const int GwlWndProc = -4;
+ private const uint WmHotkey = 0x0312;
+
+ // HOT_KEY_MODIFIERS flags
+ private const uint ModAlt = 0x0001;
+ private const uint ModControl = 0x0002;
+ private const uint ModShift = 0x0004;
+ private const uint ModWin = 0x0008;
+ private const uint ModNoRepeat = 0x4000;
+
+ [LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
+ private static partial nint SetWindowLongPtrNative(nint hWnd, int nIndex, nint dwNewLong);
+
+ [LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
+ private static partial nint CallWindowProcNative(nint lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
+
+ [LibraryImport("user32.dll", EntryPoint = "RegisterHotKey", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool RegisterHotKeyNative(nint hWnd, int id, uint fsModifiers, uint vk);
+
+ [LibraryImport("user32.dll", EntryPoint = "UnregisterHotKey", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool UnregisterHotKeyNative(nint hWnd, int id);
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorManager.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorManager.cs
new file mode 100644
index 0000000000..1f9a353303
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorManager.cs
@@ -0,0 +1,460 @@
+// 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 PowerDisplay.Common.Drivers;
+using PowerDisplay.Common.Drivers.DDC;
+using PowerDisplay.Common.Drivers.WMI;
+using PowerDisplay.Common.Interfaces;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Common.Services;
+using PowerDisplay.Common.Utils;
+using Monitor = PowerDisplay.Common.Models.Monitor;
+
+namespace PowerDisplay.Helpers
+{
+ ///
+ /// Monitor manager for unified control of all monitors
+ /// No interface abstraction - KISS principle (only one implementation needed)
+ ///
+ public partial class MonitorManager : IDisposable
+ {
+ private readonly List _monitors = new();
+ private readonly Dictionary _monitorLookup = new();
+ private readonly SemaphoreSlim _discoveryLock = new(1, 1);
+ private readonly DisplayRotationService _rotationService = new();
+
+ // Controllers stored by type for O(1) lookup based on CommunicationMethod
+ private DdcCiController? _ddcController;
+ private WmiController? _wmiController;
+ private bool _disposed;
+
+ public IReadOnlyList Monitors => _monitors.AsReadOnly();
+
+ public MonitorManager()
+ {
+ // Initialize controllers
+ InitializeControllers();
+ }
+
+ ///
+ /// Initialize controllers
+ ///
+ private void InitializeControllers()
+ {
+ try
+ {
+ // DDC/CI controller (external monitors)
+ _ddcController = new DdcCiController();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
+ }
+
+ try
+ {
+ // WMI controller (internal monitors)
+ // Always create - DiscoverMonitorsAsync returns empty list if WMI is unavailable
+ _wmiController = new WmiController();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Discover all monitors from all controllers.
+ /// Each controller is responsible for fully initializing its monitors
+ /// (including brightness, capabilities, input source, color temperature, etc.)
+ ///
+ public async Task> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
+ {
+ await _discoveryLock.WaitAsync(cancellationToken);
+
+ try
+ {
+ var discoveredMonitors = await DiscoverFromAllControllersAsync(cancellationToken);
+
+ // Update collections
+ _monitors.Clear();
+ _monitorLookup.Clear();
+
+ var sortedMonitors = discoveredMonitors
+ .OrderBy(m => m.MonitorNumber)
+ .ToList();
+
+ _monitors.AddRange(sortedMonitors);
+ foreach (var monitor in sortedMonitors)
+ {
+ _monitorLookup[monitor.Id] = monitor;
+ }
+
+ return _monitors.AsReadOnly();
+ }
+ finally
+ {
+ _discoveryLock.Release();
+ }
+ }
+
+ ///
+ /// Discover monitors from all registered controllers in parallel.
+ ///
+ private async Task> DiscoverFromAllControllersAsync(CancellationToken cancellationToken)
+ {
+ var tasks = new List>>();
+
+ if (_ddcController != null)
+ {
+ tasks.Add(SafeDiscoverAsync(_ddcController, cancellationToken));
+ }
+
+ if (_wmiController != null)
+ {
+ tasks.Add(SafeDiscoverAsync(_wmiController, cancellationToken));
+ }
+
+ var results = await Task.WhenAll(tasks);
+ return results.SelectMany(m => m).ToList();
+ }
+
+ ///
+ /// Safely discover monitors from a controller, returning empty list on failure.
+ ///
+ private static async Task> SafeDiscoverAsync(
+ IMonitorController controller,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ return await controller.DiscoverMonitorsAsync(cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}");
+ return Enumerable.Empty();
+ }
+ }
+
+ ///
+ /// Get brightness of the specified monitor
+ ///
+ public async Task GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
+ {
+ var monitor = GetMonitor(monitorId);
+ if (monitor == null)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+
+ var controller = GetControllerForMonitor(monitor);
+ if (controller == null)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+
+ try
+ {
+ var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
+
+ // Update cached brightness value
+ if (brightnessInfo.IsValid)
+ {
+ monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
+ }
+
+ return brightnessInfo;
+ }
+ catch (Exception ex)
+ {
+ // Mark monitor as unavailable
+ Logger.LogError($"Failed to get brightness for monitor {monitorId}: {ex.Message}");
+ monitor.IsAvailable = false;
+ return VcpFeatureValue.Invalid;
+ }
+ }
+
+ ///
+ /// Set brightness of the specified monitor
+ ///
+ public Task SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
+ => ExecuteMonitorOperationAsync(
+ monitorId,
+ brightness,
+ (ctrl, mon, val, ct) => ctrl.SetBrightnessAsync(mon, val, ct),
+ (mon, val) => mon.UpdateStatus(val, true),
+ cancellationToken);
+
+ ///
+ /// Set contrast of the specified monitor
+ ///
+ public Task SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
+ => ExecuteMonitorOperationAsync(
+ monitorId,
+ contrast,
+ (ctrl, mon, val, ct) => ctrl.SetContrastAsync(mon, val, ct),
+ (mon, val) => mon.CurrentContrast = val,
+ cancellationToken);
+
+ ///
+ /// Set volume of the specified monitor
+ ///
+ public Task SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
+ => ExecuteMonitorOperationAsync(
+ monitorId,
+ volume,
+ (ctrl, mon, val, ct) => ctrl.SetVolumeAsync(mon, val, ct),
+ (mon, val) => mon.CurrentVolume = val,
+ cancellationToken);
+
+ ///
+ /// Get monitor color temperature
+ ///
+ public async Task GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
+ {
+ var monitor = GetMonitor(monitorId);
+ if (monitor == null)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+
+ var controller = GetControllerForMonitor(monitor);
+ if (controller == null)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+
+ try
+ {
+ return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
+ }
+ catch (Exception ex) when (ex is not OutOfMemoryException)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+ }
+
+ ///
+ /// Set monitor color temperature
+ ///
+ public Task SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
+ => ExecuteMonitorOperationAsync(
+ monitorId,
+ colorTemperature,
+ (ctrl, mon, val, ct) => ctrl.SetColorTemperatureAsync(mon, val, ct),
+ (mon, val) => mon.CurrentColorTemperature = val,
+ cancellationToken);
+
+ ///
+ /// Get current input source for a monitor
+ ///
+ public async Task GetInputSourceAsync(string monitorId, CancellationToken cancellationToken = default)
+ {
+ var monitor = GetMonitor(monitorId);
+ if (monitor == null)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+
+ var controller = GetControllerForMonitor(monitor);
+ if (controller == null)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+
+ try
+ {
+ return await controller.GetInputSourceAsync(monitor, cancellationToken);
+ }
+ catch (Exception ex) when (ex is not OutOfMemoryException)
+ {
+ return VcpFeatureValue.Invalid;
+ }
+ }
+
+ ///
+ /// Set input source for a monitor
+ ///
+ public Task SetInputSourceAsync(string monitorId, int inputSource, CancellationToken cancellationToken = default)
+ => ExecuteMonitorOperationAsync(
+ monitorId,
+ inputSource,
+ (ctrl, mon, val, ct) => ctrl.SetInputSourceAsync(mon, val, ct),
+ (mon, val) => mon.CurrentInputSource = val,
+ cancellationToken);
+
+ ///
+ /// Set power state for a monitor using VCP 0xD6.
+ /// Note: Setting any state other than On (0x01) will turn off the display.
+ /// We don't update monitor state since the display will be off.
+ ///
+ public Task SetPowerStateAsync(string monitorId, int powerState, CancellationToken cancellationToken = default)
+ => ExecuteMonitorOperationAsync(
+ monitorId,
+ powerState,
+ (ctrl, mon, val, ct) => ctrl.SetPowerStateAsync(mon, val, ct),
+ (mon, val) => { }, // No state update - display will be off for non-On values
+ cancellationToken);
+
+ ///
+ /// Set rotation/orientation for a monitor.
+ /// Uses Windows ChangeDisplaySettingsEx API (not DDC/CI).
+ /// After successful rotation, refreshes orientation for all monitors sharing the same GdiDeviceName
+ /// (important for mirror/clone mode where multiple monitors share one display source).
+ ///
+ /// Monitor ID
+ /// Orientation: 0=normal, 1=90°, 2=180°, 3=270°
+ /// Cancellation token
+ /// Operation result
+ public Task SetRotationAsync(string monitorId, int orientation, CancellationToken cancellationToken = default)
+ {
+ var monitor = GetMonitor(monitorId);
+ if (monitor == null)
+ {
+ Logger.LogError($"[MonitorManager] SetRotation: Monitor not found: {monitorId}");
+ return Task.FromResult(MonitorOperationResult.Failure("Monitor not found"));
+ }
+
+ // Rotation uses Windows display settings API, not DDC/CI controller
+ // Prefer using Monitor object which contains GdiDeviceName for accurate adapter targeting
+ var result = _rotationService.SetRotation(monitor, orientation);
+
+ if (result.IsSuccess)
+ {
+ // Refresh orientation for all monitors - rotation affects the GdiDeviceName (display source),
+ // and in mirror mode multiple monitors may share the same GdiDeviceName
+ RefreshAllOrientations();
+ }
+ else
+ {
+ Logger.LogError($"[MonitorManager] SetRotation: Failed for {monitorId}: {result.ErrorMessage}");
+ }
+
+ return Task.FromResult(result);
+ }
+
+ ///
+ /// Refresh orientation values for all monitors by querying current display settings.
+ /// This ensures all monitors reflect the actual system state, which is important
+ /// in mirror mode where multiple monitors share the same GdiDeviceName.
+ ///
+ public void RefreshAllOrientations()
+ {
+ foreach (var monitor in _monitors)
+ {
+ if (string.IsNullOrEmpty(monitor.GdiDeviceName))
+ {
+ continue;
+ }
+
+ var currentOrientation = _rotationService.GetCurrentOrientation(monitor.GdiDeviceName);
+ if (currentOrientation >= 0 && currentOrientation != monitor.Orientation)
+ {
+ monitor.Orientation = currentOrientation;
+ monitor.LastUpdate = DateTime.Now;
+ }
+ }
+ }
+
+ ///
+ /// Get monitor by ID. Uses dictionary lookup for O(1) performance.
+ ///
+ public Monitor? GetMonitor(string monitorId)
+ {
+ return _monitorLookup.TryGetValue(monitorId, out var monitor) ? monitor : null;
+ }
+
+ ///
+ /// Get controller for the monitor based on CommunicationMethod.
+ /// O(1) lookup - no async validation needed since controller type is determined at discovery.
+ ///
+ private IMonitorController? GetControllerForMonitor(Monitor monitor)
+ {
+ return monitor.CommunicationMethod switch
+ {
+ "WMI" => _wmiController,
+ "DDC/CI" => _ddcController,
+ _ => null,
+ };
+ }
+
+ ///
+ /// Generic helper to execute monitor operations with common error handling.
+ /// Eliminates code duplication across Set* methods.
+ ///
+ private async Task ExecuteMonitorOperationAsync(
+ string monitorId,
+ T value,
+ Func> operation,
+ Action onSuccess,
+ CancellationToken cancellationToken = default)
+ {
+ var monitor = GetMonitor(monitorId);
+ if (monitor == null)
+ {
+ Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
+ return MonitorOperationResult.Failure("Monitor not found");
+ }
+
+ var controller = GetControllerForMonitor(monitor);
+ if (controller == null)
+ {
+ Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}");
+ return MonitorOperationResult.Failure("No controller available for this monitor");
+ }
+
+ try
+ {
+ var result = await operation(controller, monitor, value, cancellationToken);
+
+ if (result.IsSuccess)
+ {
+ onSuccess(monitor, value);
+ monitor.LastUpdate = DateTime.Now;
+ }
+ else
+ {
+ monitor.IsAvailable = false;
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ monitor.IsAvailable = false;
+ Logger.LogError($"[MonitorManager] Operation failed for {monitorId}: {ex.Message}");
+ return MonitorOperationResult.Failure($"Exception: {ex.Message}");
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed && disposing)
+ {
+ _discoveryLock?.Dispose();
+
+ // Release controllers
+ _ddcController?.Dispose();
+ _wmiController?.Dispose();
+
+ _monitors.Clear();
+ _monitorLookup.Clear();
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/NamedPipeProcessor.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/NamedPipeProcessor.cs
new file mode 100644
index 0000000000..9945ad9da9
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/NamedPipeProcessor.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.IO.Pipes;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using ManagedCommon;
+
+namespace PowerDisplay.Helpers;
+
+///
+/// Processes messages from the Module DLL via Named Pipe.
+/// Based on AdvancedPaste NamedPipeProcessor pattern.
+///
+public static class NamedPipeProcessor
+{
+ ///
+ /// Connects to a named pipe and processes incoming messages.
+ /// This method runs continuously until cancelled or the pipe is disconnected.
+ ///
+ /// The name of the pipe to connect to.
+ /// Timeout for initial connection.
+ /// Handler for each received message.
+ /// Token to cancel the operation.
+ public static async Task ProcessNamedPipeAsync(
+ string pipeName,
+ TimeSpan connectTimeout,
+ Action messageHandler,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ using NamedPipeClientStream pipeClient = new(".", pipeName, PipeDirection.In);
+
+ Logger.LogInfo($"[NamedPipe] Connecting to pipe: {pipeName}");
+ await pipeClient.ConnectAsync(connectTimeout, cancellationToken);
+ Logger.LogInfo($"[NamedPipe] Connected to pipe: {pipeName}");
+
+ using StreamReader streamReader = new(pipeClient, Encoding.Unicode);
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ var message = await streamReader.ReadLineAsync(cancellationToken);
+
+ if (message != null)
+ {
+ Logger.LogInfo($"[NamedPipe] Received message: {message}");
+ messageHandler(message);
+ }
+
+ // Small delay to prevent tight loop
+ var intraMessageDelay = TimeSpan.FromMilliseconds(10);
+ await Task.Delay(intraMessageDelay, cancellationToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ Logger.LogInfo("[NamedPipe] Processing cancelled");
+ }
+ catch (IOException ex)
+ {
+ // Pipe disconnected, this is expected when the module DLL terminates
+ Logger.LogInfo($"[NamedPipe] Pipe disconnected: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[NamedPipe] Error processing pipe: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/NativeEventWaiter.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/NativeEventWaiter.cs
new file mode 100644
index 0000000000..33ef13664c
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/NativeEventWaiter.cs
@@ -0,0 +1,89 @@
+// 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.Threading;
+using ManagedCommon;
+using Microsoft.UI.Dispatching;
+
+namespace PowerDisplay.Helpers
+{
+ ///
+ /// Helper class for waiting on Windows Named Events (Awake pattern)
+ /// Based on Peek.UI implementation
+ ///
+ public static class NativeEventWaiter
+ {
+ ///
+ /// Wait for a Windows Event in a background thread and invoke callback on UI thread when signaled
+ ///
+ /// Name of the Windows Event to wait for
+ /// Callback to invoke when event is signaled
+ /// Token to cancel the wait loop
+ public static void WaitForEventLoop(string eventName, Action callback, CancellationToken cancellationToken)
+ {
+ Logger.LogTrace($"[NativeEventWaiter] Setting up event loop for event: {eventName}");
+ var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+
+ if (dispatcherQueue == null)
+ {
+ Logger.LogError($"[NativeEventWaiter] DispatcherQueue is null for event: {eventName}");
+ return;
+ }
+
+ Logger.LogTrace($"[NativeEventWaiter] DispatcherQueue obtained for event: {eventName}");
+
+ var t = new Thread(() =>
+ {
+ Logger.LogInfo($"[NativeEventWaiter] Background thread started for event: {eventName}");
+ try
+ {
+ Logger.LogTrace($"[NativeEventWaiter] Creating EventWaitHandle for event: {eventName}");
+ using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
+ Logger.LogInfo($"[NativeEventWaiter] EventWaitHandle created successfully for event: {eventName}");
+
+ int waitCount = 0;
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ // Use 500ms timeout for polling
+ if (eventHandle.WaitOne(500))
+ {
+ waitCount++;
+ Logger.LogInfo($"[NativeEventWaiter] Event SIGNALED: {eventName} (signal count: {waitCount})");
+ bool enqueued = dispatcherQueue.TryEnqueue(() =>
+ {
+ Logger.LogTrace($"[NativeEventWaiter] Executing callback on UI thread for event: {eventName}");
+ try
+ {
+ callback();
+ Logger.LogTrace($"[NativeEventWaiter] Callback completed for event: {eventName}");
+ }
+ catch (Exception callbackEx)
+ {
+ Logger.LogError($"[NativeEventWaiter] Callback exception for event {eventName}: {callbackEx.Message}");
+ }
+ });
+
+ if (!enqueued)
+ {
+ Logger.LogError($"[NativeEventWaiter] Failed to enqueue callback to UI thread for event: {eventName}");
+ }
+ }
+ }
+
+ Logger.LogInfo($"[NativeEventWaiter] Event loop ending for event: {eventName} (cancellation requested)");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[NativeEventWaiter] Exception in event loop for {eventName}: {ex.Message}\n{ex.StackTrace}");
+ }
+ });
+
+ t.IsBackground = true;
+ t.Name = $"NativeEventWaiter_{eventName}";
+ t.Start();
+ Logger.LogTrace($"[NativeEventWaiter] Background thread started with name: {t.Name}");
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/ResourceLoaderInstance.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/ResourceLoaderInstance.cs
new file mode 100644
index 0000000000..cb549336fd
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/ResourceLoaderInstance.cs
@@ -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 Microsoft.Windows.ApplicationModel.Resources;
+
+namespace PowerDisplay.Helpers
+{
+ public static class ResourceLoaderInstance
+ {
+ public static ResourceLoader ResourceLoader { get; private set; }
+
+ static ResourceLoaderInstance()
+ {
+ ResourceLoader = new ResourceLoader("PowerToys.PowerDisplay.pri");
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/SettingsDeepLink.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/SettingsDeepLink.cs
new file mode 100644
index 0000000000..d721fa5811
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/SettingsDeepLink.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.IO;
+
+namespace PowerDisplay.Helpers
+{
+ public static class SettingsDeepLink
+ {
+ public static void OpenSettings(bool mainExecutableIsOnTheParentFolder)
+ {
+ try
+ {
+ var directoryPath = System.AppContext.BaseDirectory;
+ if (mainExecutableIsOnTheParentFolder)
+ {
+ // Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
+ directoryPath = Path.Combine(directoryPath, "..");
+ directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
+ }
+ else
+ {
+ // PowerToys.exe is in the same path as the application.
+ directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
+ }
+
+ Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=PowerDisplay" });
+ }
+ catch
+ {
+ // Silently ignore errors opening settings
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconService.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconService.cs
new file mode 100644
index 0000000000..ab8dca6b80
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconService.cs
@@ -0,0 +1,319 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.InteropServices;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.UI.Xaml;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.Shell;
+using Windows.Win32.UI.WindowsAndMessaging;
+using WinRT.Interop;
+
+namespace PowerDisplay.Helpers
+{
+ ///
+ /// Window procedure delegate for handling window messages.
+ /// Uses primitive types to avoid accessibility issues with CsWin32-generated types.
+ ///
+ /// Handle to the window.
+ /// The message.
+ /// Additional message information.
+ /// Additional message.
+ /// The result of the message processing.
+ internal delegate nint WndProcDelegate(nint hwnd, uint msg, nuint wParam, nint lParam);
+
+ internal sealed partial class TrayIconService
+ {
+ private const uint MyNotifyId = 1001;
+ private const uint WmTrayIcon = PInvoke.WM_USER + 1;
+
+ private readonly SettingsUtils _settingsUtils;
+ private readonly Action _toggleWindowAction;
+ private readonly Action _exitAction;
+ private readonly Action _openSettingsAction;
+ private readonly uint _wmTaskbarRestart;
+
+ private Window? _window;
+ private nint _hwnd;
+ private nint _originalWndProc;
+ private WndProcDelegate? _trayWndProc;
+ private NOTIFYICONDATAW? _trayIconData;
+ private nint _largeIcon;
+ private nint _popupMenu;
+
+ public TrayIconService(
+ SettingsUtils settingsUtils,
+ Action toggleWindowAction,
+ Action exitAction,
+ Action openSettingsAction)
+ {
+ _settingsUtils = settingsUtils;
+ _toggleWindowAction = toggleWindowAction;
+ _exitAction = exitAction;
+ _openSettingsAction = openSettingsAction;
+
+ // TaskbarCreated is the message that's broadcast when explorer.exe
+ // restarts. We need to know when that happens to be able to bring our
+ // notification area icon back
+ _wmTaskbarRestart = RegisterWindowMessageNative("TaskbarCreated");
+ }
+
+ public void SetupTrayIcon(bool? showSystemTrayIcon = null)
+ {
+ var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
+ bool shouldShow = showSystemTrayIcon ?? settings.Properties.ShowSystemTrayIcon;
+
+ if (shouldShow)
+ {
+ if (_window is null)
+ {
+ _window = new Window();
+ _hwnd = WindowNative.GetWindowHandle(_window);
+
+ // LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
+ // member (and instead like, use a local), then the pointer we marshal
+ // into the WindowLongPtr will be useless after we leave this function,
+ // and our **WindProc will explode**.
+ _trayWndProc = WindowProc;
+ var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_trayWndProc);
+ _originalWndProc = SetWindowLongPtrNative(_hwnd, GwlWndproc, hotKeyPrcPointer);
+ }
+
+ if (_trayIconData is null)
+ {
+ // We need to stash this handle, so it doesn't clean itself up. If
+ // explorer restarts, we'll come back through here, and we don't
+ // really need to re-load the icon in that case. We can just use
+ // the handle from the first time.
+ _largeIcon = GetAppIconHandle();
+ unsafe
+ {
+ _trayIconData = new NOTIFYICONDATAW()
+ {
+ cbSize = (uint)sizeof(NOTIFYICONDATAW),
+ hWnd = new HWND(_hwnd),
+ uID = MyNotifyId,
+ uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_ICON | NOTIFY_ICON_DATA_FLAGS.NIF_TIP,
+ uCallbackMessage = WmTrayIcon,
+ hIcon = new HICON(_largeIcon),
+ szTip = GetString("AppName"),
+ };
+ }
+ }
+
+ var d = (NOTIFYICONDATAW)_trayIconData;
+
+ // Add the notification icon
+ unsafe
+ {
+ bool success = Shell_NotifyIconNative((uint)NOTIFY_ICON_MESSAGE.NIM_ADD, &d);
+ if (!success)
+ {
+ // Shell_NotifyIcon can fail if explorer.exe isn't ready yet (e.g., during system startup)
+ // Reset _trayIconData to allow retry via WM_WINDOWPOSCHANGING or WM_TASKBAR_RESTART
+ Logger.LogWarning("[TrayIcon] Shell_NotifyIcon(NIM_ADD) failed, will retry later");
+ _trayIconData = null;
+ return;
+ }
+ }
+
+ if (_popupMenu == 0)
+ {
+ _popupMenu = CreatePopupMenu();
+ InsertMenuNative(_popupMenu, 0, (uint)(MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING), PInvoke.WM_USER + 1, GetString("TrayMenu_Settings"));
+ InsertMenuNative(_popupMenu, 1, (uint)(MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING), PInvoke.WM_USER + 2, GetString("TrayMenu_Exit"));
+ }
+ }
+ else
+ {
+ Destroy();
+ }
+ }
+
+ public void Destroy()
+ {
+ if (_trayIconData is not null)
+ {
+ var d = (NOTIFYICONDATAW)_trayIconData;
+ unsafe
+ {
+ if (Shell_NotifyIconNative((uint)NOTIFY_ICON_MESSAGE.NIM_DELETE, &d))
+ {
+ _trayIconData = null;
+ }
+ }
+ }
+
+ if (_popupMenu != 0)
+ {
+ DestroyMenu(_popupMenu);
+ _popupMenu = 0;
+ }
+
+ if (_largeIcon != 0)
+ {
+ DestroyIcon(_largeIcon);
+ _largeIcon = 0;
+ }
+
+ if (_window is not null)
+ {
+ _window.Close();
+ _window = null;
+ _hwnd = 0;
+ }
+ }
+
+ private static string GetString(string key)
+ {
+ try
+ {
+ return ResourceLoaderInstance.ResourceLoader.GetString(key);
+ }
+ catch
+ {
+ return "unknown";
+ }
+ }
+
+ private nint GetAppIconHandle()
+ {
+ var exePath = Path.Combine(AppContext.BaseDirectory, "PowerToys.PowerDisplay.exe");
+ ExtractIconExNative(exePath, 0, out var largeIcon, out _, 1);
+ return largeIcon;
+ }
+
+ private nint WindowProc(
+ nint hwnd,
+ uint uMsg,
+ nuint wParam,
+ nint lParam)
+ {
+ switch (uMsg)
+ {
+ case PInvoke.WM_COMMAND:
+ {
+ if (wParam == PInvoke.WM_USER + 1)
+ {
+ // Settings menu item
+ _openSettingsAction?.Invoke();
+ }
+ else if (wParam == PInvoke.WM_USER + 2)
+ {
+ // Exit menu item
+ _exitAction?.Invoke();
+ }
+ }
+
+ break;
+
+ // Shell_NotifyIcon can fail when we invoke it during the time explorer.exe isn't present/ready to handle it.
+ // We'll also never receive _wmTaskbarRestart message if the first call to Shell_NotifyIcon failed, so we use
+ // WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
+ case PInvoke.WM_WINDOWPOSCHANGING:
+ {
+ if (_trayIconData is null)
+ {
+ SetupTrayIcon();
+ }
+ }
+
+ break;
+ default:
+ // _wmTaskbarRestart isn't a compile-time constant, so we can't
+ // use it in a case label
+ if (uMsg == _wmTaskbarRestart)
+ {
+ // Handle the case where explorer.exe restarts.
+ // Even if we created it before, do it again
+ SetupTrayIcon();
+ }
+ else if (uMsg == WmTrayIcon)
+ {
+ switch ((uint)lParam)
+ {
+ case PInvoke.WM_RBUTTONUP:
+ {
+ if (_popupMenu != 0)
+ {
+ GetCursorPos(out var cursorPos);
+ SetForegroundWindow(_hwnd);
+ TrackPopupMenuExNative(_popupMenu, (uint)TRACK_POPUP_MENU_FLAGS.TPM_LEFTALIGN | (uint)TRACK_POPUP_MENU_FLAGS.TPM_BOTTOMALIGN, cursorPos.X, cursorPos.Y, _hwnd, 0);
+ }
+ }
+
+ break;
+ case PInvoke.WM_LBUTTONUP:
+ case PInvoke.WM_LBUTTONDBLCLK:
+ _toggleWindowAction?.Invoke();
+ break;
+ }
+ }
+
+ break;
+ }
+
+ return CallWindowProcIntPtr(_originalWndProc, hwnd, uMsg, wParam, lParam);
+ }
+
+ [LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
+ private static partial nint CallWindowProcIntPtr(IntPtr lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
+
+ [LibraryImport("user32.dll", EntryPoint = "RegisterWindowMessageW", StringMarshalling = StringMarshalling.Utf16)]
+ private static partial uint RegisterWindowMessageNative(string lpString);
+
+ [LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
+ private static partial nint SetWindowLongPtrNative(nint hWnd, int nIndex, nint dwNewLong);
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool GetCursorPos(out POINT lpPoint);
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool SetForegroundWindow(nint hWnd);
+
+ // Shell APIs - use uint for enums and unsafe pointer for struct
+ [LibraryImport("shell32.dll", EntryPoint = "Shell_NotifyIconW")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static unsafe partial bool Shell_NotifyIconNative(uint dwMessage, NOTIFYICONDATAW* lpData);
+
+ [LibraryImport("shell32.dll", EntryPoint = "ExtractIconExW", StringMarshalling = StringMarshalling.Utf16)]
+ private static partial uint ExtractIconExNative(string lpszFile, int nIconIndex, out nint phiconLarge, out nint phiconSmall, uint nIcons);
+
+ // Menu APIs
+ [LibraryImport("user32.dll")]
+ private static partial nint CreatePopupMenu();
+
+ [LibraryImport("user32.dll", EntryPoint = "InsertMenuW", StringMarshalling = StringMarshalling.Utf16)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool InsertMenuNative(nint hMenu, uint uPosition, uint uFlags, nuint uIDNewItem, string? lpNewItem);
+
+ [LibraryImport("user32.dll", EntryPoint = "TrackPopupMenuEx")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool TrackPopupMenuExNative(nint hMenu, uint uFlags, int x, int y, nint hwnd, nint lptpm);
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool DestroyMenu(nint hMenu);
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool DestroyIcon(nint hIcon);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ private const int GwlWndproc = -4;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/TypePreservation.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/TypePreservation.cs
new file mode 100644
index 0000000000..6ae3be94dc
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/TypePreservation.cs
@@ -0,0 +1,81 @@
+// 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;
+
+namespace PowerDisplay.Helpers
+{
+ ///
+ /// This class ensures types used in XAML are preserved during AOT compilation.
+ /// Framework types cannot have attributes added directly to their definitions since they're external types.
+ /// Use DynamicDependency to preserve all members of these WinUI3 framework types.
+ ///
+ internal static class TypePreservation
+ {
+ // Core WinUI3 Controls used in XAML
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Window))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Application))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Grid))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Border))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ScrollViewer))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.StackPanel))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ItemsControl))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Slider))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.TextBlock))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Button))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIcon))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ProgressRing))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.InfoBar))]
+
+ // Animation and Transform types
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.Storyboard))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.DoubleAnimation))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.CubicEase))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.TranslateTransform))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.TransitionCollection))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.EntranceThemeTransition))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.RepositionThemeTransition))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.EasingFunctionBase))]
+
+ // Template and Resource types
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DataTemplate))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ItemsPanelTemplate))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Style))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIconSource))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.ResourceDictionary))]
+
+ // Text and Document types
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Documents.Run))]
+
+ // Layout types
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.RowDefinition))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ColumnDefinition))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.RowDefinitionCollection))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ColumnDefinitionCollection))]
+
+ // Media types for brushes and visuals
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.SolidColorBrush))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Brush))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Transform))]
+
+ // Core UI element types
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.UIElement))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.FrameworkElement))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DependencyObject))]
+
+ // Thickness and other value types used in XAML (structs, not enums)
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Thickness))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.CornerRadius))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.GridLength))]
+
+ // ToolTip service used in buttons
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ToolTipService))]
+
+ public static void PreserveTypes()
+ {
+ // This method exists only to hold the DynamicDependency attributes above.
+ // It must be called to ensure the types are not trimmed during AOT compilation.
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/VisibilityConverter.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/VisibilityConverter.cs
new file mode 100644
index 0000000000..ef3314f440
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/VisibilityConverter.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.UI.Xaml;
+
+namespace PowerDisplay.Helpers
+{
+ ///
+ /// Provides conversion utilities for Visibility binding in x:Bind scenarios.
+ /// AOT-compatible alternative to IValueConverter implementations.
+ ///
+ public static class VisibilityConverter
+ {
+ ///
+ /// Converts a boolean value to a Visibility value.
+ ///
+ /// The boolean value to convert.
+ /// Visibility.Visible if true, Visibility.Collapsed if false.
+ public static Visibility BoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/WindowHelper.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/WindowHelper.cs
new file mode 100644
index 0000000000..b5ae7a391f
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Helpers/WindowHelper.cs
@@ -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.Runtime.InteropServices;
+using Microsoft.UI;
+using Microsoft.UI.Windowing;
+using WinUIEx;
+
+namespace PowerDisplay.Helpers
+{
+ internal static partial class WindowHelper
+ {
+ // Cursor position structure for GetCursorPos
+ [StructLayout(LayoutKind.Sequential)]
+ private struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ // Cursor position for detecting the monitor with the mouse
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool GetCursorPos(out POINT lpPoint);
+
+ // Window Styles
+ private const int GwlStyle = -16;
+ private const int WsCaption = 0x00C00000;
+ private const int WsThickframe = 0x00040000;
+ private const int WsMinimizebox = 0x00020000;
+ private const int WsMaximizebox = 0x00010000;
+ private const int WsSysmenu = 0x00080000;
+
+ // Extended Window Styles
+ private const int GwlExstyle = -20;
+ private const int WsExDlgmodalframe = 0x00000001;
+ private const int WsExWindowedge = 0x00000100;
+ private const int WsExClientedge = 0x00000200;
+ private const int WsExStaticedge = 0x00020000;
+ private const int WsExToolwindow = 0x00000080;
+
+ private const uint SwpNosize = 0x0001;
+ private const uint SwpNomove = 0x0002;
+ private const uint SwpFramechanged = 0x0020;
+ private const nint HwndTopmost = -1;
+ private const nint HwndNotopmost = -2;
+
+ // ShowWindow commands
+ private const int SwHide = 0;
+ private const int SwShow = 5;
+
+ // P/Invoke declarations (64-bit only - PowerToys only builds for x64/ARM64)
+ [LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
+ private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
+
+ [LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
+ private static partial nint SetWindowLong(nint hWnd, int nIndex, nint dwNewLong);
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool SetWindowPos(
+ nint hWnd,
+ nint hWndInsertAfter,
+ int x,
+ int y,
+ int cx,
+ int cy,
+ uint uFlags);
+
+ [LibraryImport("user32.dll", EntryPoint = "ShowWindow")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool ShowWindowNative(nint hWnd, int nCmdShow);
+
+ [LibraryImport("user32.dll", EntryPoint = "IsWindowVisible")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool IsWindowVisibleNative(nint hWnd);
+
+ ///
+ /// Check if window is visible
+ ///
+ public static bool IsWindowVisible(nint hWnd)
+ {
+ return IsWindowVisibleNative(hWnd);
+ }
+
+ ///
+ /// Disable window moving and resizing functionality
+ ///
+ public static void DisableWindowMovingAndResizing(nint hWnd)
+ {
+ // Get current window style
+ nint style = GetWindowLongPtr(hWnd, GwlStyle);
+
+ // Remove resizable borders, title bar, and system menu
+ style &= ~WsThickframe;
+ style &= ~WsMaximizebox;
+ style &= ~WsMinimizebox;
+ style &= ~WsCaption; // Remove entire title bar
+ style &= ~WsSysmenu; // Remove system menu
+
+ // Set new window style
+ _ = SetWindowLong(hWnd, GwlStyle, style);
+
+ // Get extended style and remove related borders
+ nint exStyle = GetWindowLongPtr(hWnd, GwlExstyle);
+ exStyle &= ~WsExDlgmodalframe;
+ exStyle &= ~WsExWindowedge;
+ exStyle &= ~WsExClientedge;
+ exStyle &= ~WsExStaticedge;
+ _ = SetWindowLong(hWnd, GwlExstyle, exStyle);
+
+ // Refresh window frame
+ SetWindowPos(
+ hWnd,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ SwpNomove | SwpNosize | SwpFramechanged);
+ }
+
+ ///
+ /// Set whether window is topmost
+ ///
+ public static void SetWindowTopmost(nint hWnd, bool topmost)
+ {
+ SetWindowPos(
+ hWnd,
+ topmost ? HwndTopmost : HwndNotopmost,
+ 0,
+ 0,
+ 0,
+ 0,
+ SwpNomove | SwpNosize);
+ }
+
+ ///
+ /// Show or hide window
+ ///
+ public static void ShowWindow(nint hWnd, bool show)
+ {
+ ShowWindowNative(hWnd, show ? SwShow : SwHide);
+ }
+
+ ///
+ /// Hide window from taskbar
+ ///
+ public static void HideFromTaskbar(nint hWnd)
+ {
+ // Get current extended style
+ nint exStyle = GetWindowLongPtr(hWnd, GwlExstyle);
+
+ // Add WS_EX_TOOLWINDOW style to hide window from taskbar
+ exStyle |= WsExToolwindow;
+
+ // Set new extended style
+ _ = SetWindowLong(hWnd, GwlExstyle, exStyle);
+
+ // Refresh window frame
+ SetWindowPos(
+ hWnd,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ SwpNomove | SwpNosize | SwpFramechanged);
+ }
+
+ ///
+ /// Get the DPI scale factor for a window (relative to standard 96 DPI)
+ ///
+ /// WinUIEx window
+ /// DPI scale factor (1.0 = 100%, 1.25 = 125%, 1.5 = 150%, 2.0 = 200%)
+ public static double GetDpiScale(WindowEx window)
+ {
+ return (float)window.GetDpiForWindow() / 96.0;
+ }
+
+ ///
+ /// Convert device-independent units (DIU) to physical pixels
+ ///
+ /// Device-independent unit value
+ /// DPI scale factor
+ /// Physical pixel value
+ public static int ScaleToPhysicalPixels(int diu, double dpiScale)
+ {
+ return (int)Math.Ceiling(diu * dpiScale);
+ }
+
+ ///
+ /// Position a window at the bottom-right corner of the monitor where the mouse cursor is located.
+ /// Correctly handles all edge cases:
+ /// - Multi-monitor setups
+ /// - Taskbar at any position (top/bottom/left/right)
+ /// - Different DPI settings
+ ///
+ /// WinUIEx window to position
+ /// Window width in device-independent units (DIU)
+ /// Window height in device-independent units (DIU)
+ /// Right margin in device-independent units (DIU)
+ public static void PositionWindowBottomRight(
+ WindowEx window,
+ int width,
+ int height,
+ int rightMargin = 0)
+ {
+ // RectWork already includes correct offsets for taskbar position
+ var monitors = MonitorInfo.GetDisplayMonitors();
+ if (monitors == null || monitors.Count == 0)
+ {
+ ManagedCommon.Logger.LogWarning("PositionWindowBottomRight: No monitors found, skipping positioning");
+ return;
+ }
+
+ // Find the monitor where the mouse cursor is located
+ var targetMonitor = GetMonitorAtCursor(monitors);
+ var workArea = targetMonitor.RectWork;
+ double dpiScale = GetDpiScale(window);
+
+ // Calculate bottom-right position
+ // RectWork.Right/Bottom already account for taskbar position
+ double x = workArea.Right - (dpiScale * (width + rightMargin));
+ double y = workArea.Bottom - (dpiScale * height);
+
+ window.MoveAndResize(x, y, width, height);
+ }
+
+ ///
+ /// Get the monitor where the mouse cursor is currently located.
+ /// Falls back to primary monitor if cursor position cannot be determined.
+ ///
+ /// List of available monitors
+ /// MonitorInfo of the monitor containing the cursor
+ private static MonitorInfo GetMonitorAtCursor(IList monitors)
+ {
+ // Try to get cursor position using Win32 API
+ if (GetCursorPos(out var cursorPos))
+ {
+ // Find the monitor that contains the cursor point
+ foreach (var monitor in monitors)
+ {
+ if (cursorPos.X >= monitor.RectMonitor.Left &&
+ cursorPos.X < monitor.RectMonitor.Right &&
+ cursorPos.Y >= monitor.RectMonitor.Top &&
+ cursorPos.Y < monitor.RectMonitor.Bottom)
+ {
+ return monitor;
+ }
+ }
+ }
+
+ // Fallback to first monitor (typically primary)
+ return monitors[0];
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/NativeMethods.json b/src/modules/powerdisplay/PowerDisplay/NativeMethods.json
new file mode 100644
index 0000000000..450ecacafd
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/NativeMethods.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://aka.ms/CsWin32.schema.json",
+ "public": true,
+ "allowMarshaling": false
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/NativeMethods.txt b/src/modules/powerdisplay/PowerDisplay/NativeMethods.txt
new file mode 100644
index 0000000000..754b73be27
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/NativeMethods.txt
@@ -0,0 +1,17 @@
+// Structs and types only - functions use LibraryImport for AOT compatibility
+NOTIFYICONDATAW
+NOTIFY_ICON_MESSAGE
+NOTIFY_ICON_DATA_FLAGS
+MENU_ITEM_FLAGS
+TRACK_POPUP_MENU_FLAGS
+
+// Window message constants (used by TrayIconService)
+WM_USER
+WM_COMMAND
+WM_RBUTTONUP
+WM_LBUTTONUP
+WM_LBUTTONDBLCLK
+WM_WINDOWPOSCHANGING
+
+// COM wait flags for single instance redirection (constants only)
+CWMO_FLAGS
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj b/src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
new file mode 100644
index 0000000000..78107c261b
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+ WinExe
+ PowerDisplay
+ app.manifest
+ Assets\PowerDisplay\PowerDisplay.ico
+ x64;ARM64
+ true
+ true
+ true
+ None
+ false
+ false
+ true
+ ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps
+ PowerToys.PowerDisplay
+
+ PowerToys.PowerDisplay.pri
+ true
+ enable
+
+ DISABLE_XAML_GENERATED_MAIN
+
+
+
+ false
+ false
+ true
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+ true
+
+
\ No newline at end of file
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml
new file mode 100644
index 0000000000..9822ec0c29
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs
new file mode 100644
index 0000000000..6e6d91df76
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs
@@ -0,0 +1,379 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml;
+using Microsoft.Windows.AppLifecycle;
+using PowerDisplay.Common;
+using PowerDisplay.Helpers;
+using PowerDisplay.Serialization;
+using PowerToys.Interop;
+
+namespace PowerDisplay
+{
+ ///
+ /// PowerDisplay application main class
+ ///
+ public partial class App : Application
+ {
+ private readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
+ private Window? _mainWindow;
+ private int _powerToysRunnerPid;
+ private string? _pipeName;
+ private TrayIconService? _trayIconService;
+
+ public App(int runnerPid, string? pipeName)
+ {
+ Logger.LogInfo($"App constructor: Starting with runnerPid={runnerPid}, pipeName={pipeName ?? "null"}");
+ _powerToysRunnerPid = runnerPid;
+ _pipeName = pipeName;
+
+ Logger.LogTrace("App constructor: Calling InitializeComponent");
+ this.InitializeComponent();
+
+ // Ensure types used in XAML are preserved for AOT compilation
+ TypePreservation.PreserveTypes();
+
+ // Note: Logger is already initialized in Program.cs before App constructor
+ Logger.LogTrace("App constructor: InitializeComponent completed");
+
+ // Initialize PowerToys telemetry
+ try
+ {
+ PowerToysTelemetry.Log.WriteEvent(new Telemetry.Events.PowerDisplayStartEvent());
+ Logger.LogTrace("App constructor: Telemetry event sent");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"App constructor: Telemetry failed: {ex.Message}");
+ }
+
+ // Initialize language settings
+ string appLanguage = LanguageHelper.LoadLanguage();
+ if (!string.IsNullOrEmpty(appLanguage))
+ {
+ Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
+ Logger.LogTrace($"App constructor: Language set to {appLanguage}");
+ }
+
+ // Handle unhandled exceptions
+ this.UnhandledException += OnUnhandledException;
+ Logger.LogInfo("App constructor: Completed");
+ }
+
+ ///
+ /// Handle unhandled exceptions
+ ///
+ private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
+ {
+ Logger.LogError("Unhandled exception", e.Exception);
+ }
+
+ ///
+ /// Called when the application is launched
+ ///
+ /// Launch arguments
+ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
+ {
+ Logger.LogInfo("OnLaunched: Application launching");
+ try
+ {
+ // Single instance is already ensured by AppInstance.FindOrRegisterForKey() in Program.cs
+ // PID is already parsed in Program.cs and passed to constructor
+
+ // Set up Windows Events monitoring (Awake pattern)
+ // Note: PowerDisplay.exe should NOT listen to RefreshMonitorsEvent
+ // That event is sent BY PowerDisplay TO Settings UI for one-way notification
+ Logger.LogInfo("OnLaunched: Registering Windows Events for IPC...");
+ RegisterWindowEvent(Constants.TogglePowerDisplayEvent(), mw => mw.ToggleWindow(), "Toggle");
+ Logger.LogTrace($"OnLaunched: Registered Toggle event: {Constants.TogglePowerDisplayEvent()}");
+ RegisterEvent(Constants.TerminatePowerDisplayEvent(), () => Environment.Exit(0), "Terminate");
+ Logger.LogTrace($"OnLaunched: Registered Terminate event: {Constants.TerminatePowerDisplayEvent()}");
+ RegisterWindowEvent(
+ Constants.SettingsUpdatedPowerDisplayEvent(),
+ mw =>
+ {
+ mw.ViewModel.ApplySettingsFromUI();
+
+ // Refresh tray icon based on updated settings
+ _trayIconService?.SetupTrayIcon();
+ },
+ "SettingsUpdated");
+ RegisterWindowEvent(
+ Constants.HotkeyUpdatedPowerDisplayEvent(),
+ mw => mw.ReloadHotkeySettings(),
+ "HotkeyUpdated");
+ RegisterViewModelEvent(Constants.PowerDisplaySendSettingsTelemetryEvent(), vm => vm.SendSettingsTelemetry(), "SendSettingsTelemetry");
+
+ // LightSwitch integration - apply profiles when theme changes
+ RegisterViewModelEvent(PathConstants.LightSwitchLightThemeEventName, vm => vm.ApplyLightSwitchProfile(isLightMode: true), "LightSwitch-Light");
+ RegisterViewModelEvent(PathConstants.LightSwitchDarkThemeEventName, vm => vm.ApplyLightSwitchProfile(isLightMode: false), "LightSwitch-Dark");
+ Logger.LogInfo("OnLaunched: All Windows Events registered");
+
+ // Connect to Named Pipe for IPC with module DLL (if pipe name provided)
+ if (!string.IsNullOrEmpty(_pipeName))
+ {
+ Logger.LogInfo($"OnLaunched: Starting Named Pipe processing for pipe: {_pipeName}");
+ ProcessNamedPipe(_pipeName);
+ }
+ else
+ {
+ Logger.LogInfo("OnLaunched: No pipe name provided, skipping Named Pipe setup");
+ }
+
+ // Monitor Runner process (backup exit mechanism)
+ if (_powerToysRunnerPid > 0)
+ {
+ Logger.LogInfo($"OnLaunched: PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
+
+ RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
+ {
+ Logger.LogInfo("OnLaunched: PowerToys Runner exited. Exiting PowerDisplay");
+ Environment.Exit(0);
+ });
+ }
+ else
+ {
+ Logger.LogInfo("OnLaunched: PowerDisplay started in standalone mode (no runner PID)");
+ }
+
+ // Create main window
+ Logger.LogInfo("OnLaunched: Creating MainWindow");
+ _mainWindow = new MainWindow();
+ Logger.LogInfo("OnLaunched: MainWindow created");
+
+ // Initialize tray icon service
+ Logger.LogTrace("OnLaunched: Initializing TrayIconService");
+ _trayIconService = new TrayIconService(
+ _settingsUtils,
+ ToggleMainWindow,
+ () => Environment.Exit(0),
+ OpenSettings);
+ _trayIconService.SetupTrayIcon();
+ Logger.LogTrace("OnLaunched: TrayIconService initialized");
+
+ // Window visibility depends on launch mode
+ bool isStandaloneMode = _powerToysRunnerPid <= 0;
+ Logger.LogInfo($"OnLaunched: isStandaloneMode={isStandaloneMode}");
+
+ if (isStandaloneMode)
+ {
+ // Standalone mode - activate and show window immediately
+ Logger.LogInfo("OnLaunched: Activating window (standalone mode)");
+ _mainWindow.Activate();
+ Logger.LogInfo("OnLaunched: Window activated (standalone mode)");
+ }
+ else
+ {
+ // PowerToys mode - window remains hidden until show event received
+ // Background initialization runs automatically via MainWindow constructor
+ Logger.LogInfo("OnLaunched: Window created but hidden, waiting for show/toggle event (PowerToys mode)");
+ }
+
+ Logger.LogInfo("OnLaunched: Application launch completed");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"OnLaunched: PowerDisplay startup failed: {ex.Message}\n{ex.StackTrace}");
+ }
+ }
+
+ ///
+ /// Register a simple event handler (no window access needed)
+ ///
+ private void RegisterEvent(string eventName, Action action, string logName)
+ {
+ Logger.LogTrace($"RegisterEvent: Setting up event listener for '{logName}' on event '{eventName}'");
+ NativeEventWaiter.WaitForEventLoop(
+ eventName,
+ () =>
+ {
+ Logger.LogInfo($"[EVENT] {logName} event received from event '{eventName}'");
+ try
+ {
+ action();
+ Logger.LogTrace($"[EVENT] {logName} action completed");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[EVENT] {logName} action failed: {ex.Message}");
+ }
+ },
+ CancellationToken.None);
+ }
+
+ ///
+ /// Register an event handler that operates on MainWindow directly
+ /// NativeEventWaiter already marshals to UI thread
+ ///
+ private void RegisterWindowEvent(string eventName, Action action, string logName)
+ {
+ Logger.LogTrace($"RegisterWindowEvent: Setting up window event listener for '{logName}' on event '{eventName}'");
+ NativeEventWaiter.WaitForEventLoop(
+ eventName,
+ () =>
+ {
+ Logger.LogInfo($"[EVENT] {logName} window event received from event '{eventName}'");
+ if (_mainWindow is MainWindow mainWindow)
+ {
+ Logger.LogTrace($"[EVENT] {logName}: MainWindow is valid, invoking action");
+ try
+ {
+ action(mainWindow);
+ Logger.LogTrace($"[EVENT] {logName}: Window action completed");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[EVENT] {logName}: Window action failed: {ex.Message}");
+ }
+ }
+ else
+ {
+ Logger.LogError($"[EVENT] {logName}: _mainWindow is null or not MainWindow type");
+ }
+ },
+ CancellationToken.None);
+ }
+
+ ///
+ /// Register an event handler that operates on ViewModel via DispatcherQueue
+ /// Used for Settings UI IPC events that need ViewModel access
+ ///
+ private void RegisterViewModelEvent(string eventName, Action action, string logName)
+ {
+ NativeEventWaiter.WaitForEventLoop(
+ eventName,
+ () =>
+ {
+ Logger.LogInfo($"[EVENT] {logName} event received");
+ _mainWindow?.DispatcherQueue.TryEnqueue(() =>
+ {
+ if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
+ {
+ action(mainWindow.ViewModel);
+ }
+ });
+ },
+ CancellationToken.None);
+ }
+
+ ///
+ /// Gets the main window instance
+ ///
+ public Window? MainWindow => _mainWindow;
+
+ ///
+ /// Toggle the main window visibility
+ ///
+ private void ToggleMainWindow()
+ {
+ Logger.LogInfo("ToggleMainWindow: Called");
+ if (_mainWindow is MainWindow mainWindow)
+ {
+ Logger.LogTrace($"ToggleMainWindow: MainWindow is valid, current visibility={mainWindow.IsWindowVisible()}");
+ mainWindow.ToggleWindow();
+ }
+ else
+ {
+ Logger.LogError("ToggleMainWindow: _mainWindow is null or not MainWindow type");
+ }
+ }
+
+ ///
+ /// Open PowerDisplay settings in PowerToys Settings UI
+ ///
+ private void OpenSettings()
+ {
+ // mainExecutableIsOnTheParentFolder = true because PowerDisplay is a WinUI 3 app
+ // deployed in a subfolder (PowerDisplay\) while PowerToys.exe is in the parent folder
+ SettingsDeepLink.OpenSettings(true);
+ }
+
+ ///
+ /// Refresh tray icon based on current settings
+ ///
+ public void RefreshTrayIcon()
+ {
+ _trayIconService?.SetupTrayIcon();
+ }
+
+ ///
+ /// Check if running standalone (not launched from PowerToys Runner)
+ ///
+ public bool IsRunningDetachedFromPowerToys()
+ {
+ return _powerToysRunnerPid == -1;
+ }
+
+ ///
+ /// Shutdown application (Awake pattern - simple and clean)
+ ///
+ public void Shutdown()
+ {
+ Logger.LogInfo("PowerDisplay shutting down");
+ _trayIconService?.Destroy();
+ Environment.Exit(0);
+ }
+
+ ///
+ /// Connect to Named Pipe and process messages from module DLL
+ ///
+ private void ProcessNamedPipe(string pipeName)
+ {
+ void OnMessage(string message) => _mainWindow?.DispatcherQueue.TryEnqueue(async () => await OnNamedPipeMessage(message));
+
+ Task.Run(async () => await NamedPipeProcessor.ProcessNamedPipeAsync(
+ pipeName,
+ connectTimeout: TimeSpan.FromSeconds(10),
+ OnMessage,
+ CancellationToken.None));
+ }
+
+ ///
+ /// Handle messages received from the module DLL via Named Pipe
+ ///
+ private async Task OnNamedPipeMessage(string message)
+ {
+ var messageParts = message.Split(' ', 2);
+ var messageType = messageParts[0];
+
+ Logger.LogInfo($"[NamedPipe] Processing message type: {messageType}");
+
+ if (messageType == Constants.PowerDisplayToggleMessage())
+ {
+ // Toggle window visibility
+ if (_mainWindow is MainWindow mainWindow)
+ {
+ mainWindow.ToggleWindow();
+ }
+ }
+ else if (messageType == Constants.PowerDisplayApplyProfileMessage())
+ {
+ // Apply profile by name
+ if (messageParts.Length > 1 && _mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
+ {
+ var profileName = messageParts[1].Trim();
+ Logger.LogInfo($"[NamedPipe] Applying profile: {profileName}");
+ await mainWindow.ViewModel.ApplyProfileByNameAsync(profileName);
+ }
+ }
+ else if (messageType == Constants.PowerDisplayTerminateAppMessage())
+ {
+ // Terminate the application
+ Logger.LogInfo("[NamedPipe] Received terminate message");
+ Shutdown();
+ }
+ else
+ {
+ Logger.LogWarning($"[NamedPipe] Unknown message type: {messageType}");
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/IdentifyWindow.xaml b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/IdentifyWindow.xaml
new file mode 100644
index 0000000000..7d41fdc12d
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/IdentifyWindow.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/IdentifyWindow.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/IdentifyWindow.xaml.cs
new file mode 100644
index 0000000000..1668aef820
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/IdentifyWindow.xaml.cs
@@ -0,0 +1,72 @@
+// 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.Threading.Tasks;
+using Microsoft.UI.Windowing;
+using Windows.Graphics;
+using WinUIEx;
+
+namespace PowerDisplay.PowerDisplayXAML
+{
+ ///
+ /// Interaction logic for IdentifyWindow.xaml
+ ///
+ public sealed partial class IdentifyWindow : WindowEx
+ {
+ // Window size in device-independent units (DIU)
+ private const int WindowWidthDiu = 300;
+ private const int WindowHeightDiu = 280;
+
+ private double _dpiScale = 1.0;
+
+ public IdentifyWindow(string displayText)
+ {
+ InitializeComponent();
+ NumberText.Text = displayText;
+
+ // Configure window style
+ ConfigureWindow();
+
+ // Auto close after 3 seconds
+ Task.Delay(3000).ContinueWith(_ =>
+ {
+ DispatcherQueue.TryEnqueue(() =>
+ {
+ Close();
+ });
+ });
+ }
+
+ private void ConfigureWindow()
+ {
+ _dpiScale = this.GetDpiForWindow() / 96.0;
+
+ // Set window size scaled for DPI
+ // AppWindow.Resize expects physical pixels
+ int physicalWidth = (int)(WindowWidthDiu * _dpiScale);
+ int physicalHeight = (int)(WindowHeightDiu * _dpiScale);
+ this.AppWindow.Resize(new SizeInt32 { Width = physicalWidth, Height = physicalHeight });
+ this.IsAlwaysOnTop = true;
+ }
+
+ ///
+ /// Position the window at the center of the specified display area
+ ///
+ public void PositionOnDisplay(DisplayArea displayArea)
+ {
+ var workArea = displayArea.WorkArea;
+
+ // Window size in physical pixels (already scaled for DPI)
+ int physicalWidth = (int)(WindowWidthDiu * _dpiScale);
+ int physicalHeight = (int)(WindowHeightDiu * _dpiScale);
+
+ // Calculate center position (WorkArea coordinates are in physical pixels)
+ int x = workArea.X + ((workArea.Width - physicalWidth) / 2);
+ int y = workArea.Y + ((workArea.Height - physicalHeight) / 2);
+
+ // Use WindowEx's AppWindow property
+ this.AppWindow.Move(new PointInt32(x, y));
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml
new file mode 100644
index 0000000000..7c43bc105f
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml
@@ -0,0 +1,552 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml.cs
new file mode 100644
index 0000000000..62fa666d6e
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml.cs
@@ -0,0 +1,715 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading.Tasks;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Input;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Configuration;
+using PowerDisplay.Helpers;
+using PowerDisplay.ViewModels;
+using Windows.Graphics;
+using WinUIEx;
+using Monitor = PowerDisplay.Common.Models.Monitor;
+
+namespace PowerDisplay
+{
+ ///
+ /// PowerDisplay main window
+ ///
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
+ public sealed partial class MainWindow : WindowEx, IDisposable
+ {
+ private readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
+ private MainViewModel? _viewModel;
+ private HotkeyService? _hotkeyService;
+
+ // Expose ViewModel as property for x:Bind
+ public MainViewModel ViewModel => _viewModel ?? throw new InvalidOperationException("ViewModel not initialized");
+
+ public MainWindow()
+ {
+ Logger.LogInfo("MainWindow constructor: Starting");
+ try
+ {
+ // 1. Create ViewModel BEFORE InitializeComponent to avoid x:Bind failures
+ // x:Bind evaluates during InitializeComponent, so ViewModel must exist first
+ Logger.LogTrace("MainWindow constructor: Creating MainViewModel");
+ _viewModel = new MainViewModel();
+ Logger.LogTrace("MainWindow constructor: MainViewModel created");
+
+ Logger.LogTrace("MainWindow constructor: Calling InitializeComponent");
+ this.InitializeComponent();
+ Logger.LogTrace("MainWindow constructor: InitializeComponent completed");
+
+ // 2. Configure window immediately (synchronous, no data dependency)
+ Logger.LogTrace("MainWindow constructor: Configuring window");
+ ConfigureWindow();
+
+ // 3. Set up data context and update bindings
+ RootGrid.DataContext = _viewModel;
+ Bindings.Update();
+ Logger.LogTrace("MainWindow constructor: Data context set and bindings updated");
+
+ // 4. Register event handlers
+ RegisterEventHandlers();
+ Logger.LogTrace("MainWindow constructor: Event handlers registered");
+
+ // 5. Initialize HotkeyService for in-process hotkey handling (CmdPal pattern)
+ // This avoids IPC timing issues with Runner's centralized hotkey mechanism
+ Logger.LogTrace("MainWindow constructor: Initializing HotkeyService");
+ _hotkeyService = new HotkeyService(_settingsUtils, ToggleWindow);
+ _hotkeyService.Initialize(this);
+ Logger.LogTrace("MainWindow constructor: HotkeyService initialized");
+
+ // Note: ViewModel handles all async initialization internally.
+ // We listen to InitializationCompleted event to know when data is ready.
+ // No duplicate initialization here - single responsibility in ViewModel.
+ Logger.LogInfo("MainWindow constructor: Completed");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"MainWindow constructor: Initialization failed: {ex.Message}\n{ex.StackTrace}");
+ ShowError($"Unable to start main window: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Register all event handlers for window and ViewModel
+ ///
+ private void RegisterEventHandlers()
+ {
+ // Window events
+ this.Closed += OnWindowClosed;
+ this.Activated += OnWindowActivated;
+
+ // ViewModel events - _viewModel is guaranteed non-null here as this is called after initialization
+ if (_viewModel != null)
+ {
+ _viewModel.InitializationCompleted += OnViewModelInitializationCompleted;
+ _viewModel.UIRefreshRequested += OnUIRefreshRequested;
+ _viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
+ _viewModel.PropertyChanged += OnViewModelPropertyChanged;
+ }
+ }
+
+ ///
+ /// Called when ViewModel completes initial monitor discovery.
+ /// This is the single source of truth for initialization state.
+ ///
+ private void OnViewModelInitializationCompleted(object? sender, EventArgs e)
+ {
+ _hasInitialized = true;
+ Logger.LogInfo("MainWindow: Initialization completed via ViewModel event, _hasInitialized=true");
+ AdjustWindowSizeToContent();
+ }
+
+ private bool _hasInitialized;
+
+ private void ShowError(string message)
+ {
+ Logger.LogError($"Error: {message}");
+ }
+
+ private void OnWindowActivated(object sender, WindowActivatedEventArgs args)
+ {
+ Logger.LogTrace($"OnWindowActivated: WindowActivationState={args.WindowActivationState}");
+
+ // Auto-hide window when it loses focus (deactivated)
+ if (args.WindowActivationState == WindowActivationState.Deactivated)
+ {
+ Logger.LogInfo("OnWindowActivated: Window deactivated, hiding window");
+ HideWindow();
+ }
+ }
+
+ private void OnWindowClosed(object sender, WindowEventArgs args)
+ {
+ // If only user operation (although we hide close button), just hide window
+ args.Handled = true; // Prevent window closing
+ HideWindow();
+ }
+
+ public void ShowWindow()
+ {
+ Logger.LogInfo($"ShowWindow: Called, _hasInitialized={_hasInitialized}");
+ try
+ {
+ // If not initialized, log warning but continue showing
+ if (!_hasInitialized)
+ {
+ Logger.LogWarning("ShowWindow: Window not fully initialized yet, showing anyway");
+ }
+
+ // Adjust size BEFORE showing to prevent flicker
+ // This measures content and positions window at correct size
+ Logger.LogTrace("ShowWindow: Adjusting window size to content");
+ AdjustWindowSizeToContent();
+
+ // CRITICAL: WinUI3 windows must be Activated at least once to display properly.
+ // In PowerToys mode, window is created but never activated until first show.
+ // Without Activate(), Show() may not actually render the window on screen.
+ Logger.LogTrace("ShowWindow: Calling this.Activate()");
+ this.Activate();
+
+ // Now show the window - it should appear at the correct size
+ Logger.LogTrace("ShowWindow: Calling this.Show()");
+ this.Show();
+
+ // Ensure window stays on top of other windows
+ this.IsAlwaysOnTop = true;
+ Logger.LogTrace("ShowWindow: IsAlwaysOnTop set to true");
+
+ // Ensure window gets keyboard focus using WinUIEx's BringToFront
+ // This is necessary for Tab navigation to work without clicking first
+ this.BringToFront();
+ Logger.LogTrace("ShowWindow: BringToFront called");
+
+ // Clear focus from any interactive element (e.g., Slider) to prevent
+ // showing the value tooltip when the window opens
+ RootGrid.Focus(FocusState.Programmatic);
+
+ // Verify window is visible
+ bool isVisible = IsWindowVisible();
+ Logger.LogInfo($"ShowWindow: Window visibility after show: {isVisible}");
+ if (!isVisible)
+ {
+ Logger.LogError("ShowWindow: Window not visible after show attempt, forcing visibility");
+ this.Activate();
+ this.Show();
+ this.BringToFront();
+ Logger.LogInfo($"ShowWindow: After forced show, visibility: {IsWindowVisible()}");
+ }
+ else
+ {
+ Logger.LogInfo("ShowWindow: Window shown successfully");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"ShowWindow: Failed to show window: {ex.Message}\n{ex.StackTrace}");
+ throw;
+ }
+ }
+
+ public void HideWindow()
+ {
+ Logger.LogInfo("HideWindow: Hiding window");
+
+ // Hide window
+ this.Hide();
+
+ Logger.LogTrace($"HideWindow: Window hidden, visibility now: {IsWindowVisible()}");
+ }
+
+ ///
+ /// Check if window is currently visible
+ ///
+ /// True if window is visible, false otherwise
+ public bool IsWindowVisible()
+ {
+ bool visible = this.Visible;
+ Logger.LogTrace($"IsWindowVisible: Returning {visible}");
+ return visible;
+ }
+
+ ///
+ /// Toggle window visibility (show if hidden, hide if visible)
+ ///
+ public void ToggleWindow()
+ {
+ bool currentlyVisible = IsWindowVisible();
+ Logger.LogInfo($"ToggleWindow: Called, current visibility={currentlyVisible}");
+ try
+ {
+ if (currentlyVisible)
+ {
+ Logger.LogInfo("ToggleWindow: Window is visible, hiding");
+ HideWindow();
+ }
+ else
+ {
+ Logger.LogInfo("ToggleWindow: Window is hidden, showing");
+ ShowWindow();
+ }
+
+ Logger.LogInfo($"ToggleWindow: Completed, new visibility={IsWindowVisible()}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"ToggleWindow: Failed to toggle window: {ex.Message}\n{ex.StackTrace}");
+ throw;
+ }
+ }
+
+ private void OnUIRefreshRequested(object? sender, EventArgs e)
+ {
+ // Adjust window size when UI configuration changes (feature visibility toggles)
+ DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
+ }
+
+ private void OnMonitorsCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+ {
+ // Adjust window size when monitors collection changes (event-driven!)
+ // The UI binding will update first, then we adjust size
+ DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
+ {
+ AdjustWindowSizeToContent();
+ });
+ }
+
+ private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ // Adjust window size when relevant properties change (event-driven!)
+ if (e.PropertyName == nameof(_viewModel.IsScanning) ||
+ e.PropertyName == nameof(_viewModel.HasMonitors) ||
+ e.PropertyName == nameof(_viewModel.ShowNoMonitorsMessage))
+ {
+ // Use Low priority to ensure UI bindings update first
+ DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
+ {
+ AdjustWindowSizeToContent();
+ });
+ }
+ }
+
+ private void OnRefreshClick(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // Refresh monitor list
+ if (_viewModel?.RefreshCommand?.CanExecute(null) == true)
+ {
+ _viewModel.RefreshCommand.Execute(null);
+
+ // Window size will be adjusted automatically by OnMonitorsCollectionChanged event!
+ // No delay needed - event-driven design
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"OnRefreshClick failed: {ex}");
+ }
+ }
+
+ private void OnSettingsClick(object sender, RoutedEventArgs e)
+ {
+ // Open PowerDisplay settings in PowerToys Settings UI
+ // mainExecutableIsOnTheParentFolder = true because PowerDisplay is a WinUI 3 app
+ // deployed in a subfolder (PowerDisplay\) while PowerToys.exe is in the parent folder
+ SettingsDeepLink.OpenSettings(true);
+ }
+
+ ///
+ /// Configure window properties (synchronous, no data dependency)
+ ///
+ private void ConfigureWindow()
+ {
+ try
+ {
+ // Window properties (IsResizable, IsMaximizable, IsMinimizable,
+ // IsTitleBarVisible, IsShownInSwitchers) are set in XAML
+
+ // Set minimal initial window size - will be adjusted before showing
+ // Using minimal height to prevent "large window shrinking" flicker
+ this.AppWindow.Resize(new SizeInt32 { Width = AppConstants.UI.WindowWidth, Height = 100 });
+
+ // Position window at bottom right corner
+ PositionWindowAtBottomRight();
+
+ // Set window title
+ this.AppWindow.Title = "PowerDisplay";
+
+ // Custom title bar - completely remove all buttons
+ var titleBar = this.AppWindow.TitleBar;
+ if (titleBar != null)
+ {
+ // Extend content into title bar area
+ titleBar.ExtendsContentIntoTitleBar = true;
+
+ // Completely remove title bar height
+ titleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
+
+ // Set all button colors to transparent
+ titleBar.ButtonBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
+ titleBar.ButtonInactiveBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
+ titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
+ titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
+ titleBar.ButtonHoverForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
+ titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
+ titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
+ titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
+
+ // Disable title bar interaction area
+ titleBar.SetDragRectangles(Array.Empty());
+ }
+
+ // Use Win32 API to further disable window moving (removes WS_CAPTION, WS_SYSMENU, etc.)
+ var hWnd = this.GetWindowHandle();
+ WindowHelper.DisableWindowMovingAndResizing(hWnd);
+ }
+ catch (Exception ex)
+ {
+ // Ignore window setup errors
+ Logger.LogWarning($"Window configuration error: {ex.Message}");
+ }
+ }
+
+ private void AdjustWindowSizeToContent()
+ {
+ try
+ {
+ if (RootGrid == null)
+ {
+ return;
+ }
+
+ // Force layout update and measure content height
+ RootGrid.UpdateLayout();
+ MainContainer?.Measure(new Windows.Foundation.Size(AppConstants.UI.WindowWidth, double.PositiveInfinity));
+ var contentHeight = (int)Math.Ceiling(MainContainer?.DesiredSize.Height ?? 0);
+
+ // Apply min/max height limits and reposition (WindowEx handles DPI automatically)
+ // Min height ensures window is visible even if content hasn't loaded yet
+ var finalHeight = Math.Max(AppConstants.UI.MinWindowHeight, Math.Min(contentHeight, AppConstants.UI.MaxWindowHeight));
+ Logger.LogTrace($"AdjustWindowSizeToContent: contentHeight={contentHeight}, finalHeight={finalHeight}");
+ WindowHelper.PositionWindowBottomRight(this, AppConstants.UI.WindowWidth, finalHeight, AppConstants.UI.WindowRightMargin);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error adjusting window size: {ex.Message}");
+ }
+ }
+
+ private void PositionWindowAtBottomRight()
+ {
+ try
+ {
+ var windowSize = this.AppWindow.Size;
+ WindowHelper.PositionWindowBottomRight(
+ this, // MainWindow inherits from WindowEx
+ AppConstants.UI.WindowWidth,
+ windowSize.Height,
+ AppConstants.UI.WindowRightMargin);
+ }
+ catch (Exception)
+ {
+ // Window positioning failures are non-critical, silently ignore
+ }
+ }
+
+ ///
+ /// Slider PointerCaptureLost event handler - updates ViewModel when drag completes
+ /// This is the WinUI3 recommended way to detect drag completion
+ ///
+ private void Slider_PointerCaptureLost(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
+ {
+ var slider = sender as Slider;
+ if (slider == null)
+ {
+ return;
+ }
+
+ var propertyName = slider.Tag as string;
+ var monitorVm = slider.DataContext as MonitorViewModel;
+
+ if (monitorVm == null || propertyName == null)
+ {
+ return;
+ }
+
+ // Get final value after drag completes
+ int finalValue = (int)slider.Value;
+
+ // Now update the ViewModel, which will trigger hardware operation
+ switch (propertyName)
+ {
+ case "Brightness":
+ monitorVm.Brightness = finalValue;
+ break;
+ case "Contrast":
+ monitorVm.ContrastPercent = finalValue;
+ break;
+ case "Volume":
+ monitorVm.Volume = finalValue;
+ break;
+ }
+ }
+
+ ///
+ /// Slider KeyUp event handler - updates ViewModel when arrow keys are released
+ /// This handles keyboard navigation for accessibility
+ ///
+ private void Slider_KeyUp(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
+ {
+ // Only handle arrow keys (Left, Right, Up, Down)
+ if (e.Key != Windows.System.VirtualKey.Left &&
+ e.Key != Windows.System.VirtualKey.Right &&
+ e.Key != Windows.System.VirtualKey.Up &&
+ e.Key != Windows.System.VirtualKey.Down)
+ {
+ return;
+ }
+
+ var slider = sender as Slider;
+ if (slider == null)
+ {
+ return;
+ }
+
+ var propertyName = slider.Tag as string;
+ var monitorVm = slider.DataContext as MonitorViewModel;
+
+ if (monitorVm == null || propertyName == null)
+ {
+ return;
+ }
+
+ // Get the current value after key press
+ int finalValue = (int)slider.Value;
+
+ // Update the ViewModel, which will trigger hardware operation
+ switch (propertyName)
+ {
+ case "Brightness":
+ monitorVm.Brightness = finalValue;
+ break;
+ case "Contrast":
+ monitorVm.ContrastPercent = finalValue;
+ break;
+ case "Volume":
+ monitorVm.Volume = finalValue;
+ break;
+ }
+ }
+
+ ///
+ /// Input source ListView selection changed handler - switches the monitor input source
+ ///
+ private async void InputSourceListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ListView listView)
+ {
+ return;
+ }
+
+ // Get the selected input source item
+ var selectedItem = listView.SelectedItem as InputSourceItem;
+ if (selectedItem == null)
+ {
+ return;
+ }
+
+ Logger.LogInfo($"[UI] InputSourceListView_SelectionChanged: Selected {selectedItem.Name} (0x{selectedItem.Value:X2}) for monitor {selectedItem.MonitorId}");
+
+ // Find the monitor by ID
+ MonitorViewModel? monitorVm = null;
+ if (!string.IsNullOrEmpty(selectedItem.MonitorId) && _viewModel != null)
+ {
+ monitorVm = _viewModel.Monitors.FirstOrDefault(m => m.Id == selectedItem.MonitorId);
+ }
+
+ if (monitorVm == null)
+ {
+ Logger.LogWarning("[UI] InputSourceListView_SelectionChanged: Could not find MonitorViewModel");
+ return;
+ }
+
+ // Set the input source
+ await monitorVm.SetInputSourceAsync(selectedItem.Value);
+ }
+
+ ///
+ /// Power state ListView selection changed handler - switches the monitor power state.
+ /// Note: Selecting any state other than "On" will turn off the display.
+ ///
+ private async void PowerStateListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ListView listView)
+ {
+ return;
+ }
+
+ // Get the selected power state item
+ var selectedItem = listView.SelectedItem as PowerStateItem;
+ if (selectedItem == null)
+ {
+ return;
+ }
+
+ // Skip if "On" is selected - the monitor is already on
+ if (selectedItem.Value == PowerStateItem.PowerStateOn)
+ {
+ return;
+ }
+
+ Logger.LogInfo($"[UI] PowerStateListView_SelectionChanged: Selected {selectedItem.Name} (0x{selectedItem.Value:X2}) for monitor {selectedItem.MonitorId}");
+
+ // Find the monitor by ID
+ MonitorViewModel? monitorVm = null;
+ if (!string.IsNullOrEmpty(selectedItem.MonitorId) && _viewModel != null)
+ {
+ monitorVm = _viewModel.Monitors.FirstOrDefault(m => m.Id == selectedItem.MonitorId);
+ }
+
+ if (monitorVm == null)
+ {
+ Logger.LogWarning("[UI] PowerStateListView_SelectionChanged: Could not find MonitorViewModel");
+ return;
+ }
+
+ // Set the power state - this will turn off the display
+ await monitorVm.SetPowerStateAsync(selectedItem.Value);
+ }
+
+ ///
+ /// Rotation button click handler - changes monitor orientation
+ ///
+ private async void RotationButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Microsoft.UI.Xaml.Controls.Primitives.ToggleButton toggleButton)
+ {
+ return;
+ }
+
+ // Get the orientation from the Tag
+ if (toggleButton.Tag is not string tagStr || !int.TryParse(tagStr, out int orientation))
+ {
+ Logger.LogWarning("[UI] RotationButton_Click: Invalid Tag");
+ return;
+ }
+
+ var monitorVm = toggleButton.DataContext as MonitorViewModel;
+ if (monitorVm == null)
+ {
+ Logger.LogWarning("[UI] RotationButton_Click: Could not find MonitorViewModel");
+ return;
+ }
+
+ // If clicking the current orientation, restore the checked state and do nothing
+ if (monitorVm.CurrentRotation == orientation)
+ {
+ toggleButton.IsChecked = true;
+ return;
+ }
+
+ Logger.LogInfo($"[UI] RotationButton_Click: Setting rotation for {monitorVm.Name} to {orientation}");
+
+ // Set the rotation
+ await monitorVm.SetRotationAsync(orientation);
+ }
+
+ ///
+ /// Profile selection changed handler - applies the selected profile
+ ///
+ private void ProfileListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ListView listView)
+ {
+ return;
+ }
+
+ var selectedProfile = listView.SelectedItem as PowerDisplayProfile;
+ if (selectedProfile == null || !selectedProfile.IsValid())
+ {
+ return;
+ }
+
+ Logger.LogInfo($"[UI] ProfileListView_SelectionChanged: Applying profile '{selectedProfile.Name}'");
+
+ // Apply profile via ViewModel command
+ if (_viewModel?.ApplyProfileCommand?.CanExecute(selectedProfile) == true)
+ {
+ _viewModel.ApplyProfileCommand.Execute(selectedProfile);
+ }
+
+ // Close the flyout after selection
+ ProfilesFlyout?.Hide();
+
+ // Clear selection to allow reselecting the same profile
+ listView.SelectedItem = null;
+ }
+
+ ///
+ /// Color temperature selection changed handler - applies the selected color temperature preset
+ ///
+ private async void ColorTemperatureListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ListView listView)
+ {
+ return;
+ }
+
+ var selectedItem = listView.SelectedItem as ColorTemperatureItem;
+ if (selectedItem == null)
+ {
+ return;
+ }
+
+ Logger.LogInfo($"[UI] ColorTemperatureListView_SelectionChanged: Selected {selectedItem.DisplayName} (0x{selectedItem.VcpValue:X2}) for monitor {selectedItem.MonitorId}");
+
+ // Find the monitor by ID
+ MonitorViewModel? monitorVm = null;
+ if (!string.IsNullOrEmpty(selectedItem.MonitorId) && _viewModel != null)
+ {
+ monitorVm = _viewModel.Monitors.FirstOrDefault(m => m.Id == selectedItem.MonitorId);
+ }
+
+ if (monitorVm == null)
+ {
+ Logger.LogWarning("[UI] ColorTemperatureListView_SelectionChanged: Could not find MonitorViewModel");
+ return;
+ }
+
+ // Apply the color temperature
+ await monitorVm.SetColorTemperatureAsync(selectedItem.VcpValue);
+
+ // Clear selection to allow reselecting the same preset
+ listView.SelectedItem = null;
+ }
+
+ ///
+ /// Flyout opened event handler - sets focus to the first focusable element inside the flyout.
+ /// This enables keyboard navigation when the flyout opens.
+ ///
+ private void Flyout_Opened(object sender, object e)
+ {
+ if (sender is Flyout flyout && flyout.Content is FrameworkElement content)
+ {
+ // Use DispatcherQueue to ensure the flyout content is fully rendered before setting focus
+ DispatcherQueue.TryEnqueue(() =>
+ {
+ var firstFocusable = FocusManager.FindFirstFocusableElement(content);
+ if (firstFocusable is Control control)
+ {
+ control.Focus(FocusState.Programmatic);
+ }
+ });
+ }
+ }
+
+ public void Dispose()
+ {
+ _hotkeyService?.Dispose();
+ _viewModel?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Reload hotkey settings. Call this when settings change.
+ ///
+ public void ReloadHotkeySettings()
+ {
+ _hotkeyService?.ReloadSettings();
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MonitorIcon.xaml b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MonitorIcon.xaml
new file mode 100644
index 0000000000..10244cee6c
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MonitorIcon.xaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MonitorIcon.xaml.cs b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MonitorIcon.xaml.cs
new file mode 100644
index 0000000000..3948332d0b
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MonitorIcon.xaml.cs
@@ -0,0 +1,45 @@
+// 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.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace PowerDisplay;
+
+public sealed partial class MonitorIcon : UserControl
+{
+ public MonitorIcon()
+ {
+ InitializeComponent();
+ }
+
+ public bool IsBuiltIn
+ {
+ get => (bool)GetValue(IsBuiltInProperty);
+ set => SetValue(IsBuiltInProperty, value);
+ }
+
+ public static readonly DependencyProperty IsBuiltInProperty = DependencyProperty.Register(nameof(IsBuiltIn), typeof(bool), typeof(MonitorIcon), new PropertyMetadata(false, OnPropertyChanged));
+
+ public int MonitorNumber
+ {
+ get => (int)GetValue(MonitorNumberProperty);
+ set => SetValue(MonitorNumberProperty, value);
+ }
+
+ public static readonly DependencyProperty MonitorNumberProperty = DependencyProperty.Register(nameof(MonitorNumber), typeof(int), typeof(MonitorIcon), new PropertyMetadata(0, OnPropertyChanged));
+
+ private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var monIcon = (MonitorIcon)d;
+ if (monIcon.IsBuiltIn)
+ {
+ VisualStateManager.GoToState(monIcon, "BuiltIn", true);
+ }
+ else
+ {
+ VisualStateManager.GoToState(monIcon, "Monitor", true);
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Program.cs b/src/modules/powerdisplay/PowerDisplay/Program.cs
new file mode 100644
index 0000000000..f893741e77
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Program.cs
@@ -0,0 +1,138 @@
+// 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;
+using System.Threading;
+using System.Threading.Tasks;
+using ManagedCommon;
+using Microsoft.UI.Dispatching;
+using Microsoft.Windows.AppLifecycle;
+
+namespace PowerDisplay
+{
+ public static partial class Program
+ {
+ private static App? _app;
+
+ // LibraryImport for AOT compatibility - COM wait constants
+ private const uint CowaitDefault = 0;
+ private const uint InfiniteTimeout = 0xFFFFFFFF;
+
+ [LibraryImport("ole32.dll")]
+ private static partial int CoWaitForMultipleObjects(
+ uint dwFlags,
+ uint dwTimeout,
+ int cHandles,
+ nint[] pHandles,
+ out uint lpdwIndex);
+
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ // Initialize COM wrappers first (needed for AppInstance)
+ WinRT.ComWrappersSupport.InitializeComWrappers();
+
+ // Single instance check BEFORE logger initialization to avoid creating extra log files
+ // Command Palette pattern: check for existing instance first
+ var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
+ var keyInstance = AppInstance.FindOrRegisterForKey("PowerToys_PowerDisplay_Instance");
+
+ if (!keyInstance.IsCurrent)
+ {
+ // Another instance exists - redirect and exit WITHOUT initializing logger
+ // This prevents creation of extra log files for short-lived redirect processes
+ RedirectActivationTo(activationArgs, keyInstance);
+ return 0;
+ }
+
+ // This is the primary instance - now initialize logger
+ Logger.InitializeLogger("\\PowerDisplay\\Logs");
+ Logger.LogInfo("PowerDisplay starting");
+
+ // Register activation handler for future redirects
+ keyInstance.Activated += OnActivated;
+
+ // Parse command line arguments:
+ // args[0] = runner_pid (Awake pattern)
+ // args[1] = pipe_name (Named Pipe for IPC with module DLL)
+ int runnerPid = -1;
+ string? pipeName = null;
+
+ if (args.Length >= 1)
+ {
+ if (int.TryParse(args[0], out int parsedPid))
+ {
+ runnerPid = parsedPid;
+ }
+ }
+
+ if (args.Length >= 2)
+ {
+ pipeName = args[1];
+ }
+
+ Microsoft.UI.Xaml.Application.Start((p) =>
+ {
+ var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
+ SynchronizationContext.SetSynchronizationContext(context);
+ _app = new App(runnerPid, pipeName);
+ });
+ return 0;
+ }
+
+ ///
+ /// Redirect activation to existing instance (Command Palette pattern)
+ /// Called BEFORE logger is initialized, so no logging here
+ ///
+ private static void RedirectActivationTo(AppActivationArguments args, AppInstance keyInstance)
+ {
+ // Do the redirection on another thread, and use a non-blocking
+ // wait method to wait for the redirection to complete.
+ using var redirectSemaphore = new Semaphore(0, 1);
+ var redirectTimeout = TimeSpan.FromSeconds(10);
+
+ _ = Task.Run(() =>
+ {
+ using var cts = new CancellationTokenSource(redirectTimeout);
+ try
+ {
+ keyInstance.RedirectActivationToAsync(args)
+ .AsTask(cts.Token)
+ .GetAwaiter()
+ .GetResult();
+ }
+ catch
+ {
+ // Silently ignore errors - logger not initialized yet
+ }
+ finally
+ {
+ redirectSemaphore.Release();
+ }
+ });
+
+ // Use CoWaitForMultipleObjects to pump COM messages while waiting
+ nint[] handles = [redirectSemaphore.SafeWaitHandle.DangerousGetHandle()];
+ _ = CoWaitForMultipleObjects(
+ CowaitDefault,
+ InfiniteTimeout,
+ 1,
+ handles,
+ out _);
+ }
+
+ ///
+ /// Called when an existing instance is activated by another process.
+ /// This happens when EnsureProcessRunning() launches a new process while one is already running.
+ /// We intentionally don't show the window here - window visibility should only be controlled via:
+ /// - Toggle event (hotkey, tray icon click, Settings UI Launch button)
+ /// - Standalone mode startup (handled in OnLaunched)
+ ///
+ private static void OnActivated(object? sender, AppActivationArguments args)
+ {
+ Logger.LogInfo("OnActivated: Redirect activation received - window visibility unchanged");
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Serialization/IPCMessageAction.cs b/src/modules/powerdisplay/PowerDisplay/Serialization/IPCMessageAction.cs
new file mode 100644
index 0000000000..b929db1b52
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Serialization/IPCMessageAction.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+
+namespace PowerDisplay.Serialization
+{
+ ///
+ /// IPC message wrapper for parsing action-based messages.
+ /// Used in App.xaml.cs for dynamic IPC command handling.
+ ///
+ internal sealed class IpcMessageAction
+ {
+ [JsonPropertyName("action")]
+ public string? Action { get; set; }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs b/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs
new file mode 100644
index 0000000000..239d777693
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.PowerToys.Settings.UI.Library;
+using PowerDisplay.Common.Models;
+
+namespace PowerDisplay.Serialization
+{
+ ///
+ /// JSON source generation context for AOT compatibility.
+ /// Eliminates reflection-based JSON serialization.
+ /// Note: MonitorStateFile and MonitorStateEntry are now in PowerDisplay.Lib
+ /// and should be serialized using ProfileSerializationContext from the Lib.
+ ///
+ [JsonSerializable(typeof(IpcMessageAction))]
+ [JsonSerializable(typeof(PowerDisplaySettings))]
+ [JsonSerializable(typeof(PowerDisplayProfiles))]
+ [JsonSerializable(typeof(PowerDisplayProfile))]
+ [JsonSerializable(typeof(ProfileMonitorSetting))]
+
+ // MonitorInfo and related types (Settings.UI.Library)
+ [JsonSerializable(typeof(MonitorInfo))]
+ [JsonSerializable(typeof(VcpCodeDisplayInfo))]
+ [JsonSerializable(typeof(VcpValueInfo))]
+
+ // Generic collection types
+ [JsonSerializable(typeof(List))]
+ [JsonSerializable(typeof(List))]
+ [JsonSerializable(typeof(List))]
+ [JsonSerializable(typeof(List))]
+ [JsonSerializable(typeof(List))]
+ [JsonSerializable(typeof(List))]
+
+ [JsonSourceGenerationOptions(
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.Never)]
+ internal sealed partial class AppJsonContext : JsonSerializerContext
+ {
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Services/LightSwitchService.cs b/src/modules/powerdisplay/PowerDisplay/Services/LightSwitchService.cs
new file mode 100644
index 0000000000..7182fd32ed
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Services/LightSwitchService.cs
@@ -0,0 +1,77 @@
+// 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 ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Settings.UI.Library;
+
+namespace PowerDisplay.Services
+{
+ ///
+ /// Service for handling LightSwitch theme change events.
+ /// Reads LightSwitch settings using the standard PowerToys settings pattern.
+ ///
+ public static class LightSwitchService
+ {
+ private const string LogPrefix = "[LightSwitch]";
+
+ ///
+ /// Get the profile name to apply for the given theme.
+ ///
+ /// Whether the theme changed to light mode.
+ /// The profile name to apply, or null if no profile is configured.
+ public static string? GetProfileForTheme(bool isLightMode)
+ {
+ try
+ {
+ Logger.LogInfo($"{LogPrefix} Processing theme change to {(isLightMode ? "light" : "dark")} mode");
+
+ var settings = SettingsUtils.Default.GetSettingsOrDefault(LightSwitchSettings.ModuleName);
+
+ if (settings?.Properties == null)
+ {
+ Logger.LogWarning($"{LogPrefix} LightSwitch settings not found");
+ return null;
+ }
+
+ string? profileName;
+ if (isLightMode)
+ {
+ if (!settings.Properties.EnableLightModeProfile.Value)
+ {
+ Logger.LogInfo($"{LogPrefix} Light mode profile is disabled");
+ return null;
+ }
+
+ profileName = settings.Properties.LightModeProfile.Value;
+ }
+ else
+ {
+ if (!settings.Properties.EnableDarkModeProfile.Value)
+ {
+ Logger.LogInfo($"{LogPrefix} Dark mode profile is disabled");
+ return null;
+ }
+
+ profileName = settings.Properties.DarkModeProfile.Value;
+ }
+
+ if (string.IsNullOrEmpty(profileName) || profileName == "(None)")
+ {
+ Logger.LogInfo($"{LogPrefix} No profile configured for {(isLightMode ? "light" : "dark")} mode");
+ return null;
+ }
+
+ Logger.LogInfo($"{LogPrefix} Profile to apply: {profileName}");
+ return profileName;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"{LogPrefix} Failed to get profile for theme: {ex.Message}");
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Strings/en-us/Resources.resw b/src/modules/powerdisplay/PowerDisplay/Strings/en-us/Resources.resw
new file mode 100644
index 0000000000..166140cb1f
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Strings/en-us/Resources.resw
@@ -0,0 +1,96 @@
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Scanning monitors..
+
+
+ No monitors detected
+
+
+ Rescan connected monitors
+
+
+ Settings
+
+
+ Monitor
+
+
+ Brightness
+
+
+ Contrast
+
+
+ Volume
+
+
+ Rotation
+
+
+ Normal (0°)
+
+
+ Rotate left (270°)
+
+
+ Rotate right (90°)
+
+
+ Inverted (180°)
+
+
+ Volume
+
+
+ Contrast
+
+
+ Brightness
+
+
+ PowerDisplay
+
+
+ Settings
+
+
+ Exit
+
+
+ Quick apply profiles
+
+
+ Identify monitors
+
+
+ Input source
+
+
+ Power state
+
+
+ More options
+
+
+ Profiles
+
+
+ Color temperature
+
+
+ Color temperature
+
+
diff --git a/src/modules/powerdisplay/PowerDisplay/Telemetry/Events/PowerDisplaySettingsTelemetryEvent.cs b/src/modules/powerdisplay/PowerDisplay/Telemetry/Events/PowerDisplaySettingsTelemetryEvent.cs
new file mode 100644
index 0000000000..d29976742f
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Telemetry/Events/PowerDisplaySettingsTelemetryEvent.cs
@@ -0,0 +1,42 @@
+// 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.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace PowerDisplay.Telemetry.Events
+{
+ ///
+ /// Telemetry event for PowerDisplay settings
+ /// Sent when Runner requests settings telemetry via send_settings_telemetry()
+ ///
+ [EventData]
+ public class PowerDisplaySettingsTelemetryEvent : EventBase, IEvent
+ {
+ public new string EventName => "PowerDisplay_Settings";
+
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+
+ ///
+ /// Whether the hotkey is enabled
+ ///
+ public bool HotkeyEnabled { get; set; }
+
+ ///
+ /// Whether the tray icon is enabled
+ ///
+ public bool TrayIconEnabled { get; set; }
+
+ ///
+ /// Number of monitors currently detected
+ ///
+ public int MonitorCount { get; set; }
+
+ ///
+ /// Number of profiles saved
+ ///
+ public int ProfileCount { get; set; }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/Telemetry/Events/PowerDisplayStartEvent.cs b/src/modules/powerdisplay/PowerDisplay/Telemetry/Events/PowerDisplayStartEvent.cs
new file mode 100644
index 0000000000..397fc722e2
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/Telemetry/Events/PowerDisplayStartEvent.cs
@@ -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.Diagnostics.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace PowerDisplay.Telemetry.Events
+{
+ [EventData]
+ public class PowerDisplayStartEvent : EventBase, IEvent
+ {
+ public new string EventName => "PowerDisplay_Start";
+
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/ColorTemperatureItem.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/ColorTemperatureItem.cs
new file mode 100644
index 0000000000..281dd2d3a4
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/ColorTemperatureItem.cs
@@ -0,0 +1,31 @@
+// 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.ViewModels;
+
+///
+/// Represents a color temperature preset option for display in UI
+///
+public class ColorTemperatureItem
+{
+ ///
+ /// VCP value for this color temperature preset (e.g., 0x05 for 6500K)
+ ///
+ public int VcpValue { get; set; }
+
+ ///
+ /// Human-readable name (e.g., "6500K", "sRGB", "User 1")
+ ///
+ public string DisplayName { get; set; } = string.Empty;
+
+ ///
+ /// Whether this preset is currently selected
+ ///
+ public bool IsSelected { get; set; }
+
+ ///
+ /// Monitor ID for direct lookup (Flyout popup is not in visual tree)
+ ///
+ public string MonitorId { get; set; } = string.Empty;
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/InputSourceItem.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/InputSourceItem.cs
new file mode 100644
index 0000000000..25a53efbe0
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/InputSourceItem.cs
@@ -0,0 +1,33 @@
+// 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.UI.Xaml;
+
+namespace PowerDisplay.ViewModels;
+
+///
+/// Represents an input source option for display in UI
+///
+public class InputSourceItem
+{
+ ///
+ /// VCP value for this input source (e.g., 0x11 for HDMI-1)
+ ///
+ public int Value { get; set; }
+
+ ///
+ /// Human-readable name (e.g., "HDMI-1", "DisplayPort-1")
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Visibility of selection indicator (Visible when selected)
+ ///
+ public Visibility SelectionVisibility { get; set; } = Visibility.Collapsed;
+
+ ///
+ /// Monitor ID for direct lookup (Flyout popup is not in visual tree)
+ ///
+ public string MonitorId { get; set; } = string.Empty;
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs
new file mode 100644
index 0000000000..02b3a35009
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs
@@ -0,0 +1,165 @@
+// 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 Microsoft.PowerToys.Settings.UI.Library;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Helpers;
+using Monitor = PowerDisplay.Common.Models.Monitor;
+
+namespace PowerDisplay.ViewModels;
+
+///
+/// MainViewModel - Monitor discovery and management methods
+///
+public partial class MainViewModel
+{
+ private async Task InitializeAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ IsScanning = true;
+
+ // Discover monitors
+ var monitors = await _monitorManager.DiscoverMonitorsAsync(cancellationToken);
+
+ // Update UI on the dispatcher thread, then complete initialization asynchronously
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ try
+ {
+ UpdateMonitorList(monitors, isInitialLoad: true);
+
+ // Complete initialization asynchronously (restore settings if enabled)
+ // IsScanning remains true until restore completes
+ _ = CompleteInitializationAsync();
+ }
+ catch (Exception lambdaEx)
+ {
+ Logger.LogError($"[InitializeAsync] UI update failed: {lambdaEx.Message}");
+ IsScanning = false;
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[InitializeAsync] Monitor discovery failed: {ex.Message}");
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ IsScanning = false;
+ });
+ }
+ }
+
+ ///
+ /// Complete initialization by restoring settings (if enabled) and firing completion event.
+ /// IsScanning remains true until this method completes, so user sees discovery UI during restore.
+ ///
+ private async Task CompleteInitializationAsync()
+ {
+ try
+ {
+ // Check if we should restore settings on startup
+ var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
+ if (settings.Properties.RestoreSettingsOnStartup)
+ {
+ await RestoreMonitorSettingsAsync();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[CompleteInitializationAsync] Failed to restore settings: {ex.Message}");
+ }
+ finally
+ {
+ // Always complete initialization, even if restore failed
+ IsScanning = false;
+ IsInitialized = true;
+
+ // Start watching for display changes after initialization
+ StartDisplayWatching();
+
+ // Notify listeners that initialization is complete
+ InitializationCompleted?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Refresh monitors list asynchronously.
+ ///
+ /// If true, skip the IsScanning check (used by OnDisplayChanged which sets IsScanning before calling).
+ public async Task RefreshMonitorsAsync(bool skipScanningCheck = false)
+ {
+ if (!skipScanningCheck && IsScanning)
+ {
+ return;
+ }
+
+ try
+ {
+ IsScanning = true;
+
+ var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
+
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ UpdateMonitorList(monitors, isInitialLoad: false);
+ IsScanning = false;
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[RefreshMonitorsAsync] Refresh failed: {ex.Message}");
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ IsScanning = false;
+ });
+ }
+ }
+
+ private void UpdateMonitorList(IReadOnlyList monitors, bool isInitialLoad)
+ {
+ Monitors.Clear();
+
+ // Load settings to check for hidden monitors
+ var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
+ var hiddenMonitorIds = GetHiddenMonitorIds(settings);
+
+ foreach (var monitor in monitors)
+ {
+ // Skip monitors that are marked as hidden in settings
+ if (hiddenMonitorIds.Contains(monitor.Id))
+ {
+ continue;
+ }
+
+ var vm = new MonitorViewModel(monitor, _monitorManager, this);
+ ApplyFeatureVisibility(vm, settings);
+ Monitors.Add(vm);
+ }
+
+ OnPropertyChanged(nameof(HasMonitors));
+ OnPropertyChanged(nameof(ShowNoMonitorsMessage));
+
+ // Save monitor information to settings
+ SaveMonitorsToSettings();
+
+ // Note: RestoreMonitorSettingsAsync is now called from InitializeAsync/CompleteInitializationAsync
+ // to ensure scanning state is maintained until restore completes
+ }
+
+ ///
+ /// Get set of hidden monitor IDs from settings
+ ///
+ private HashSet GetHiddenMonitorIds(PowerDisplaySettings settings)
+ => new HashSet(
+ settings.Properties.Monitors
+ .Where(m => m.IsHidden)
+ .Select(m => m.Id));
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs
new file mode 100644
index 0000000000..dc9e7ccbbd
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs
@@ -0,0 +1,550 @@
+// 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.Tasks;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Telemetry;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Common.Services;
+using PowerDisplay.Common.Utils;
+using PowerDisplay.Serialization;
+using PowerDisplay.Services;
+using PowerDisplay.Telemetry.Events;
+using PowerToys.Interop;
+
+namespace PowerDisplay.ViewModels;
+
+///
+/// MainViewModel - Settings UI synchronization and Profile management methods
+///
+public partial class MainViewModel
+{
+ ///
+ /// Check if a value is within the valid range (inclusive).
+ ///
+ private static bool IsValueInRange(int value, int min, int max) => value >= min && value <= max;
+
+ ///
+ /// Apply settings changes from Settings UI (IPC event handler entry point)
+ /// Only applies UI configuration changes. Hardware parameter changes (e.g., color temperature)
+ /// should be triggered via custom actions to avoid unwanted side effects when non-hardware
+ /// settings (like RestoreSettingsOnStartup) are changed.
+ ///
+ public void ApplySettingsFromUI()
+ {
+ try
+ {
+ // Rebuild monitor list with updated hidden monitor settings
+ // UpdateMonitorList already handles filtering hidden monitors
+ UpdateMonitorList(_monitorManager.Monitors, isInitialLoad: false);
+
+ // Apply UI configuration changes only (feature visibility toggles, etc.)
+ // Hardware parameters (brightness, color temperature) are applied via custom actions
+ var settings = _settingsUtils.GetSettingsOrDefault("PowerDisplay");
+ ApplyUIConfiguration(settings);
+
+ // Reload profiles in case they were added/updated/deleted in Settings UI
+ LoadProfiles();
+
+ // Reload UI display settings (profile switcher, identify button, color temp switcher)
+ LoadUIDisplaySettings();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[Settings] Failed to apply settings from UI: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Apply UI-only configuration changes (feature visibility toggles)
+ /// Synchronous, lightweight operation
+ ///
+ private void ApplyUIConfiguration(PowerDisplaySettings settings)
+ {
+ try
+ {
+ foreach (var monitorVm in Monitors)
+ {
+ ApplyFeatureVisibility(monitorVm, settings);
+ }
+
+ // Trigger UI refresh
+ UIRefreshRequested?.Invoke(this, EventArgs.Empty);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[Settings] Failed to apply UI configuration: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Apply profile by name (called via Named Pipe from Settings UI)
+ /// This is the new direct method that receives the profile name via IPC.
+ ///
+ /// The name of the profile to apply.
+ public async Task ApplyProfileByNameAsync(string profileName)
+ {
+ try
+ {
+ Logger.LogInfo($"[Profile] Applying profile by name: {profileName}");
+
+ // Load profiles and find the requested one
+ var profilesData = ProfileService.LoadProfiles();
+ var profile = profilesData.GetProfile(profileName);
+
+ if (profile == null || !profile.IsValid())
+ {
+ Logger.LogWarning($"[Profile] Profile '{profileName}' not found or invalid");
+ return;
+ }
+
+ // Apply the profile settings to monitors
+ await ApplyProfileAsync(profile.MonitorSettings);
+ Logger.LogInfo($"[Profile] Successfully applied profile: {profileName}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[Profile] Failed to apply profile '{profileName}': {ex.Message}");
+ }
+ }
+
+ ///
+ /// Handle theme change from LightSwitch by applying the appropriate profile.
+ /// Called from App.xaml.cs when LightSwitch theme events are received.
+ ///
+ /// Whether the theme changed to light mode.
+ public void ApplyLightSwitchProfile(bool isLightMode)
+ {
+ var profileName = LightSwitchService.GetProfileForTheme(isLightMode);
+
+ if (string.IsNullOrEmpty(profileName))
+ {
+ return;
+ }
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ Logger.LogInfo($"[LightSwitch Integration] Applying profile: {profileName}");
+
+ // Load and apply the profile
+ var profilesData = ProfileService.LoadProfiles();
+ var profile = profilesData.GetProfile(profileName);
+
+ if (profile == null || !profile.IsValid())
+ {
+ Logger.LogWarning($"[LightSwitch Integration] Profile '{profileName}' not found or invalid");
+ return;
+ }
+
+ // Apply the profile - need to dispatch to UI thread since MonitorViewModels are UI-bound
+ var tcs = new TaskCompletionSource();
+ var enqueued = _dispatcherQueue.TryEnqueue(() =>
+ {
+ // Start the async operation and handle completion
+ _ = ApplyProfileAndCompleteAsync(profile.MonitorSettings, tcs);
+ });
+
+ if (!enqueued)
+ {
+ Logger.LogError($"[LightSwitch Integration] Failed to enqueue profile application to UI thread");
+ return;
+ }
+
+ await tcs.Task;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[LightSwitch Integration] Failed to apply profile: {ex.GetType().Name}: {ex.Message}");
+ if (ex.InnerException != null)
+ {
+ Logger.LogError($"[LightSwitch Integration] Inner exception: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}");
+ }
+ }
+ });
+ }
+
+ ///
+ /// Helper method to apply profile and signal completion.
+ ///
+ private async Task ApplyProfileAndCompleteAsync(List monitorSettings, TaskCompletionSource tcs)
+ {
+ try
+ {
+ await ApplyProfileAsync(monitorSettings);
+ tcs.SetResult(true);
+ }
+ catch (Exception ex)
+ {
+ tcs.SetException(ex);
+ }
+ }
+
+ ///
+ /// Apply profile settings to monitors
+ ///
+ private async Task ApplyProfileAsync(List monitorSettings)
+ {
+ var updateTasks = new List();
+
+ foreach (var setting in monitorSettings)
+ {
+ // Find monitor by Id (unique identifier)
+ var monitorVm = Monitors.FirstOrDefault(m => m.Id == setting.MonitorId);
+
+ if (monitorVm == null)
+ {
+ continue;
+ }
+
+ // Apply brightness if included in profile
+ if (setting.Brightness.HasValue &&
+ IsValueInRange(setting.Brightness.Value, monitorVm.MinBrightness, monitorVm.MaxBrightness))
+ {
+ updateTasks.Add(monitorVm.SetBrightnessAsync(setting.Brightness.Value));
+ }
+
+ // Apply contrast if supported and value provided
+ if (setting.Contrast.HasValue && monitorVm.ShowContrast &&
+ IsValueInRange(setting.Contrast.Value, monitorVm.MinContrast, monitorVm.MaxContrast))
+ {
+ updateTasks.Add(monitorVm.SetContrastAsync(setting.Contrast.Value));
+ }
+
+ // Apply volume if supported and value provided
+ if (setting.Volume.HasValue && monitorVm.ShowVolume &&
+ IsValueInRange(setting.Volume.Value, monitorVm.MinVolume, monitorVm.MaxVolume))
+ {
+ updateTasks.Add(monitorVm.SetVolumeAsync(setting.Volume.Value));
+ }
+
+ // Apply color temperature if included in profile
+ if (setting.ColorTemperatureVcp.HasValue && setting.ColorTemperatureVcp.Value > 0)
+ {
+ updateTasks.Add(monitorVm.SetColorTemperatureAsync(setting.ColorTemperatureVcp.Value));
+ }
+ }
+
+ // Wait for all updates to complete
+ if (updateTasks.Count > 0)
+ {
+ await Task.WhenAll(updateTasks);
+ }
+ }
+
+ ///
+ /// Restore monitor settings from state file - ONLY called at startup when RestoreSettingsOnStartup is enabled.
+ /// Compares saved values with current hardware values and only writes when different.
+ ///
+ public async Task RestoreMonitorSettingsAsync()
+ {
+ try
+ {
+ IsLoading = true;
+ var updateTasks = new List();
+
+ foreach (var monitorVm in Monitors)
+ {
+ var savedState = _stateManager.GetMonitorParameters(monitorVm.Id);
+ if (!savedState.HasValue)
+ {
+ continue;
+ }
+
+ // Restore brightness if different from current
+ if (IsValueInRange(savedState.Value.Brightness, monitorVm.MinBrightness, monitorVm.MaxBrightness) &&
+ savedState.Value.Brightness != monitorVm.Brightness)
+ {
+ updateTasks.Add(monitorVm.SetBrightnessAsync(savedState.Value.Brightness));
+ }
+
+ // Restore color temperature if different from current
+ if (savedState.Value.ColorTemperatureVcp > 0 &&
+ savedState.Value.ColorTemperatureVcp != monitorVm.ColorTemperature)
+ {
+ updateTasks.Add(monitorVm.SetColorTemperatureAsync(savedState.Value.ColorTemperatureVcp));
+ }
+
+ // Restore contrast if different from current
+ if (monitorVm.ShowContrast &&
+ IsValueInRange(savedState.Value.Contrast, monitorVm.MinContrast, monitorVm.MaxContrast) &&
+ savedState.Value.Contrast != monitorVm.Contrast)
+ {
+ updateTasks.Add(monitorVm.SetContrastAsync(savedState.Value.Contrast));
+ }
+
+ // Restore volume if different from current
+ if (monitorVm.ShowVolume &&
+ IsValueInRange(savedState.Value.Volume, monitorVm.MinVolume, monitorVm.MaxVolume) &&
+ savedState.Value.Volume != monitorVm.Volume)
+ {
+ updateTasks.Add(monitorVm.SetVolumeAsync(savedState.Value.Volume));
+ }
+ }
+
+ if (updateTasks.Count > 0)
+ {
+ await Task.WhenAll(updateTasks);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[RestoreMonitorSettings] Failed: {ex.Message}");
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ ///
+ /// Apply feature visibility settings to a monitor ViewModel
+ ///
+ private void ApplyFeatureVisibility(MonitorViewModel monitorVm, PowerDisplaySettings settings)
+ {
+ var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m =>
+ m.Id == monitorVm.Id);
+
+ if (monitorSettings != null)
+ {
+ monitorVm.ShowContrast = monitorSettings.EnableContrast;
+ monitorVm.ShowVolume = monitorSettings.EnableVolume;
+ monitorVm.ShowInputSource = monitorSettings.EnableInputSource;
+ monitorVm.ShowRotation = monitorSettings.EnableRotation;
+ monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
+ monitorVm.ShowPowerState = monitorSettings.EnablePowerState;
+ }
+ }
+
+ ///
+ /// Thread-safe save method that can be called from background threads.
+ /// Does not access UI collections or update UI properties.
+ ///
+ public void SaveMonitorSettingDirect(string monitorId, string property, int value)
+ {
+ try
+ {
+ // This is thread-safe - _stateManager has internal locking
+ // No UI thread operations, no ObservableCollection access
+ _stateManager.UpdateMonitorParameter(monitorId, property, value);
+ }
+ catch (Exception ex)
+ {
+ // Only log, don't update UI from background thread
+ Logger.LogError($"Failed to queue setting save for monitorId '{monitorId}': {ex.Message}");
+ }
+ }
+
+ ///
+ /// Save monitor information to settings.json for Settings UI to read
+ ///
+ private void SaveMonitorsToSettings()
+ {
+ try
+ {
+ // Load current settings to preserve user preferences (including IsHidden)
+ var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
+
+ // Create lookup of existing monitors by Id to preserve settings
+ // Filter out monitors with empty IDs to avoid dictionary key collision errors
+ var existingMonitorSettings = settings.Properties.Monitors
+ .Where(m => !string.IsNullOrEmpty(m.Id))
+ .GroupBy(m => m.Id)
+ .ToDictionary(g => g.Key, g => g.First());
+
+ // Build monitor list using Settings UI's MonitorInfo model
+ // Only include monitors with valid (non-empty) IDs to auto-fix corrupted settings
+ var monitors = new List();
+
+ foreach (var vm in Monitors)
+ {
+ // Skip monitors with empty IDs - they are invalid and would cause issues
+ if (string.IsNullOrEmpty(vm.Id))
+ {
+ Logger.LogWarning($"[SaveMonitors] Skipping monitor '{vm.Name}' with empty Id");
+ continue;
+ }
+
+ var monitorInfo = CreateMonitorInfo(vm);
+ ApplyPreservedUserSettings(monitorInfo, existingMonitorSettings);
+ monitors.Add(monitorInfo);
+ }
+
+ // Also add hidden monitors from existing settings (monitors that are hidden but still connected)
+ // Only include those with valid IDs
+ foreach (var existingMonitor in settings.Properties.Monitors.Where(m => m.IsHidden && !string.IsNullOrEmpty(m.Id)))
+ {
+ // Only add if not already in the list (to avoid duplicates)
+ if (!monitors.Any(m => m.Id == existingMonitor.Id))
+ {
+ monitors.Add(existingMonitor);
+ }
+ }
+
+ // Update monitors list
+ settings.Properties.Monitors = monitors;
+
+ // Save back to settings.json using source-generated context for AOT
+ _settingsUtils.SaveSettings(
+ System.Text.Json.JsonSerializer.Serialize(settings, AppJsonContext.Default.PowerDisplaySettings),
+ PowerDisplaySettings.ModuleName);
+
+ // Signal Settings UI that monitor list has been updated
+ SignalMonitorsRefreshEvent();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to save monitors to settings.json: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Create MonitorInfo object from MonitorViewModel
+ ///
+ private Microsoft.PowerToys.Settings.UI.Library.MonitorInfo CreateMonitorInfo(MonitorViewModel vm)
+ {
+ // Validate monitor Id - this should never be empty for properly discovered monitors
+ if (string.IsNullOrEmpty(vm.Id))
+ {
+ Logger.LogWarning($"[CreateMonitorInfo] Monitor '{vm.Name}' has empty Id - this may cause issues with Settings UI");
+ }
+
+ var monitorInfo = new Microsoft.PowerToys.Settings.UI.Library.MonitorInfo
+ {
+ Name = vm.Name,
+ Id = vm.Id,
+ CommunicationMethod = vm.CommunicationMethod,
+ CurrentBrightness = vm.Brightness,
+ ColorTemperatureVcp = vm.ColorTemperature,
+ CapabilitiesRaw = vm.CapabilitiesRaw,
+ VcpCodesFormatted = vm.VcpCapabilitiesInfo?.GetSortedVcpCodes()
+ .Select(info => FormatVcpCodeForDisplay(info.Code, info))
+ .ToList() ?? new List(),
+
+ // Infer support flags from VCP capabilities
+ // VCP 0x12 (18) = Contrast, 0x14 (20) = Color Temperature, 0x60 (96) = Input Source, 0x62 (98) = Volume, 0xD6 (214) = Power Mode
+ SupportsContrast = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x12) ?? false,
+ SupportsColorTemperature = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x14) ?? false,
+ SupportsInputSource = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x60) ?? false,
+ SupportsVolume = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x62) ?? false,
+ SupportsPowerState = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0xD6) ?? false,
+
+ // Default Enable* to match Supports* for new monitors (first-time setup)
+ // ApplyPreservedUserSettings will override these with saved user preferences if they exist
+ EnableContrast = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x12) ?? false,
+ EnableVolume = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x62) ?? false,
+ EnableInputSource = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x60) ?? false,
+ EnableColorTemperature = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x14) ?? false,
+ EnablePowerState = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0xD6) ?? false,
+
+ // Monitor number for display name formatting
+ MonitorNumber = vm.MonitorNumber,
+ };
+
+ return monitorInfo;
+ }
+
+ ///
+ /// Apply preserved user settings from existing monitor settings
+ ///
+ private void ApplyPreservedUserSettings(
+ Microsoft.PowerToys.Settings.UI.Library.MonitorInfo monitorInfo,
+ Dictionary existingSettings)
+ {
+ if (existingSettings.TryGetValue(monitorInfo.Id, out var existingMonitor))
+ {
+ monitorInfo.IsHidden = existingMonitor.IsHidden;
+ monitorInfo.EnableContrast = existingMonitor.EnableContrast;
+ monitorInfo.EnableVolume = existingMonitor.EnableVolume;
+ monitorInfo.EnableInputSource = existingMonitor.EnableInputSource;
+ monitorInfo.EnableRotation = existingMonitor.EnableRotation;
+ monitorInfo.EnableColorTemperature = existingMonitor.EnableColorTemperature;
+ monitorInfo.EnablePowerState = existingMonitor.EnablePowerState;
+ }
+ }
+
+ ///
+ /// Signal Settings UI that the monitor list has been refreshed
+ ///
+ private void SignalMonitorsRefreshEvent()
+ {
+ EventHelper.SignalEvent(Constants.RefreshPowerDisplayMonitorsEvent());
+ }
+
+ ///
+ /// Format VCP code information for display in Settings UI
+ ///
+ private Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo FormatVcpCodeForDisplay(byte code, VcpCodeInfo info)
+ {
+ var result = new Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo
+ {
+ Code = info.FormattedCode,
+ Title = info.FormattedTitle,
+ };
+
+ if (info.IsContinuous)
+ {
+ result.Values = "Continuous range";
+ result.HasValues = true;
+ }
+ else if (info.HasDiscreteValues)
+ {
+ var formattedValues = info.SupportedValues
+ .Select(v => Common.Utils.VcpNames.GetFormattedValueName(code, v))
+ .ToList();
+ result.Values = $"Values: {string.Join(", ", formattedValues)}";
+ result.HasValues = true;
+
+ // Populate value list for Settings UI ComboBox
+ // Store raw name (without formatting) so Settings UI can format it consistently
+ result.ValueList = info.SupportedValues
+ .Select(v => new Microsoft.PowerToys.Settings.UI.Library.VcpValueInfo
+ {
+ Value = $"0x{v:X2}",
+ Name = Common.Utils.VcpNames.GetValueName(code, v),
+ })
+ .ToList();
+ }
+ else
+ {
+ result.HasValues = false;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Send settings telemetry event (triggered by Runner via send_settings_telemetry())
+ ///
+ public void SendSettingsTelemetry()
+ {
+ try
+ {
+ // Load current settings to get hotkey and tray icon status
+ var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
+
+ // Load profiles to get count
+ var profilesData = ProfileService.LoadProfiles();
+
+ var telemetryEvent = new PowerDisplaySettingsTelemetryEvent
+ {
+ HotkeyEnabled = settings.Properties.ActivationShortcut?.IsValid() ?? false,
+ TrayIconEnabled = settings.Properties.ShowSystemTrayIcon,
+ MonitorCount = Monitors.Count,
+ ProfileCount = profilesData?.Profiles?.Count ?? 0,
+ };
+
+ PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[Telemetry] Failed to send settings telemetry: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000000..d3a831a07d
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
@@ -0,0 +1,428 @@
+// 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.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.Input;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.UI;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Windowing;
+using PowerDisplay.Common.Drivers;
+using PowerDisplay.Common.Drivers.DDC;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Common.Services;
+using PowerDisplay.Helpers;
+using PowerDisplay.PowerDisplayXAML;
+
+namespace PowerDisplay.ViewModels;
+
+///
+/// Main ViewModel for the PowerDisplay application.
+/// Split into partial classes for better maintainability:
+/// - MainViewModel.cs: Core properties, construction, and disposal
+/// - MainViewModel.Monitors.cs: Monitor discovery and management
+/// - MainViewModel.Settings.cs: Settings UI synchronization and profiles
+///
+[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
+public partial class MainViewModel : INotifyPropertyChanged, IDisposable
+{
+ [LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW", StringMarshalling = StringMarshalling.Utf16)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfoEx lpmi);
+
+ private readonly MonitorManager _monitorManager;
+ private readonly DispatcherQueue _dispatcherQueue;
+ private readonly CancellationTokenSource _cancellationTokenSource;
+ private readonly SettingsUtils _settingsUtils;
+ private readonly MonitorStateManager _stateManager;
+ private readonly DisplayChangeWatcher _displayChangeWatcher;
+
+ private ObservableCollection _monitors;
+ private ObservableCollection _profiles;
+ private bool _isScanning;
+ private bool _isInitialized;
+ private bool _isLoading;
+
+ ///
+ /// Event triggered when UI refresh is requested due to settings changes
+ ///
+ public event EventHandler? UIRefreshRequested;
+
+ ///
+ /// Event triggered when initial monitor discovery is completed.
+ /// Used by MainWindow to know when data is ready for display.
+ ///
+ public event EventHandler? InitializationCompleted;
+
+ public MainViewModel()
+ {
+ _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+ _cancellationTokenSource = new CancellationTokenSource();
+ _monitors = new ObservableCollection();
+ _profiles = new ObservableCollection();
+ _isScanning = true;
+
+ // Initialize settings utils
+ _settingsUtils = SettingsUtils.Default;
+ _stateManager = new MonitorStateManager();
+
+ // Initialize the monitor manager
+ _monitorManager = new MonitorManager();
+
+ // Load profiles for quick apply feature
+ LoadProfiles();
+
+ // Load UI display settings (profile switcher, identify button, color temp switcher)
+ LoadUIDisplaySettings();
+
+ // Initialize display change watcher for auto-refresh on monitor plug/unplug
+ // Use MonitorRefreshDelay from settings to allow hardware to stabilize after plug/unplug
+ var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
+ int delaySeconds = Math.Clamp(settings?.Properties?.MonitorRefreshDelay ?? 5, 1, 30);
+ _displayChangeWatcher = new DisplayChangeWatcher(_dispatcherQueue, TimeSpan.FromSeconds(delaySeconds));
+ _displayChangeWatcher.DisplayChanged += OnDisplayChanged;
+
+ // Start initial discovery
+ _ = InitializeAsync(_cancellationTokenSource.Token);
+ }
+
+ public ObservableCollection Monitors
+ {
+ get => _monitors;
+ set
+ {
+ _monitors = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public ObservableCollection Profiles
+ {
+ get => _profiles;
+ set
+ {
+ _profiles = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasProfiles));
+ }
+ }
+
+ public bool HasProfiles => Profiles.Count > 0;
+
+ // UI display control properties - loaded from settings
+ private bool _showProfileSwitcher = true;
+ private bool _showIdentifyMonitorsButton = true;
+
+ ///
+ /// Gets a value indicating whether to show the profile switcher button.
+ /// Combines settings value with HasProfiles check.
+ ///
+ public bool ShowProfileSwitcherButton => _showProfileSwitcher && HasProfiles;
+
+ ///
+ /// Gets or sets a value indicating whether to show the profile switcher (from settings).
+ ///
+ public bool ShowProfileSwitcher
+ {
+ get => _showProfileSwitcher;
+ set
+ {
+ if (_showProfileSwitcher != value)
+ {
+ _showProfileSwitcher = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(ShowProfileSwitcherButton));
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to show the identify monitors button.
+ ///
+ public bool ShowIdentifyMonitorsButton
+ {
+ get => _showIdentifyMonitorsButton;
+ set
+ {
+ if (_showIdentifyMonitorsButton != value)
+ {
+ _showIdentifyMonitorsButton = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public bool IsScanning
+ {
+ get => _isScanning;
+ set
+ {
+ if (_isScanning != value)
+ {
+ _isScanning = value;
+ OnPropertyChanged();
+
+ // Dependent properties that change with IsScanning
+ OnPropertyChanged(nameof(HasMonitors));
+ OnPropertyChanged(nameof(ShowNoMonitorsMessage));
+ OnPropertyChanged(nameof(IsInteractionEnabled));
+ }
+ }
+ }
+
+ public bool HasMonitors => !IsScanning && Monitors.Count > 0;
+
+ public bool ShowNoMonitorsMessage => !IsScanning && Monitors.Count == 0;
+
+ public bool IsInitialized
+ {
+ get => _isInitialized;
+ private set
+ {
+ _isInitialized = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public bool IsLoading
+ {
+ get => _isLoading;
+ private set
+ {
+ _isLoading = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(IsInteractionEnabled));
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether user interaction is enabled (not loading or scanning).
+ ///
+ public bool IsInteractionEnabled => !IsLoading && !IsScanning;
+
+ [RelayCommand]
+ private async Task RefreshAsync() => await RefreshMonitorsAsync();
+
+ [RelayCommand]
+ private unsafe void IdentifyMonitors()
+ {
+ try
+ {
+ // Get all display areas (virtual desktop regions)
+ var displayAreas = DisplayArea.FindAll();
+
+ // Get all monitor info from QueryDisplayConfig
+ var allDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo().Values.ToList();
+
+ // Build GDI name to MonitorNumber(s) mapping
+ // Note: In mirror mode, multiple monitors may share the same GdiDeviceName
+ var gdiToMonitorNumbers = allDisplayInfo
+ .Where(info => info.MonitorNumber > 0)
+ .GroupBy(info => info.GdiDeviceName, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(
+ g => g.Key,
+ g => g.Select(info => info.MonitorNumber).Distinct().OrderBy(n => n).ToList(),
+ StringComparer.OrdinalIgnoreCase);
+
+ // For each DisplayArea, get its HMONITOR, then get GDI device name to find MonitorNumber(s)
+ int windowsCreated = 0;
+ for (int i = 0; i < displayAreas.Count; i++)
+ {
+ var displayArea = displayAreas[i];
+
+ // Convert DisplayId to HMONITOR
+ var hMonitor = Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
+ if (hMonitor == IntPtr.Zero)
+ {
+ continue;
+ }
+
+ // Get GDI device name from HMONITOR
+ var monitorInfo = new MonitorInfoEx { CbSize = (uint)sizeof(MonitorInfoEx) };
+ if (!GetMonitorInfo(hMonitor, ref monitorInfo))
+ {
+ continue;
+ }
+
+ var gdiDeviceName = monitorInfo.GetDeviceName();
+
+ // Look up MonitorNumber(s) by GDI device name
+ if (!gdiToMonitorNumbers.TryGetValue(gdiDeviceName, out var monitorNumbers) || monitorNumbers.Count == 0)
+ {
+ continue;
+ }
+
+ // Format display text: single number for normal mode, "1|2" for mirror mode
+ var displayText = string.Join("|", monitorNumbers);
+
+ // Create and position identify window
+ var identifyWindow = new IdentifyWindow(displayText);
+ identifyWindow.PositionOnDisplay(displayArea);
+ identifyWindow.Activate();
+ windowsCreated++;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to identify monitors: {ex.Message}");
+ }
+ }
+
+ [RelayCommand]
+ private async Task ApplyProfile(PowerDisplayProfile? profile)
+ {
+ if (profile != null && profile.IsValid())
+ {
+ await ApplyProfileAsync(profile.MonitorSettings);
+ }
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public void Dispose()
+ {
+ // Cancel all async operations first
+ _cancellationTokenSource?.Cancel();
+
+ // Dispose each resource independently to ensure all get cleaned up
+ try
+ {
+ _displayChangeWatcher?.Dispose();
+ }
+ catch
+ {
+ }
+
+ // Dispose monitor view models
+ foreach (var vm in Monitors)
+ {
+ try
+ {
+ vm.Dispose();
+ }
+ catch
+ {
+ }
+ }
+
+ try
+ {
+ _monitorManager?.Dispose();
+ }
+ catch
+ {
+ }
+
+ try
+ {
+ _stateManager?.Dispose();
+ }
+ catch
+ {
+ }
+
+ try
+ {
+ _cancellationTokenSource?.Dispose();
+ }
+ catch
+ {
+ }
+
+ try
+ {
+ Monitors.Clear();
+ }
+ catch
+ {
+ }
+
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Load profiles from disk for quick apply feature
+ ///
+ private void LoadProfiles()
+ {
+ try
+ {
+ var profilesData = ProfileService.LoadProfiles();
+ _profiles.Clear();
+ foreach (var profile in profilesData.Profiles)
+ {
+ _profiles.Add(profile);
+ }
+
+ OnPropertyChanged(nameof(HasProfiles));
+ OnPropertyChanged(nameof(ShowProfileSwitcherButton));
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[Profile] Failed to load profiles: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Load UI display settings from settings file
+ ///
+ private void LoadUIDisplaySettings()
+ {
+ try
+ {
+ var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
+ ShowProfileSwitcher = settings.Properties.ShowProfileSwitcher;
+ ShowIdentifyMonitorsButton = settings.Properties.ShowIdentifyMonitorsButton;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[Settings] Failed to load UI display settings: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Handles display configuration changes detected by the DisplayChangeWatcher.
+ /// The DisplayChangeWatcher already applies the configured delay (MonitorRefreshDelay)
+ /// to allow hardware to stabilize, so we can refresh immediately here.
+ ///
+ private async void OnDisplayChanged(object? sender, EventArgs e)
+ {
+ // Set scanning state to provide visual feedback
+ IsScanning = true;
+
+ // Perform refresh - DisplayChangeWatcher has already waited for hardware to stabilize
+ await RefreshMonitorsAsync(skipScanningCheck: true);
+ }
+
+ ///
+ /// Starts watching for display changes. Call after initialization is complete.
+ ///
+ public void StartDisplayWatching()
+ {
+ _displayChangeWatcher.Start();
+ }
+
+ ///
+ /// Stops watching for display changes.
+ ///
+ public void StopDisplayWatching()
+ {
+ _displayChangeWatcher.Stop();
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs
new file mode 100644
index 0000000000..5629ebdacf
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs
@@ -0,0 +1,869 @@
+// 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.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.Input;
+using ManagedCommon;
+using Microsoft.UI.Xaml;
+
+using PowerDisplay.Common.Models;
+using PowerDisplay.Configuration;
+using PowerDisplay.Helpers;
+using Monitor = PowerDisplay.Common.Models.Monitor;
+
+namespace PowerDisplay.ViewModels;
+
+///
+/// ViewModel for individual monitor
+///
+public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
+{
+ private readonly Monitor _monitor;
+ private readonly MonitorManager _monitorManager;
+ private readonly MainViewModel? _mainViewModel;
+
+ private int _brightness;
+ private int _contrast;
+ private int _volume;
+ private bool _isAvailable;
+
+ // Visibility settings (controlled by Settings UI)
+ private bool _showContrast;
+ private bool _showVolume;
+ private bool _showInputSource;
+ private bool _showRotation;
+ private bool _showPowerState;
+
+ ///
+ /// Updates a property value directly without triggering hardware updates.
+ /// Used during initialization to update UI from saved state.
+ ///
+ internal void UpdatePropertySilently(string propertyName, int value)
+ {
+ switch (propertyName)
+ {
+ case nameof(Brightness):
+ _brightness = value;
+ OnPropertyChanged(nameof(Brightness));
+ break;
+ case nameof(Contrast):
+ _contrast = value;
+ OnPropertyChanged(nameof(Contrast));
+ OnPropertyChanged(nameof(ContrastPercent));
+ break;
+ case nameof(Volume):
+ _volume = value;
+ OnPropertyChanged(nameof(Volume));
+ break;
+ case nameof(ColorTemperature):
+ // Update underlying monitor model
+ _monitor.CurrentColorTemperature = value;
+ OnPropertyChanged(nameof(ColorTemperature));
+ OnPropertyChanged(nameof(ColorTemperaturePresetName));
+ break;
+ }
+ }
+
+ ///
+ /// Apply brightness with hardware update and state persistence.
+ ///
+ /// Brightness value (0-100)
+ public async Task SetBrightnessAsync(int brightness)
+ {
+ brightness = Math.Clamp(brightness, MinBrightness, MaxBrightness);
+
+ // Update UI state immediately
+ if (_brightness != brightness)
+ {
+ _brightness = brightness;
+ OnPropertyChanged(nameof(Brightness));
+ }
+
+ // Apply to hardware
+ await ApplyPropertyToHardwareAsync(nameof(Brightness), brightness, _monitorManager.SetBrightnessAsync);
+ }
+
+ ///
+ /// Apply contrast with hardware update and state persistence.
+ ///
+ public async Task SetContrastAsync(int contrast)
+ {
+ contrast = Math.Clamp(contrast, MinContrast, MaxContrast);
+
+ if (_contrast != contrast)
+ {
+ _contrast = contrast;
+ OnPropertyChanged(nameof(Contrast));
+ OnPropertyChanged(nameof(ContrastPercent));
+ }
+
+ await ApplyPropertyToHardwareAsync(nameof(Contrast), contrast, _monitorManager.SetContrastAsync);
+ }
+
+ ///
+ /// Apply volume with hardware update and state persistence.
+ ///
+ public async Task SetVolumeAsync(int volume)
+ {
+ volume = Math.Clamp(volume, MinVolume, MaxVolume);
+
+ if (_volume != volume)
+ {
+ _volume = volume;
+ OnPropertyChanged(nameof(Volume));
+ }
+
+ await ApplyPropertyToHardwareAsync(nameof(Volume), volume, _monitorManager.SetVolumeAsync);
+ }
+
+ ///
+ /// Unified method to apply color temperature with hardware update and state persistence.
+ /// Always immediate (no debouncing for discrete preset values).
+ ///
+ public async Task SetColorTemperatureAsync(int colorTemperature)
+ {
+ try
+ {
+ var result = await _monitorManager.SetColorTemperatureAsync(Id, colorTemperature);
+
+ if (result.IsSuccess)
+ {
+ _monitor.CurrentColorTemperature = colorTemperature;
+ OnPropertyChanged(nameof(ColorTemperature));
+ OnPropertyChanged(nameof(ColorTemperaturePresetName));
+
+ // Refresh the color presets list to update IsSelected checkmarks in UI
+ RefreshAvailableColorPresets();
+
+ _mainViewModel?.SaveMonitorSettingDirect(_monitor.Id, nameof(ColorTemperature), colorTemperature);
+ }
+ else
+ {
+ Logger.LogWarning($"[{Id}] Failed to set color temperature: {result.ErrorMessage}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[{Id}] Exception setting color temperature: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Generic method to apply a monitor property to hardware and persist state.
+ /// Consolidates common logic for brightness, contrast, and volume operations.
+ ///
+ /// Name of the property being set (for logging and state persistence)
+ /// Value to apply
+ /// Async function to call on MonitorManager
+ private async Task ApplyPropertyToHardwareAsync(
+ string propertyName,
+ int value,
+ Func> setAsyncFunc)
+ {
+ try
+ {
+ var result = await setAsyncFunc(Id, value, default);
+
+ if (result.IsSuccess)
+ {
+ _mainViewModel?.SaveMonitorSettingDirect(_monitor.Id, propertyName, value);
+ }
+ else
+ {
+ Logger.LogWarning($"[{Id}] Failed to set {propertyName.ToLowerInvariant()}: {result.ErrorMessage}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[{Id}] Exception setting {propertyName.ToLowerInvariant()}: {ex.Message}");
+ }
+ }
+
+ // Property to access IsInteractionEnabled from parent ViewModel
+ public bool IsInteractionEnabled => _mainViewModel?.IsInteractionEnabled ?? true;
+
+ public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel)
+ {
+ _monitor = monitor;
+ _monitorManager = monitorManager;
+ _mainViewModel = mainViewModel;
+
+ // Subscribe to MainViewModel property changes to update IsInteractionEnabled
+ if (_mainViewModel != null)
+ {
+ _mainViewModel.PropertyChanged += OnMainViewModelPropertyChanged;
+ }
+
+ // Subscribe to underlying Monitor property changes (e.g., Orientation updates in mirror mode)
+ _monitor.PropertyChanged += OnMonitorPropertyChanged;
+
+ // Initialize Show properties based on hardware capabilities
+ _showContrast = monitor.SupportsContrast;
+ _showVolume = monitor.SupportsVolume;
+ _showInputSource = monitor.SupportsInputSource;
+ _showPowerState = monitor.SupportsPowerState;
+ _showColorTemperature = monitor.SupportsColorTemperature;
+
+ // Initialize basic properties from monitor
+ _brightness = monitor.CurrentBrightness;
+ _contrast = monitor.CurrentContrast;
+ _volume = monitor.CurrentVolume;
+ _isAvailable = monitor.IsAvailable;
+ }
+
+ public string Id => _monitor.Id;
+
+ public string Name => _monitor.Name;
+
+ ///
+ /// Gets the monitor number from the underlying monitor model (Windows DISPLAY number)
+ ///
+ public int MonitorNumber => _monitor.MonitorNumber;
+
+ ///
+ /// Gets the display name - includes monitor number when multiple monitors exist.
+ /// Follows the same logic as Settings UI's MonitorInfo.DisplayName for consistency.
+ ///
+ public string DisplayName
+ {
+ get
+ {
+ var monitorCount = _mainViewModel?.Monitors?.Count ?? 0;
+
+ // Show monitor number only when there are multiple monitors and MonitorNumber is valid
+ if (monitorCount > 1 && MonitorNumber > 0)
+ {
+ return $"{Name} {MonitorNumber}";
+ }
+
+ return Name;
+ }
+ }
+
+ public string CommunicationMethod => _monitor.CommunicationMethod;
+
+ public bool IsInternal => _monitor.CommunicationMethod == "WMI";
+
+ public string? CapabilitiesRaw => _monitor.CapabilitiesRaw;
+
+ public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo;
+
+ ///
+ /// Gets the icon glyph based on communication method
+ /// WMI monitors (laptop internal displays) use laptop icon, others use external monitor icon
+ ///
+ public string MonitorIconGlyph => _monitor.CommunicationMethod?.Contains("WMI", StringComparison.OrdinalIgnoreCase) == true
+ ? AppConstants.UI.InternalMonitorGlyph // Laptop icon for WMI
+ : AppConstants.UI.ExternalMonitorGlyph; // External monitor icon for DDC/CI and others
+
+ // Monitor property ranges
+ public int MinBrightness => _monitor.MinBrightness;
+
+ public int MaxBrightness => _monitor.MaxBrightness;
+
+ public int MinContrast => _monitor.MinContrast;
+
+ public int MaxContrast => _monitor.MaxContrast;
+
+ public int MinVolume => _monitor.MinVolume;
+
+ public int MaxVolume => _monitor.MaxVolume;
+
+ // Advanced control display logic
+ public bool HasAdvancedControls => ShowContrast || ShowVolume;
+
+ public bool ShowContrast
+ {
+ get => _showContrast;
+ set
+ {
+ if (_showContrast != value)
+ {
+ _showContrast = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasAdvancedControls));
+ }
+ }
+ }
+
+ public bool ShowVolume
+ {
+ get => _showVolume;
+ set
+ {
+ if (_showVolume != value)
+ {
+ _showVolume = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasAdvancedControls));
+ }
+ }
+ }
+
+ public bool ShowInputSource
+ {
+ get => _showInputSource;
+ set
+ {
+ if (_showInputSource != value)
+ {
+ _showInputSource = value;
+ OnPropertyChanged();
+ OnMoreButtonPropertiesChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to show power state control in the More Button flyout.
+ ///
+ public bool ShowPowerState
+ {
+ get => _showPowerState && SupportsPowerState;
+ set
+ {
+ if (_showPowerState != value)
+ {
+ _showPowerState = value;
+ OnPropertyChanged();
+ OnMoreButtonPropertiesChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the More Button should be visible.
+ /// Visible when at least one feature (InputSource or PowerState) is enabled.
+ ///
+ public bool ShowMoreButton => ShowInputSource || ShowPowerState;
+
+ ///
+ /// Gets a value indicating whether to show separator after Input Source section.
+ /// Only shown when both InputSource and PowerState are visible.
+ ///
+ public bool ShowSeparatorAfterInputSource => ShowInputSource && ShowPowerState;
+
+ ///
+ /// Notifies property changes for More Button related properties.
+ ///
+ private void OnMoreButtonPropertiesChanged()
+ {
+ OnPropertyChanged(nameof(ShowMoreButton));
+ OnPropertyChanged(nameof(ShowSeparatorAfterInputSource));
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to show rotation controls (controlled by Settings UI, default false).
+ ///
+ public bool ShowRotation
+ {
+ get => _showRotation;
+ set
+ {
+ if (_showRotation != value)
+ {
+ _showRotation = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets the current rotation/orientation of the monitor (0=normal, 1=90°, 2=180°, 3=270°)
+ ///
+ public int CurrentRotation => _monitor.Orientation;
+
+ ///
+ /// Gets a value indicating whether the current rotation is 0° (normal/default).
+ ///
+ public bool IsRotation0 => CurrentRotation == 0;
+
+ ///
+ /// Gets a value indicating whether the current rotation is 90° (rotated right).
+ ///
+ public bool IsRotation1 => CurrentRotation == 1;
+
+ ///
+ /// Gets a value indicating whether the current rotation is 180° (inverted).
+ ///
+ public bool IsRotation2 => CurrentRotation == 2;
+
+ ///
+ /// Gets a value indicating whether the current rotation is 270° (rotated left).
+ ///
+ public bool IsRotation3 => CurrentRotation == 3;
+
+ ///
+ /// Set rotation/orientation for this monitor.
+ /// Note: MonitorManager.SetRotationAsync will refresh all monitors' orientations after success,
+ /// which triggers PropertyChanged through OnMonitorPropertyChanged - no manual notification needed here.
+ ///
+ /// Orientation: 0=normal, 1=90°, 2=180°, 3=270°
+ public async Task SetRotationAsync(int orientation)
+ {
+ // Validate orientation range (0=normal, 1=90°, 2=180°, 3=270°)
+ if (orientation < 0 || orientation > 3)
+ {
+ return;
+ }
+
+ // If already at this orientation, do nothing
+ if (CurrentRotation == orientation)
+ {
+ return;
+ }
+
+ try
+ {
+ var result = await _monitorManager.SetRotationAsync(Id, orientation);
+
+ if (!result.IsSuccess)
+ {
+ Logger.LogWarning($"[{Id}] Failed to set rotation: {result.ErrorMessage}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[{Id}] Exception setting rotation: {ex.Message}");
+ }
+ }
+
+ public int Brightness
+ {
+ get => _brightness;
+ set
+ {
+ if (_brightness != value)
+ {
+ _ = SetBrightnessAsync(value);
+ }
+ }
+ }
+
+ ///
+ /// Gets color temperature VCP preset value (from VCP code 0x14).
+ /// Read-only in flyout UI - controlled via Settings UI.
+ /// Returns the raw VCP value (e.g., 0x05 for 6500K).
+ ///
+ public int ColorTemperature => _monitor.CurrentColorTemperature;
+
+ ///
+ /// Gets human-readable color temperature preset name (e.g., "6500K", "sRGB")
+ ///
+ public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
+
+ ///
+ /// Gets a value indicating whether this monitor supports color temperature via VCP 0x14
+ ///
+ public bool SupportsColorTemperature => _monitor.SupportsColorTemperature;
+
+ private List? _availableColorPresets;
+ private bool _showColorTemperature;
+
+ ///
+ /// Gets or sets a value indicating whether to show color temperature switcher (controlled by Settings UI, default false).
+ ///
+ public bool ShowColorTemperature
+ {
+ get => _showColorTemperature && SupportsColorTemperature;
+ set
+ {
+ if (_showColorTemperature != value)
+ {
+ _showColorTemperature = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets available color temperature presets for this monitor
+ ///
+ public List? AvailableColorPresets
+ {
+ get
+ {
+ if (_availableColorPresets == null && SupportsColorTemperature)
+ {
+ RefreshAvailableColorPresets();
+ }
+
+ return _availableColorPresets;
+ }
+ }
+
+ ///
+ /// Standard MCCS color temperature presets (VCP 0x14 values) to use as fallback
+ /// when the monitor doesn't report discrete values in its capabilities string.
+ ///
+ private static readonly int[] StandardColorTemperaturePresets = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08, 0x09, 0x0A, 0x0B };
+
+ ///
+ /// Refresh the list of available color temperature presets based on monitor capabilities
+ ///
+ private void RefreshAvailableColorPresets()
+ {
+ if (!SupportsColorTemperature)
+ {
+ _availableColorPresets = null;
+ return;
+ }
+
+ IEnumerable presetValues;
+ var vcpInfo = VcpCapabilitiesInfo;
+
+ // Try to get discrete values from capabilities string
+ if (vcpInfo != null &&
+ vcpInfo.SupportedVcpCodes.TryGetValue(0x14, out var colorTempInfo) &&
+ colorTempInfo.HasDiscreteValues &&
+ colorTempInfo.SupportedValues.Count > 0)
+ {
+ // Use values from capabilities string
+ presetValues = colorTempInfo.SupportedValues;
+ }
+ else
+ {
+ // Fallback to standard MCCS presets when capabilities don't list discrete values
+ presetValues = StandardColorTemperaturePresets;
+ }
+
+ _availableColorPresets = presetValues.Select(value => new ColorTemperatureItem
+ {
+ VcpValue = value,
+ DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value),
+ IsSelected = value == _monitor.CurrentColorTemperature,
+ MonitorId = _monitor.Id,
+ }).ToList();
+
+ OnPropertyChanged(nameof(AvailableColorPresets));
+ }
+
+ ///
+ /// Gets a value indicating whether this monitor supports input source switching via VCP 0x60
+ ///
+ public bool SupportsInputSource => _monitor.SupportsInputSource;
+
+ ///
+ /// Gets current input source VCP value (from VCP code 0x60)
+ ///
+ public int CurrentInputSource => _monitor.CurrentInputSource;
+
+ ///
+ /// Gets human-readable current input source name (e.g., "HDMI-1", "DisplayPort-1")
+ ///
+ public string CurrentInputSourceName => _monitor.InputSourceName;
+
+ private List? _availableInputSources;
+
+ ///
+ /// Gets available input sources for this monitor
+ ///
+ public List? AvailableInputSources
+ {
+ get
+ {
+ if (_availableInputSources == null && SupportsInputSource)
+ {
+ RefreshAvailableInputSources();
+ }
+
+ return _availableInputSources;
+ }
+ }
+
+ ///
+ /// Refresh the list of available input sources based on monitor capabilities
+ ///
+ private void RefreshAvailableInputSources()
+ {
+ var supportedSources = _monitor.SupportedInputSources;
+ if (supportedSources == null || supportedSources.Count == 0)
+ {
+ _availableInputSources = null;
+ return;
+ }
+
+ _availableInputSources = supportedSources.Select(value => new InputSourceItem
+ {
+ Value = value,
+ Name = Common.Utils.VcpNames.GetValueName(0x60, value) ?? $"Source 0x{value:X2}",
+ SelectionVisibility = value == _monitor.CurrentInputSource ? Visibility.Visible : Visibility.Collapsed,
+ MonitorId = _monitor.Id,
+ }).ToList();
+
+ OnPropertyChanged(nameof(AvailableInputSources));
+ }
+
+ ///
+ /// Set input source for this monitor
+ ///
+ public async Task SetInputSourceAsync(int inputSource)
+ {
+ try
+ {
+ var result = await _monitorManager.SetInputSourceAsync(Id, inputSource);
+
+ if (result.IsSuccess)
+ {
+ OnPropertyChanged(nameof(CurrentInputSource));
+ OnPropertyChanged(nameof(CurrentInputSourceName));
+ RefreshAvailableInputSources();
+ }
+ else
+ {
+ Logger.LogWarning($"[{Id}] Failed to set input source: {result.ErrorMessage}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[{Id}] Exception setting input source: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Command to set input source
+ ///
+ [RelayCommand]
+ private async Task SetInputSource(int? source)
+ {
+ if (source.HasValue)
+ {
+ await SetInputSourceAsync(source.Value);
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether this monitor supports power state control via VCP 0xD6
+ ///
+ public bool SupportsPowerState => _monitor.SupportsPowerState;
+
+ private List? _availablePowerStates;
+
+ ///
+ /// Gets available power states for this monitor.
+ /// The current power state is shown as selected based on the monitor's actual state.
+ ///
+ public List? AvailablePowerStates
+ {
+ get
+ {
+ if (_availablePowerStates == null && SupportsPowerState)
+ {
+ RefreshAvailablePowerStates();
+ }
+
+ return _availablePowerStates;
+ }
+ }
+
+ ///
+ /// Refresh the list of available power states based on monitor capabilities
+ ///
+ private void RefreshAvailablePowerStates()
+ {
+ var supportedStates = _monitor.SupportedPowerStates;
+ if (supportedStates == null || supportedStates.Count == 0)
+ {
+ _availablePowerStates = null;
+ return;
+ }
+
+ _availablePowerStates = supportedStates.Select(value => new PowerStateItem
+ {
+ Value = value,
+ Name = Common.Utils.VcpNames.GetValueName(0xD6, value) ?? $"State 0x{value:X2}",
+ IsSelected = value == _monitor.CurrentPowerState,
+ MonitorId = _monitor.Id,
+ }).ToList();
+
+ OnPropertyChanged(nameof(AvailablePowerStates));
+ }
+
+ ///
+ /// Set power state for this monitor.
+ /// Note: Setting any state other than "On" will turn off the display.
+ ///
+ public async Task SetPowerStateAsync(int powerState)
+ {
+ try
+ {
+ var result = await _monitorManager.SetPowerStateAsync(Id, powerState);
+
+ if (result.IsSuccess)
+ {
+ // Update the model's power state and refresh UI
+ _monitor.CurrentPowerState = powerState;
+ RefreshAvailablePowerStates();
+ }
+ else
+ {
+ Logger.LogWarning($"[{Id}] Failed to set power state: {result.ErrorMessage}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[{Id}] Exception setting power state: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Command to set power state
+ ///
+ [RelayCommand]
+ private async Task SetPowerState(int? state)
+ {
+ if (state.HasValue)
+ {
+ await SetPowerStateAsync(state.Value);
+ }
+ }
+
+ public int Contrast
+ {
+ get => _contrast;
+ set
+ {
+ if (_contrast != value)
+ {
+ _ = SetContrastAsync(value);
+ }
+ }
+ }
+
+ public int Volume
+ {
+ get => _volume;
+ set
+ {
+ if (_volume != value)
+ {
+ _ = SetVolumeAsync(value);
+ }
+ }
+ }
+
+ public bool IsAvailable
+ {
+ get => _isAvailable;
+ set
+ {
+ _isAvailable = value;
+ OnPropertyChanged();
+ }
+ }
+
+ [RelayCommand]
+ private void SetBrightness(int? brightness)
+ {
+ if (brightness.HasValue)
+ {
+ Brightness = brightness.Value;
+ }
+ }
+
+ [RelayCommand]
+ private void SetContrast(int? contrast)
+ {
+ if (contrast.HasValue)
+ {
+ Contrast = contrast.Value;
+ }
+ }
+
+ [RelayCommand]
+ private void SetVolume(int? volume)
+ {
+ if (volume.HasValue)
+ {
+ Volume = volume.Value;
+ }
+ }
+
+ public int ContrastPercent
+ {
+ get => MapToPercent(_contrast, MinContrast, MaxContrast);
+ set
+ {
+ var actualValue = MapFromPercent(value, MinContrast, MaxContrast);
+ Contrast = actualValue;
+ }
+ }
+
+ // Mapping functions for percentage conversion
+ private int MapToPercent(int value, int min, int max)
+ {
+ if (max <= min)
+ {
+ return 0;
+ }
+
+ return (int)Math.Round((value - min) * 100.0 / (max - min));
+ }
+
+ private int MapFromPercent(int percent, int min, int max)
+ {
+ if (max <= min)
+ {
+ return min;
+ }
+
+ percent = Math.Clamp(percent, 0, 100);
+ return min + (int)Math.Round(percent * (max - min) / 100.0);
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(MainViewModel.IsInteractionEnabled))
+ {
+ OnPropertyChanged(nameof(IsInteractionEnabled));
+ }
+ else if (e.PropertyName == nameof(MainViewModel.HasMonitors))
+ {
+ // Monitor count changed, update display name to show/hide number suffix
+ OnPropertyChanged(nameof(DisplayName));
+ }
+ }
+
+ private void OnMonitorPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ // Forward Orientation changes from underlying Monitor to ViewModel properties
+ // This is important for mirror mode where MonitorManager.RefreshAllOrientations()
+ // updates multiple monitors sharing the same GdiDeviceName
+ if (e.PropertyName == nameof(Monitor.Orientation))
+ {
+ OnPropertyChanged(nameof(CurrentRotation));
+ OnPropertyChanged(nameof(IsRotation0));
+ OnPropertyChanged(nameof(IsRotation1));
+ OnPropertyChanged(nameof(IsRotation2));
+ OnPropertyChanged(nameof(IsRotation3));
+ }
+ }
+
+ public void Dispose()
+ {
+ // Unsubscribe from MainViewModel events
+ if (_mainViewModel != null)
+ {
+ _mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged;
+ }
+
+ // Unsubscribe from underlying Monitor events
+ _monitor.PropertyChanged -= OnMonitorPropertyChanged;
+
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/PowerStateItem.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/PowerStateItem.cs
new file mode 100644
index 0000000000..6be02e8d7f
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/PowerStateItem.cs
@@ -0,0 +1,45 @@
+// 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.UI.Xaml;
+
+namespace PowerDisplay.ViewModels;
+
+///
+/// Represents a power state option for display in UI.
+/// VCP 0xD6 values: 0x01=On, 0x02=Standby, 0x03=Suspend, 0x04=Off(DPM), 0x05=Off(Hard)
+///
+public class PowerStateItem
+{
+ ///
+ /// VCP power mode value representing On state
+ ///
+ public const int PowerStateOn = 0x01;
+
+ ///
+ /// VCP value for this power state
+ ///
+ public int Value { get; set; }
+
+ ///
+ /// Human-readable name (e.g., "On", "Standby", "Off (DPM)")
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets whether this power state is currently selected.
+ /// Set based on monitor's actual power state during list creation.
+ ///
+ public bool IsSelected { get; set; }
+
+ ///
+ /// Visibility of selection indicator (Visible when IsSelected is true)
+ ///
+ public Visibility SelectionVisibility => IsSelected ? Visibility.Visible : Visibility.Collapsed;
+
+ ///
+ /// Monitor ID for direct lookup (Flyout popup is not in visual tree)
+ ///
+ public string MonitorId { get; set; } = string.Empty;
+}
diff --git a/src/modules/powerdisplay/PowerDisplay/app.manifest b/src/modules/powerdisplay/PowerDisplay/app.manifest
new file mode 100644
index 0000000000..8a5a071870
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay/app.manifest
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.rc b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.rc
new file mode 100644
index 0000000000..2f225053a0
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.rc
@@ -0,0 +1,97 @@
+// Microsoft Visual C++ generated resource script.
+//
+#include
+#include "resource.h"
+#include "../../../common/version/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+1 VERSIONINFO
+FILEVERSION FILE_VERSION
+PRODUCTVERSION PRODUCT_VERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+FILEFLAGS VS_FF_DEBUG
+#else
+FILEFLAGS 0x0L
+#endif
+FILEOS VOS_NT_WINDOWS32
+FILETYPE VFT_DLL
+FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
+ BEGIN
+ VALUE "CompanyName", COMPANY_NAME
+ VALUE "FileDescription", FILE_DESCRIPTION
+ VALUE "FileVersion", FILE_VERSION_STRING
+ VALUE "InternalName", INTERNAL_NAME
+ VALUE "LegalCopyright", COPYRIGHT_NOTE
+ VALUE "OriginalFilename", ORIGINAL_FILENAME
+ VALUE "ProductName", PRODUCT_NAME
+ VALUE "ProductVersion", PRODUCT_VERSION_STRING
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
+ END
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj
new file mode 100644
index 0000000000..6c68d0e291
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj
@@ -0,0 +1,133 @@
+
+
+
+
+
+ Debug
+ ARM64
+
+
+ Release
+ ARM64
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ {D1234567-8901-2345-6789-ABCDEF012345}
+ Win32Proj
+ PowerDisplayModuleInterface
+
+
+
+ DynamicLibrary
+ true
+ v143
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\$(Platform)\$(Configuration)\
+ $(Platform)\$(Configuration)\PowerDisplayModuleInterface\
+ PowerToys.PowerDisplayModuleInterface
+
+
+
+ Level3
+ true
+ _DEBUG;POWERDISPLAYMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+ Windows
+ true
+ false
+ Shlwapi.lib;Rpcrt4.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;POWERDISPLAYMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+ Windows
+ true
+ true
+ true
+ false
+ Shlwapi.lib;Rpcrt4.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)
+
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj.filters b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj.filters
new file mode 100644
index 0000000000..0872553d99
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj.filters
@@ -0,0 +1,53 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Resource Files
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.cpp b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.cpp
new file mode 100644
index 0000000000..6f35629d3b
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.cpp
@@ -0,0 +1,282 @@
+// 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.
+
+#include "pch.h"
+#include "PowerDisplayProcessManager.h"
+
+#include
+#include
+#include
+#include
+
+namespace
+{
+ std::optional get_pipe_name(const std::wstring& prefix)
+ {
+ UUID temp_uuid;
+ wchar_t* uuid_chars = nullptr;
+ if (UuidCreate(&temp_uuid) == RPC_S_UUID_NO_ADDRESS)
+ {
+ const auto val = get_last_error_message(GetLastError());
+ Logger::error(L"UuidCreate cannot create guid. {}", val.has_value() ? val.value() : L"");
+ return std::nullopt;
+ }
+ else if (UuidToString(&temp_uuid, reinterpret_cast(&uuid_chars)) != RPC_S_OK)
+ {
+ const auto val = get_last_error_message(GetLastError());
+ Logger::error(L"UuidToString cannot convert to string. {}", val.has_value() ? val.value() : L"");
+ return std::nullopt;
+ }
+
+ const auto pipe_name = std::format(L"{}{}", prefix, std::wstring(uuid_chars));
+ RpcStringFree(reinterpret_cast(&uuid_chars));
+
+ return pipe_name;
+ }
+}
+
+void PowerDisplayProcessManager::start()
+{
+ m_enabled = true;
+ submit_task([this]() { refresh(); });
+}
+
+void PowerDisplayProcessManager::stop()
+{
+ m_enabled = false;
+ submit_task([this]() { refresh(); });
+}
+
+void PowerDisplayProcessManager::send_message(const std::wstring& message_type, const std::wstring& message_arg)
+{
+ submit_task([this, message_type, message_arg] {
+ // Ensure process is running before sending message
+ if (!is_process_running() && m_enabled)
+ {
+ refresh();
+ }
+ send_named_pipe_message(message_type, message_arg);
+ });
+}
+
+void PowerDisplayProcessManager::bring_to_front()
+{
+ submit_task([this] {
+ if (!is_process_running())
+ {
+ return;
+ }
+
+ const auto enum_windows = [](HWND hwnd, LPARAM param) -> BOOL {
+ const auto process_handle = reinterpret_cast(param);
+ DWORD window_process_id = 0;
+
+ GetWindowThreadProcessId(hwnd, &window_process_id);
+ if (GetProcessId(process_handle) == window_process_id)
+ {
+ SetForegroundWindow(hwnd);
+ return FALSE;
+ }
+ return TRUE;
+ };
+
+ EnumWindows(enum_windows, reinterpret_cast(m_hProcess));
+ });
+}
+
+bool PowerDisplayProcessManager::is_running() const
+{
+ return is_process_running();
+}
+
+void PowerDisplayProcessManager::submit_task(std::function task)
+{
+ m_thread_executor.submit(OnThreadExecutor::task_t{ task });
+}
+
+bool PowerDisplayProcessManager::is_process_running() const
+{
+ return m_hProcess != 0 && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
+}
+
+void PowerDisplayProcessManager::terminate_process()
+{
+ if (m_hProcess != 0)
+ {
+ TerminateProcess(m_hProcess, 1);
+ CloseHandle(m_hProcess);
+ m_hProcess = 0;
+ }
+}
+
+HRESULT PowerDisplayProcessManager::start_process(const std::wstring& pipe_name)
+{
+ const unsigned long powertoys_pid = GetCurrentProcessId();
+
+ // Pass both PID and pipe name as arguments
+ const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_name);
+
+ SHELLEXECUTEINFOW sei{ sizeof(sei) };
+ sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
+ sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
+ sei.nShow = SW_SHOWNORMAL;
+ sei.lpParameters = executable_args.data();
+ if (ShellExecuteExW(&sei))
+ {
+ Logger::trace("Successfully started PowerDisplay process");
+ terminate_process();
+ m_hProcess = sei.hProcess;
+ return S_OK;
+ }
+ else
+ {
+ Logger::error(L"PowerDisplay process failed to start. {}", get_last_error_or_default(GetLastError()));
+ return E_FAIL;
+ }
+}
+
+HRESULT PowerDisplayProcessManager::start_named_pipe_server(const std::wstring& pipe_name)
+{
+ m_write_pipe = nullptr;
+
+ const constexpr DWORD BUFSIZE = 4096 * 4;
+
+ const auto full_pipe_name = std::format(L"\\\\.\\pipe\\{}", pipe_name);
+
+ const auto hPipe = CreateNamedPipe(
+ full_pipe_name.c_str(), // pipe name
+ PIPE_ACCESS_OUTBOUND | // write access
+ FILE_FLAG_OVERLAPPED, // overlapped mode
+ PIPE_TYPE_MESSAGE | // message type pipe
+ PIPE_READMODE_MESSAGE | // message-read mode
+ PIPE_WAIT, // blocking mode
+ 1, // max. instances
+ BUFSIZE, // output buffer size
+ 0, // input buffer size
+ 0, // client time-out
+ NULL); // default security attribute
+
+ if (hPipe == NULL || hPipe == INVALID_HANDLE_VALUE)
+ {
+ Logger::error(L"Error creating handle for named pipe");
+ return E_FAIL;
+ }
+
+ // Create overlapped event to wait for client to connect to pipe.
+ OVERLAPPED overlapped = { 0 };
+ overlapped.hEvent = CreateEvent(nullptr, true, false, nullptr);
+ if (!overlapped.hEvent)
+ {
+ Logger::error(L"Error creating overlapped event for named pipe");
+ CloseHandle(hPipe);
+ return E_FAIL;
+ }
+
+ const auto clean_up_and_fail = [&]() {
+ CloseHandle(overlapped.hEvent);
+ CloseHandle(hPipe);
+ return E_FAIL;
+ };
+
+ if (!ConnectNamedPipe(hPipe, &overlapped))
+ {
+ const auto lastError = GetLastError();
+
+ if (lastError != ERROR_IO_PENDING && lastError != ERROR_PIPE_CONNECTED)
+ {
+ Logger::error(L"Error connecting to named pipe");
+ return clean_up_and_fail();
+ }
+ }
+
+ // Wait for client.
+ const constexpr DWORD client_timeout_millis = 5000;
+ switch (WaitForSingleObject(overlapped.hEvent, client_timeout_millis))
+ {
+ case WAIT_OBJECT_0:
+ {
+ DWORD bytes_transferred = 0;
+ if (GetOverlappedResult(hPipe, &overlapped, &bytes_transferred, FALSE))
+ {
+ CloseHandle(overlapped.hEvent);
+ m_write_pipe = std::make_unique(hPipe);
+
+ Logger::trace(L"PowerDisplay successfully connected to named pipe");
+
+ return S_OK;
+ }
+ else
+ {
+ Logger::error(L"Error waiting for PowerDisplay to connect to named pipe");
+ return clean_up_and_fail();
+ }
+ }
+
+ case WAIT_TIMEOUT:
+ case WAIT_FAILED:
+ default:
+ Logger::error(L"Error waiting for PowerDisplay to connect to named pipe");
+ return clean_up_and_fail();
+ }
+}
+
+void PowerDisplayProcessManager::refresh()
+{
+ if (m_enabled == is_process_running())
+ {
+ return;
+ }
+
+ if (m_enabled)
+ {
+ Logger::trace(L"Starting PowerDisplay process");
+
+ const auto pipe_name = get_pipe_name(L"powertoys_power_display_");
+
+ if (!pipe_name)
+ {
+ return;
+ }
+
+ if (start_process(pipe_name.value()) != S_OK)
+ {
+ return;
+ }
+
+ if (start_named_pipe_server(pipe_name.value()) != S_OK)
+ {
+ Logger::error(L"Named pipe initialization failed; terminating PowerDisplay process");
+ terminate_process();
+ }
+ }
+ else
+ {
+ Logger::trace(L"Exiting PowerDisplay process");
+
+ send_named_pipe_message(CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE);
+ WaitForSingleObject(m_hProcess, 5000);
+
+ if (is_process_running())
+ {
+ Logger::error(L"PowerDisplay process failed to gracefully exit; terminating");
+ }
+ else
+ {
+ Logger::trace(L"PowerDisplay process successfully exited");
+ }
+
+ terminate_process();
+ }
+}
+
+void PowerDisplayProcessManager::send_named_pipe_message(const std::wstring& message_type, const std::wstring& message_arg)
+{
+ if (m_write_pipe)
+ {
+ const auto message = message_arg.empty() ? std::format(L"{}\r\n", message_type) : std::format(L"{} {}\r\n", message_type, message_arg);
+
+ const CString file_name(message.c_str());
+ m_write_pipe->Write(file_name, file_name.GetLength() * sizeof(TCHAR));
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.h b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.h
new file mode 100644
index 0000000000..98e31918b3
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.h
@@ -0,0 +1,65 @@
+// 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.
+
+#pragma once
+#include "pch.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+///
+/// Manages the PowerDisplay.exe process and Named Pipe communication.
+/// Based on AdvancedPasteProcessManager pattern.
+///
+class PowerDisplayProcessManager
+{
+public:
+ PowerDisplayProcessManager() = default;
+ PowerDisplayProcessManager(const PowerDisplayProcessManager&) = delete;
+ PowerDisplayProcessManager& operator=(const PowerDisplayProcessManager&) = delete;
+
+ ///
+ /// Enable the module - starts the PowerDisplay.exe process.
+ ///
+ void start();
+
+ ///
+ /// Disable the module - terminates the PowerDisplay.exe process.
+ ///
+ void stop();
+
+ ///
+ /// Send a message to PowerDisplay.exe via Named Pipe.
+ ///
+ /// The message type (e.g., "Toggle", "ApplyProfile")
+ /// Optional message argument
+ void send_message(const std::wstring& message_type, const std::wstring& message_arg = L"");
+
+ ///
+ /// Bring the PowerDisplay window to the foreground.
+ ///
+ void bring_to_front();
+
+ ///
+ /// Check if PowerDisplay.exe process is running.
+ ///
+ bool is_running() const;
+
+private:
+ void submit_task(std::function task);
+ bool is_process_running() const;
+ void terminate_process();
+ HRESULT start_process(const std::wstring& pipe_name);
+ HRESULT start_named_pipe_server(const std::wstring& pipe_name);
+ void refresh();
+ void send_named_pipe_message(const std::wstring& message_type, const std::wstring& message_arg = L"");
+
+ OnThreadExecutor m_thread_executor; // all internal operations are done on background thread with task queue
+ std::atomic m_enabled = false; // written on main thread, read on background thread
+ HANDLE m_hProcess = 0;
+ std::unique_ptr m_write_pipe;
+};
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.cpp b/src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.cpp
new file mode 100644
index 0000000000..3ac410724b
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.cpp
@@ -0,0 +1,32 @@
+#include "pch.h"
+#include "trace.h"
+
+#include
+
+TRACELOGGING_DEFINE_PROVIDER(
+ g_hProvider,
+ "Microsoft.PowerToys",
+ // {38e8889b-9731-53f5-e901-e8a7c1753074}
+ (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
+ TraceLoggingOptionProjectTelemetry());
+
+// Log if the user has enabled or disabled the app
+void Trace::EnablePowerDisplay(_In_ bool enabled) noexcept
+{
+ TraceLoggingWriteWrapper(
+ g_hProvider,
+ "PowerDisplay_EnablePowerDisplay",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
+ TraceLoggingBoolean(enabled, "Enabled"));
+}
+
+// Log that the user tried to activate the app
+void Trace::ActivatePowerDisplay() noexcept
+{
+ TraceLoggingWriteWrapper(
+ g_hProvider,
+ "PowerDisplay_Activate",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
+}
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.h b/src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.h
new file mode 100644
index 0000000000..c650cfb346
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include
+
+class Trace : public telemetry::TraceBase
+{
+public:
+ // Log if the user has enabled or disabled the app
+ static void EnablePowerDisplay(const bool enabled) noexcept;
+
+ // Log that the user tried to activate the app
+ static void ActivatePowerDisplay() noexcept;
+};
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
new file mode 100644
index 0000000000..871a8797ef
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
@@ -0,0 +1,245 @@
+// dllmain.cpp : Defines the entry point for the DLL Application.
+#include "pch.h"
+#include
+#include
+#include "trace.h"
+#include "PowerDisplayProcessManager.h"
+#include
+#include
+#include