Skip to content

[PowerDisplay] Add headless CLI and stable profile IDs (PowerToys.PowerDisplay.Cli)#48632

Draft
moooyo wants to merge 56 commits into
mainfrom
yuleng/pd/cli/1
Draft

[PowerDisplay] Add headless CLI and stable profile IDs (PowerToys.PowerDisplay.Cli)#48632
moooyo wants to merge 56 commits into
mainfrom
yuleng/pd/cli/1

Conversation

@moooyo

@moooyo moooyo commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary of the Pull Request

Adds a headless command-line interface for PowerDisplay — PowerToys.PowerDisplay.Cli.exe, shipped alongside PowerToys.PowerDisplay.exe in WinUI3Apps\, 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 exits PROVIDER_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

  • Closes: Add PowerDisplay CLI #48713
  • Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected
  • Tests: Added/updated and all pass — 382 (Lib 146, IPC 136, CLI 85, Contracts 15)
  • Localization: All end-user-facing strings can be localized
  • New binaries: Added on the required places
    • JSON for signingPowerToys.PowerDisplay.Cli.exe/.dll added explicitly (signing is not glob-based); localized .resources.dll follow the repo convention of not being individually signed
    • WXS for installer — the exe/dll ship via the wholesale WinUI3Applications.wxs harvest (the GUI binaries ride the same harvest and aren't listed explicitly either); the localized satellite DLLs are registered in Resources.wxs (modeled on the WinUI3Apps CmdPalExtPowerToys component)
    • YML for CI pipeline — the *UnitTests.dll are auto-discovered by the **\*UnitTest*.dll VSTest glob, and every CLI/test project is in PowerToys.slnx (no *UnitTests project is listed explicitly)
    • YML for signed pipeline — signing is driven by ESRPSigning_core.json; the release pipeline's analyzeTargetGlob covers all exe/dll
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed 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 pipe PowerDisplay_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 returns PROVIDER_UNAVAILABLE (10). The contract types live in a standalone PowerDisplay.Contracts library shared by both ends. The app-side pipe server keeps the well-known, session-scoped name continuously owned (only the first instance uses FirstPipeInstance; 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; use capabilities --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 from profiles.json (the same file the GUI writes), including each profile's stable id column so it can be fed to apply-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:

  • Model: PowerDisplayProfile.Id (JSON id, int, 0 = unassigned) plus a collection-level monotonic PowerDisplayProfiles.NextId (JSON nextId) that only ever increases and is never reused. SetProfile is id-keyed (a new profile with Id == 0 is 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 shows Name (#id) in lists and pickers.
  • Migration (idempotent, on startup): EnsureIds() back-fills an id for every legacy profile still missing one and self-heals nextId past 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. A profiles.json written before this feature loads and is upgraded in place with no data loss.
  • LightSwitch: the light/dark theme→profile mapping now stores both the canonical id (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-side LightSwitchProfileResolver and the Settings ViewModel), so a deleted profile can't be replaced by a same-named different one.
  • End-to-end by id: the Settings UI applies/edits/deletes profiles by id (edit reuses the id, so renames are in-place), the app's apply path is ApplyProfileByIdAsync(int), the CLI applies by id (apply-profile <id>, profiles shows the id column), and the IPC value carried from Settings → module DLL → app is the id string (parsed with int.TryParse).
  • AOT-safe serialization: the new id/nextId and the CLI-contract id fields are registered in the System.Text.Json source-generation contexts (ProfileSerializationContext, ContractsJsonContext).

Design highlights

  • Scriptable exit-code contract (0–10, including PROVIDER_UNAVAILABLE = 10 when the app isn't reachable) so scripts branch on codes, not strings; exactly one error envelope per invocation, errors to stderr.
  • --timeout with a hard-deadline race so an unresponsive DDC monitor cannot hang the process; Ctrl+C cancels.
  • Excludes monitors the user hid in PowerDisplay settings.
  • The CLI is kept Native AOT / trim-clean (0 analyzer warnings, verified via a local dotnet publish). Note that, like PowerToys.PowerDisplay.exe, the CLI is not in the pipeline's csProjectsToPublish list, so the shipped binary is a normal managed apphost (.exe + IL .dll) and PublishAot only takes effect under dotnet publish; envelopes use System.Text.Json source generation with stable camelCase keys.
  • --help is the authoritative reference, following the repo's cli-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.resx and ships as .NET satellite assemblies (registered in Resources.wxs); InvariantGlobalization is 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 (--help text), the alignment-sensitive table headers/field labels, and the app-side validation messages returned over IPC.

Shared-library (PowerDisplay.Lib) changes

  • Moved MonitorManager into PowerDisplay.Lib/Services behind a small IMonitorManager seam (the seven Set*Async write methods) so the IPC executor is unit-testable against a fake.
  • Added MonitorReadFlags so 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. The MonitorReadFlags additions are purely additive. One behavioral change is shared with the GUI: InitializeContrast/InitializeVolume now 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

  • 382 unit tests across four projects (Lib 146, IPC 136, CLI 85, Contracts 15) — all passing (VS MSBuild, x64/Debug): hex-only discrete resolution, the capabilities --setting filter + 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-keyed SetProfile/EnsureIds/NextId self-heal, the LightSwitch id-first resolver + name→id migration, settings Clone() id round-trip, and the CLI/contract apply-profile-by-id round-trips.
  • Clean Native AOT build (0 warnings / 0 errors, including the trim/AOT analyzer).
  • End-to-end on real hardware (two HP E27q G4 over DDC/CI, app running): list; get / capabilities including the --setting filter; a hex set (success, exit 0); a friendly-name set and a bad-hex set (both rejected → exit 3); capabilities --setting brightness (rejected → exit 7); and a temporary custom VCP value mapping rendering its custom name in get/capabilities. Confirmed PROVIDER_UNAVAILABLE (10) when the module isn't running.
  • Rebuilt the PowerDisplay GUI after the shared-library move to confirm no regression.

Comment thread doc/devdocs/modules/powerdisplay/cli.md Fixed
Comment thread doc/devdocs/modules/powerdisplay/cli.md Fixed
Comment thread doc/devdocs/modules/powerdisplay/cli.md Fixed
Comment thread src/modules/powerdisplay/PowerDisplay.Cli/Program.cs Fixed
Comment thread src/modules/powerdisplay/PowerDisplay.Cli/Program.cs Fixed
@github-actions

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>
@moooyo moooyo changed the title [WIP] [PowerDisplay] Add headless CLI (PowerToys.PowerDisplay.Cli) [PowerDisplay] Add headless CLI (PowerToys.PowerDisplay.Cli) Jun 22, 2026
@moooyo moooyo marked this pull request as ready for review June 22, 2026 02:37
@moooyo moooyo requested a review from a team as a code owner June 22, 2026 02:37
@github-actions github-actions Bot added Product-Command Palette Refers to the Command Palette utility Product-PowerDisplay labels Jun 22, 2026
@moooyo moooyo removed the Product-Command Palette Refers to the Command Palette utility label Jun 24, 2026
@github-actions github-actions Bot added the Area-Localization issues regarding to Localization the application label Jun 24, 2026
@moooyo moooyo marked this pull request as draft June 24, 2026 05:04
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.
@moooyo moooyo force-pushed the yuleng/pd/cli/1 branch from c2e0dbb to d223a08 Compare June 25, 2026 06:59
Yu Leng and others added 17 commits June 25, 2026 16:15
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>
Yu Leng (from Dev Box) and others added 27 commits July 6, 2026 11:48
…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>
@moooyo moooyo changed the title [PowerDisplay] Add headless CLI (PowerToys.PowerDisplay.Cli) [PowerDisplay] Add headless CLI and stable profile IDs (PowerToys.PowerDisplay.Cli) Jul 7, 2026
@moooyo

moooyo commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Cross-reference: profile-id also proposed standalone as #49175

The profile-id portion of this branch is also proposed as a standalone PR against main: #49175[PowerDisplay] Add stable profile IDs.

Merge relationship: this branch effectively stacks on top of #49175. If #49175 merges first, this PR will be rebased onto main so it only adds the CLI layer; if this PR merges first, #49175 becomes redundant. Reviewers who want to look at the profile-id changes in isolation can use #49175.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Localization issues regarding to Localization the application Product-PowerDisplay

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add PowerDisplay CLI

2 participants