[PowerDisplay] Add headless CLI and stable profile IDs (PowerToys.PowerDisplay.Cli)#48632
Draft
moooyo wants to merge 56 commits into
Draft
[PowerDisplay] Add headless CLI and stable profile IDs (PowerToys.PowerDisplay.Cli)#48632moooyo wants to merge 56 commits into
moooyo wants to merge 56 commits into
Conversation
This comment has been minimized.
This comment has been minimized.
moooyo
pushed a commit
that referenced
this pull request
Jun 17, 2026
…bugs, broaden power-off gate, simplify, clean up From PR #48632 review: Bugs: - Reject out-of-byte hex discrete values (0x100, 0xFFFFFFFF) so they surface INVALID_DISCRETE_VALUE instead of being truncated/sign-extended into a wrong byte at the native SetVCPFeature((uint)value) call; the cap-string supported set is still used to validate. Drop the dead StartsWith("0X") clause. - CliSettingsReader: skip a null monitor array element instead of throwing and discarding MaxCompatibilityMode plus every valid hidden id. - Program: guard Logger.InitializeLogger so an unwritable log path degrades to no file listener rather than crashing with a raw stack trace outside the single-envelope error contract. - Reject a negative --timeout at parse time (ARGUMENT_ERROR) instead of silently disabling the timeout. - get: echo the user's original --setting input on an unknown-setting error, and emit that monitor-independent error without pinning it to the first monitor. Contract / safety: - Require --confirm-power-off for Standby/Suspend (DPMS sleep), not only Off, and gate on the already-resolved VCP value so the raw input is parsed once (removes the duplicate IsPowerOff resolve). Simplify / clean up: - capabilities: drop the duplicated monitor.method; CliMonitorRef.Method is now nullable/omitted and transport lives only in the communicationMethod field. - Remove now-unused PowerDisplay.Common.Services usings from the GUI and the dead MaxCompatibilityModeSet tracking from the test fake. - Docs: correct JSON envelope key order (ok, version, command), document the /? help alias and negative-timeout rejection, and reflect the broadened gate. Tests: byte-range guard (unit + end-to-end), Standby confirmation, set volume/color-temperature/input-source, get RunAsync (selector, hidden filtering, selector-conflict warning, unknown setting), case-insensitive --setting, and a both-selectors single-write assertion. All 122 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds PowerToys.PowerDisplay.Cli.exe, a headless command-line front end for the PowerDisplay module. The CLI is a thin IPC client: it connects to the running PowerDisplay app over a per-session named pipe and the app performs all hardware work (DDC/CI and GDI writes) against its live monitor cache, so the CLI itself needs no elevation or direct hardware access. Projects: - PowerDisplay.Contracts - shared request/result/error DTOs, the stable exit/error codes, the named-pipe name + line framing, and a System.Text.Json source-gen context (AOT-safe). Single source for setting names, command names, and the code->exit-code mapping; responses carry an explicit IsError discriminator. - PowerDisplay.Cli - System.CommandLine front end: parse -> CLI-side validation -> send the request over the pipe -> render human-readable text -> map the result to a process exit code. UTF-8 output, --timeout/--quiet, localized strings. - PowerDisplay (app) - named-pipe server with a cross-integrity ACL so a non-elevated CLI can reach an elevated app; a request handler that marshals onto the UI thread; and projectors/executor that turn the monitor model into DTOs and apply set / apply-profile writes with capability validation. Commands: list, get, set, capabilities, profiles, apply-profile - with -n/-i monitor selectors, --setting filter, and --confirm-power-off gating for display-blanking power states. Exit codes 0-10 are a stable contract (10 = PowerDisplay not running). Covered by Contracts/Cli/Ipc unit tests.
Dead-code removal, de-duplication and simplification across the new PowerDisplay CLI, Contracts and IPC-server layers. No change to the CLI's text output / observable behavior. Simplifications: - IpcDispatcher: collapse six near-identical per-command Send*Async helpers into one generic SendAndRenderAsync<T>; drop the standalone Deserialize<T> seam and a redundant Ok=false initializer. - Program.cs: inline single-call BuildRootCommand, drop the redundant DispatchAsync `command` parameter, extract a shared ArgumentError envelope helper. - CliRequestHandler: merge MakeInternalError/MakeArgumentError into one MakeError(...) with an optional hint; reuse it for the apply-profile not-found path. - MonitorDtoProjector/SetCommandExecutor: remove the selector "warning" channel that no caller read (the CLI emits that warning client-side), share a single ToRef, drop the redundant confirmationSetting param, and pass monitorId instead of the whole Monitor where only the id is used. - IMonitorManager: drop SetMaxCompatibilityMode/DiscoverMonitorsAsync, which production never calls through the interface (concrete MonitorManager keeps them), plus the dead stubs in the test fakes. Dead Contracts surface (only ever produced, never read by any renderer; there is no machine-readable/JSON output mode today): - Remove `Ok` from all result DTOs (success/error is conveyed by IsError + ExitCode), CliError.Setting/Requested, CliListMonitor.Supports*, CliSettingValue.Raw, and CliSetResult.BeforeRaw/AfterRaw, plus all their writers. Net -325 lines (production -211). All 163 unit tests pass (Contracts 15, CLI 56, Ipc 92). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ties --setting PowerDisplay CLI behavior changes: - `set` for the 3 discrete settings (color-temperature, input-source, power-state) now accepts ONLY a hex VCP value (0x??). Friendly-name input is dropped because the generic VCP name table can disagree with a specific monitor's value mapping (a silent ambiguity). Removes TryParseFriendlyName; error hints and option help now point at `capabilities --setting`. - `get` and `capabilities` output render discrete value names honoring the user's CustomVcpValueMapping (custom name when present, else the built-in name) — the same VcpNames.GetValueName(code, value, customMappings, monitorId) the main app uses. CustomVcpMappings are threaded from MainViewModel through CliRequestHandler.BuildResponseAsync into MonitorDtoProjector. No new wire fields: names are baked into the existing Display / DiscreteValues strings. - `capabilities` gains an optional `--setting <name>` filter restricted to the 3 discrete settings (new CapabilitiesRequest.SettingFilter). A non-discrete or unknown name returns ARGUMENT_ERROR (exit 7); validation is app-side in BuildCapabilitiesResult via VcpCodeForDiscreteSetting. Implemented test-first. All suites green: Contracts 15, CLI 56, Ipc 100 (171). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts and prune stale docs
Wire the new headless-CLI code to the single-source-of-truth constants it
already defines, instead of re-typing string/number literals at each site:
- setting names -> CliSettingNames.* (CliRequestBuilder.BuildSet,
SetCommandExecutor dispatch + Apply* args, MonitorDtoProjector switches,
MainViewModel.Settings restore path)
- discrete VCP codes 0x14/0x60/0xD6 -> NativeConstants.VcpCode{SelectColorPreset,
InputSource,PowerMode} in the new Ipc files
- success-DTO Command discriminators -> CliCommandNames.* (six Contracts results)
All substitutions are value-identical; no wire or behavior change.
Also clean up documentation rot in the same files:
- remove stale "Mirrors <CliType>.<method>" comments that referenced a CLI
projection/validation layer which no longer exists (the app-side projectors
are now the sole producers of these DTOs)
- fix the factually wrong "avoids a LINQ dependency" note on
SetCommandExecutor.ContainsValue (LINQ is used freely across the solution)
- reword CliPipeServer's concurrency doc to describe its actual serial
accept loop instead of claiming non-blocking concurrent service
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete three types that duplicated or added no information over existing contracts, collapsing the per-element copies and marker payloads they forced: - CliListMonitor was structurally identical to CliMonitorRef (only Method nullability differed, and the list path always sets it). CliListResult.Monitors now uses CliMonitorRef directly and BuildListResult projects via the shared ToRef helper; JSON output is unchanged. - ProfileChangeOutcome (a ViewModels record struct) carried the exact five fields of Contracts.CliProfileChange and reused its status constants. ProfileApplyOutcome.Changes and the profile-restore path now use CliProfileChange directly, and ProfileDtoProjector assigns the list through instead of copying every element. - The empty ListRequest/ProfilesRequest marker payloads were never read (dispatch keys off envelope.Command); removed them with the matching CliRequestEnvelope properties and the two now-trivial round-trip tests. Behavior- and wire-preserving. Full MSBuild + 169 PowerDisplay unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavior-preserving simplifications in the new CLI/IPC code: - SetCommand.CountSelectedSettings: replace the seven-branch if/count++ with a single Count over the boxed setting values. A continuous int? of 0 still boxes to a non-null object, so zero-valued settings are counted exactly as before. - SetCommandExecutor: drop the hand-rolled ContainsValue loop for the framework IReadOnlyList<int>.Contains (AOT-safe over int). - IpcDispatcher: fold the five identical `_ => CliExitCodes.Ok` exit selectors into a default-Ok SendAsync wrapper; apply-profile keeps its data-driven code. - TextCliOutput: extract the repeated `Monitor N (Name)` label into one MonitorLabel helper used by all five render sites. Full MSBuild + 169 PowerDisplay unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rence equality DispatchAsync now switches on parseResult.CommandResult.Command.Name matched against the shared CliCommandNames constants, and the subcommands are created with those same constants as their names. This removes the six identity-only Command properties on PowerDisplayRootCommand and the "instances must be shared singletons for reference-equality dispatch" constraint. Routing, the single-envelope error contract, startup ordering, and DispatchAsync's test seam are unchanged. Full MSBuild + 169 PowerDisplay unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s, pipe robustness Address review findings on the PowerDisplay CLI/IPC code: - Cancellation now maps to TIMEOUT (exit 8) instead of INTERNAL_ERROR (exit 9), matching SetCommandExecutor's documented contract; mapping moved into the testable BuildResponseAsync so the set/apply-profile paths are covered. - apply-profile threads the CancellationToken end to end so it honours --timeout/Ctrl+C like the set path; cancellation propagates as TIMEOUT rather than being swallowed into a per-setting hardware-failure outcome. - Profiles are loaded lazily: list/get/set/capabilities no longer do UI-thread synchronous disk I/O, and apply-profile no longer reads the profile file twice. - MainViewModel.MonitorManager returns IMonitorManager so the IPC path depends on the abstraction, not the concrete class. - CliPipeServer bounds each request read by time (ReadTimeoutMilliseconds) and length (MaxRequestChars) so a hung/oversized client cannot stall the single-threaded accept loop or balloon memory. - Collapsed the seven per-type Serialize overloads into one generic helper. - Extracted MonitorSelectorRequest as a shared base for GetRequest/CapabilitiesRequest. - CLI Console.CancelKeyPress handler is unsubscribed in finally; the no-timeout magic value is documented; SnapshotMonitors returns a materialized copy. Adds tests: cancellation->TIMEOUT, ReadBoundedLineAsync (8 cases), inherited selector round-trip. All PowerDisplay unit tests pass (Cli 56, Ipc 109, Contracts 14). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…jection Phase 1 of consolidating the per-setting VCP metadata that was hand-duplicated across the CLI read/write paths into a single source. - Add CliSettingKind / CliVcpSetting / CliSettingCatalog: one descriptor row per VCP setting (name, kind, VCP code, read flag, supports/current selectors). Orientation is intentionally excluded (GDI-based, not a VCP setting). - MonitorDtoProjector.BuildSettingValue and VcpCodeForDiscreteSetting now consult the catalog instead of parallel switch arms; the direct NativeConstants dependency is dropped. Behaviour-preserving: the existing MonitorDtoProjector tests stay green; adds CliSettingCatalog invariant tests. Ipc unit tests: 114 pass. Write-side descriptor fields (supported values, apply delegate, unsupported reason, blanking gate) are deferred to phase 2 where SetCommandExecutor consumes them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… CliSettingCatalog Phase 2 of consolidating per-setting VCP metadata. - Extend CliVcpSetting with the write-side fields: supported-value selector, hardware-write delegate, unsupported-reason text, and the display-blanking gate. - SetCommandExecutor.ExecuteAsync replaces its seven hand-maintained switch arms with a single catalog lookup that dispatches by Kind (continuous vs discrete); the per-setting fields (supports/current/read-flag/vcp-code/supported-values/ apply/unsupported-reason/blanking) now come from the descriptor. Orientation stays a special case (GDI, not VCP); the direct NativeConstants dependency is dropped. Behaviour-preserving: the 601-line SetCommandExecutor test suite stays green; adds catalog assertions for the blanking gate and continuous supported-values. Ipc unit tests: 116 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he monitor's supported set Phase 3 of the per-setting consolidation, and a behaviour fix it surfaced. The apply-profile outcomes path only range-checked color-temperature (0x00-0xFF) and then wrote it, while the `set` command rejects a discrete value not in the monitor's advertised supported set. A profile value the monitor does not support was therefore attempted (and typically reported as a hardware failure) by apply-profile but pre-rejected by set. - Add CliSettingValidation.IsDiscreteValueSupported as the single source of the supported-set membership rule; SetCommandExecutor.TryResolveDiscrete now uses it (behaviour unchanged). - MainViewModel.TryRestoreWithOutcomeAsync takes the advertised set (sourced via the catalog's VCP code) and, for color-temperature, rejects an unsupported value as OutOfRange *before* any hardware write — matching `set`. Behaviour change (apply-profile only): a color-temperature value outside the monitor's advertised set is now reported as OutOfRange (exit 2) and skipped, instead of being attempted and reported as a hardware failure (exit 5). Monitors that advertise no set are unaffected (byte-range guard still applies). Adds CliSettingValidation and TryRestoreWithOutcomeAsync tests. Ipc unit tests: 124 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al coverage Remove constant-equality / duplicate / mislabeled tests and consolidate over-spread cases across the PowerDisplay CLI test projects. No production code touched; every removed branch stays covered by a kept test. Deleted (constant-equality / duplicate / framework-only): - ExitCodeMatrixTests.cs (asserted CliExitCodes constants == literals; the real code->exit mapping is covered by RoundTripTests.ForErrorCode_*) - CliSettingCatalogTests.Catalog_MapsDiscreteSettingsToTheirVcpCodes (restated 0x14/0x60/0xD6; covered behaviorally by the projector filter test) - MonitorDtoProjectorTests.BuildGetResult_NoSelector_UnknownSetting_ ErrorContainsOriginalCasing (mislabeled, subset of SettingFilterIsCaseInsensitive) - ProfileDtoProjectorTests null-guard duplicate (..._NotFoundSignal) - ResourcesTests.SafeFormat_ValidTemplate_Substitutes (exercises string.Format) Consolidated (functional, folded into a representative case): - IpcDispatchTests: 5 provider-unavailable variants -> list; Success_list/get -> Success_set; two HardwareFailure exit-code values -> existing data-driven pair - ProfileDtoProjectorTests: 5 per-status field tests -> ChangeRowsCarryAllFieldsVerbatim (now covers Value/Display/Error pass-through for applied + hardware-failure rows) - RoundTripTests: drop GetRequest source-gen round-trip (subset of the inherited-selector-fields test); CapabilitiesRequest round-trip kept (only coverage of CapabilitiesRequest.MonitorId source-gen serialization) Cli 47->36, Ipc 124->116, Contracts 14->13; all suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second cleanup pass over PowerDisplay.Cli.UnitTests, following the same constant-equality / over-spread standard as the prior trim. No production code touched; every removed branch stays covered by a kept test. Deleted (constant-equality): - ResourcesTests.KnownKey_ResolvesToNeutralEnglish (asserted resx strings == English literals; brittle to harmless rewording. The string.Format path it exercised is already covered by the SafeFormat_* no-crash tests.) Consolidated (functional, folded into a representative case): - IpcDispatchTests: the two IsError-discriminator routing tests folded into the behavioral exit-code tests they were a subset of (stderr==0 into the apply-profile OutOfRange test; stdout==0 into the error-response test). Dropped the inline Assert.IsTrue/IsFalse(.IsError) DTO-constant assertions. - SetCommandInputsTests: None/OnlyBrightness/BrightnessAndContrast 1-liners merged into CountSelectedSettings_CountsAcrossThresholds; AllSeven kept (it carries the zero-valued-int boxing edge + all-fields-wired coverage). Cli 36->31; all 31 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…C test fakes - Delete doc/devdocs/modules/powerdisplay/cli.md and doc/devdocs/cli-conventions.md; CLI usage is discoverable via --help and the two only cross-referenced each other. - PowerDisplay.Cli.UnitTests: remove dead StringWriter/IDisposable from IpcDispatchTests CaptureOutput (never written, never disposed); discard the unused read result in CliPipeClientTests. - PowerDisplay.Ipc.UnitTests: consolidate three duplicated IMonitorManager stubs (NoOpManager/FailingManager duplicated across two files, plus CliRequestHandlerTests' FakeManager whose failure path was never exercised) into shared NoOpManager.cs / FailingManager.cs; drop the now-unused usings. - PowerDisplay.Contracts/CliError.cs: correct the Message/Hint doc comments to reference MessageId (not Code), matching how CliErrorLocalizer keys the localized text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cross monitors - -n now accepts a comma-separated list of 1-based monitor indices (e.g. -n 1,2,3) for the write commands (set/up/down), applying the setting to each monitor. Duplicates collapse preserving first-seen order; an empty/non-integer entry is a single ARGUMENT_ERROR. - Best-effort per-monitor dispatch: each monitor renders its own result/error and the exit code is the worst outcome (UNSUPPORTED_FEATURE does not count, mirroring apply-profile); a PROVIDER_UNAVAILABLE result aborts the remaining targets. - get/capabilities stay single-monitor and reject a comma-separated batch with ARGUMENT_ERROR. - The overall deadline scales with the target count (base 5s + 3s per additional monitor); a monitor id still wins and collapses to a single target. - Contract unchanged: the CLI loops single-monitor IPC requests, so the app-side executors and DTOs are untouched. - Adds BatchMonitorTests covering parsing/dedup/invalid input, target counting and timeout scaling, the worst-exit aggregation, dispatch routing/abort, and the read-command single-monitor guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cli-conventions.md is a repo-wide CLI-conventions guide added on main by the Image Resizer CLI PR (#44287); it was not introduced by this branch. An earlier cleanup commit removed it out of scope, so restore it to the origin/main version. This branch now leaves the shared doc untouched (its diff vs main is empty), and the PowerDisplay-specific reference to the removed modules/powerdisplay/cli.md is not reintroduced. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…orcing Address review feedback on the PowerDisplay CLI: - CliPipeServer: keep the well-known, session-scoped pipe name continuously owned. Only the first instance uses FirstPipeInstance; each replacement instance is created before the just-served one is disposed, so a local process can no longer win the between-request gap to deny service or spoof CLI traffic. Extract an internal CreateServerStream seam and add unit tests (first-instance, overlap, squatter-detection). - PowerDisplay.Cli.csproj: set InvariantGlobalization=false explicitly so satellite-resource localization cannot be silently disabled by a future SDK-default change or an AOT size-reduction tweak. - Drop unrelated changes for PR atomicity: .superpowers/ from .gitignore and the unused "esac" spell-check token. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CliRequestHandler.MakeError: drop the always-null `hint` parameter and its `Hint = hint` initializer (none of the 5 call sites pass a hint). - Cli.UnitTests: consolidate the two per-file ICliOutput test doubles (RecordingOutput, CaptureOutput) into one shared RecordingCliOutput, removing duplicated interface-implementation boilerplate. No behavior change. PowerDisplay.Cli.UnitTests (82) and PowerDisplay.Ipc.UnitTests (137) pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… refactor
apply-profile (CLI/IPC) no longer captures per-setting outcomes. It reuses the
shared fire-and-forget ApplyProfileAsync path and always exits 0 once the profile
exists; a missing profile still returns ARGUMENT_ERROR (exit 7).
Removed the outcome-capturing machinery:
- MainViewModel.TryRestoreWithOutcomeAsync + ApplyProfileWithOutcomesInternalAsync
(replaced by ApplyProfileForCliAsync returning Task<bool>)
- ProfileApplyOutcome, CliProfileChange, CliProfileMonitorOutcome
- ProfileDtoProjector.BuildApplyProfileResult and the data-driven exit code
- CliApplyProfileResult slimmed to { IsError, Version, Command, Profile }
The IPC delegate is now Task<bool>, the dispatcher returns a static exit 0, and
the text renderer prints a single confirmation line. Tests updated accordingly.
This commit also snapshots the in-progress CLI command-handler refactor on this
branch: ICliCommandHandler + per-command handlers, CliResponse, the
Continuous/DiscreteVcpSetting split, and related SetCommandExecutor /
CliSettingCatalog / CliVcpSetting / MonitorDtoProjector changes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d self-heal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…allback) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eness helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y id Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6 tasks
Contributor
Author
Cross-reference: profile-id also proposed standalone as #49175The profile-id portion of this branch is also proposed as a standalone PR against
Merge relationship: this branch effectively stacks on top of #49175. If #49175 merges first, this PR will be rebased onto |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary of the Pull Request
Adds a headless command-line interface for PowerDisplay —
PowerToys.PowerDisplay.Cli.exe, shipped alongsidePowerToys.PowerDisplay.exeinWinUI3Apps\, and gives every saved profile a stable, auto-incrementing integer id so the whole app (CLI, GUI, LightSwitch) addresses profiles by id instead of name.The CLI is a thin client: it sends each request over a per-session named pipe to the running
PowerToys.PowerDisplay.exe, which owns the DDC/CI and WMI controllers and replies. Any monitor the GUI can adjust becomes scriptable from a terminal — the PowerDisplay module just has to be running (otherwise the CLI exitsPROVIDER_UNAVAILABLE, 10).Six commands —
list,get,set,capabilities,profiles,apply-profile <id>— the last two reach the GUI's saved-profile feature so a scheduled task or scene script can apply a saved monitor configuration by its stable id. Output is human-readable text (success → stdout, warnings/errors → stderr); JSON is an internal pipe-transport detail, not a user-facing output format.PR Checklist
PowerToys.PowerDisplay.Cli.exe/.dlladded explicitly (signing is not glob-based); localized.resources.dllfollow the repo convention of not being individually signedexe/dllship via the wholesaleWinUI3Applications.wxsharvest (the GUI binaries ride the same harvest and aren't listed explicitly either); the localized satellite DLLs are registered inResources.wxs(modeled on the WinUI3AppsCmdPalExtPowerToyscomponent)*UnitTests.dllare auto-discovered by the**\*UnitTest*.dllVSTest glob, and every CLI/test project is inPowerToys.slnx(no*UnitTestsproject is listed explicitly)ESRPSigning_core.json; the release pipeline'sanalyzeTargetGlobcovers allexe/dllDetailed Description of the Pull Request / Additional comments
Architecture
The CLI performs no hardware I/O. It serializes each request to a single
\n-delimited JSON line and sends it over the session-scoped pipePowerDisplay_Cli_Session_<id>; the app's pipe server dispatches it onto the UI thread (the same path the GUI uses), does the DDC/CI or GDI work, and returns one JSON response — one connection per request/response. The app must be running and past monitor discovery; if the pipe can't be reached the CLI returnsPROVIDER_UNAVAILABLE(10). The contract types live in a standalonePowerDisplay.Contractslibrary shared by both ends. The app-side pipe server keeps the well-known, session-scoped name continuously owned (only the first instance usesFirstPipeInstance; the replacement instance is created before the served one is disposed) so a local process cannot win a between-request gap to deny service or spoof CLI traffic; covered by unit tests.Commands
list— discover monitors (number, stable id, name, transport DDC/CI or WMI).get— read one or all settings for one or all monitors; a value is reported only when the monitor supports it and discovery actually read it, so a default/stale field is never shown as a live reading. Discrete values are shown by name, honoring the user's custom VCP value names from PowerDisplay settings.set— apply exactly one setting per call: brightness/contrast/volume (0–100%), color-temperature/input-source/power-state (hex VCP value, e.g.0x05), orientation (0/90/180/270°). The three discrete settings accept only a hex value — friendly names are deliberately rejected, because the generic VCP name table can disagree with a specific monitor's mapping; usecapabilities --setting <name>to discover the valid values. Display-blanking power states (Standby/Suspend/Off) require--confirm-power-off.capabilities— print the parsed VCP capability set advertised by the monitor;--setting <color-temperature|input-source|power-state>narrows the output to that one discrete code (custom value names applied).profiles— list the saved profiles fromprofiles.json(the same file the GUI writes), including each profile's stable id column so it can be fed toapply-profile.apply-profile <id>— apply a saved profile (addressed by its stable id) — per-monitor brightness/contrast/volume/color-temperature, mirroring the GUI's apply path (match by monitor id, apply only supported values, skip not-connected/hidden monitors). Reports a per-monitor/per-setting outcome with a worst-case exit code.Profile stable IDs
Profiles used to be keyed by name, which made duplicate names ambiguous and renames lossy for anything that referenced a profile (the CLI, LightSwitch). This PR introduces a stable identity:
PowerDisplayProfile.Id(JSONid,int,0= unassigned) plus a collection-level monotonicPowerDisplayProfiles.NextId(JSONnextId) that only ever increases and is never reused.SetProfileis id-keyed (a new profile withId == 0is assigned the next id; an existing id replaces in place, so a rename keeps the same id). Duplicate profile names are now allowed — the id disambiguates, and the UI showsName (#id)in lists and pickers.EnsureIds()back-fills an id for every legacy profile still missing one and self-healsnextIdpast the highest id in use; this runs before the existing legacy-monitor-id side-file migration's early-return so it happens even with no monitors connected. Aprofiles.jsonwritten before this feature loads and is upgraded in place with no data loss.darkModeProfileId/lightModeProfileId) and the name. A one-shot startup migration converts existing name-only mappings to id+name. Resolution is id-first and a stale/deleted id does not silently fall back to the name (mirrored on both the app-sideLightSwitchProfileResolverand the Settings ViewModel), so a deleted profile can't be replaced by a same-named different one.ApplyProfileByIdAsync(int), the CLI applies by id (apply-profile <id>,profilesshows the id column), and the IPC value carried from Settings → module DLL → app is the id string (parsed withint.TryParse).id/nextIdand the CLI-contract id fields are registered in theSystem.Text.Jsonsource-generation contexts (ProfileSerializationContext,ContractsJsonContext).Design highlights
0–10, includingPROVIDER_UNAVAILABLE= 10 when the app isn't reachable) so scripts branch on codes, not strings; exactly one error envelope per invocation, errors to stderr.--timeoutwith a hard-deadline race so an unresponsive DDC monitor cannot hang the process; Ctrl+C cancels.dotnet publish). Note that, likePowerToys.PowerDisplay.exe, the CLI is not in the pipeline'scsProjectsToPublishlist, so the shipped binary is a normal managed apphost (.exe+ IL.dll) andPublishAotonly takes effect underdotnet publish; envelopes useSystem.Text.Jsonsource generation with stable camelCase keys.--helpis the authoritative reference, following the repo'scli-conventions.md. GUI-only concerns (flyout UX, hotkeys, identify overlay, linked-brightness UX state) are intentionally out of scope.Localization
The human-readable, CLI-owned prose (standalone labels, and the parse / timeout / provider-unavailable errors) is routed through
Properties/Resources.resxand ships as .NET satellite assemblies (registered inResources.wxs);InvariantGlobalizationis disabled explicitly in the csproj (rather than relying on the SDK default) so they load. The machine contract (JSON keys, error/status/exit codes, VCP names, command/flag names) stays invariant. A defensive formatter falls back to the neutral English template if a translation breaks a placeholder, so a bad translation degrades to English rather than crashing or masking the real error. Still English-only — hence the box is left unchecked — are the command/option descriptions (--helptext), the alignment-sensitive table headers/field labels, and the app-side validation messages returned over IPC.Shared-library (
PowerDisplay.Lib) changesMonitorManagerintoPowerDisplay.Lib/Servicesbehind a smallIMonitorManagerseam (the sevenSet*Asyncwrite methods) so the IPC executor is unit-testable against a fake.MonitorReadFlagsso callers can distinguish a real read from a default value; discovery now reads live orientation, which also fixes the GUI showing the correct orientation on initial load. TheMonitorReadFlagsadditions are purely additive. One behavioral change is shared with the GUI:InitializeContrast/InitializeVolumenow skip an invalid VCP range (max <= min) instead of recording contrast/volume = 0% with a bogus max, aligning them with the pre-existing brightness guard; this only affects monitors that report a malformed range.Validation Steps Performed
x64/Debug): hex-only discrete resolution, thecapabilities --settingfilter + validation, custom-name projection, monitor/profile DTO projection, the set-executor validation/exit-code matrix, the dispatcher's provider-unavailable / timeout / schema-mismatch paths, and — for profile ids — id-keyedSetProfile/EnsureIds/NextIdself-heal, the LightSwitch id-first resolver + name→id migration, settingsClone()id round-trip, and the CLI/contract apply-profile-by-id round-trips.list;get/capabilitiesincluding the--settingfilter; a hexset(success, exit 0); a friendly-namesetand a bad-hexset(both rejected → exit 3);capabilities --setting brightness(rejected → exit 7); and a temporary custom VCP value mapping rendering its custom name inget/capabilities. ConfirmedPROVIDER_UNAVAILABLE(10) when the module isn't running.