# DesktopManager - C# Library and PowerShell Module
DesktopManager is a Windows desktop automation toolkit available as a .NET library, PowerShell module, CLI, MCP server, and tray app.
The .NET library also exposes `PortableDesktopService` for current-user wallpaper, display discovery,
and session diagnostics on Windows, macOS, GNOME, and KDE Plasma. Each adapter reports its capabilities;
the window automation, tray app, CLI, and existing PowerShell commands retain their Windows scope.
See [Portable desktop integration](Docs/Portable.Desktop.md) for usage and platform limits.
The .NET 8 and .NET 10 library targets support trimming and Native AOT, including Core Audio,
wallpaper and monitor enumeration, Explorer folder capture, virtual desktops and UI Automation.
The .NET Framework and .NET Standard targets retain their legacy interop paths.
See [Native AOT qualification](Sources/DesktopManager.AotSmoke/README.md) for the contributor smoke consumer.
[](https://www.nuget.org/packages/DesktopManager)
[](https://www.nuget.org/packages/DesktopManager)
You can also download it from PowerShell Gallery
[](https://www.powershellgallery.com/packages/DesktopManager)
[](https://www.powershellgallery.com/packages/DesktopManager)
[](https://www.powershellgallery.com/packages/DesktopManager)
[](https://www.powershellgallery.com/packages/DesktopManager)
[](https://github.com/EvotecIT/DesktopManager)
[](https://github.com/EvotecIT/DesktopManager)
[](https://github.com/EvotecIT/DesktopManager)
If you would like to contact me you can do so via Twitter or LinkedIn.
[](https://twitter.com/PrzemyslawKlys)
[](https://evotec.xyz/hub)
[](https://www.linkedin.com/in/pklys)
[](https://evo.yt/discord)
[](https://www.threads.net/@przemyslaw.klys)
## What it's all about
<img height="256" align="right" src="Assets/Images/DesktopManager.jpg">
DesktopManager provides one shared Windows automation core through five surfaces:
- **C# library** for .NET applications and services
- **PowerShell module** for scripts and operator workflows
- **CLI executable** for terminal and JSON automation
- **MCP server** for inspect-first agent automation
- **Windows tray app** for daily hotkeys and monitor actions
It has following features:
- Get information about monitors
- Get information about display devices
- Get information about wallpapers
- Set wallpapers
- Get/Set desktop background color
- Get/Set monitor position
- Get/Set window position
- Get/Set window state (minimize, maximize, restore)
- Capture desktop screenshots from all monitors, a single monitor or a custom region
- Manage monitor brightness
- Start/Stop/Advance wallpaper slideshows
- Track wallpaper history
- Adjust monitor resolution and orientation
- Move monitors around the virtual desktop
- Save and restore window layouts
- Snap or move windows between monitors
- Subscribe to resolution, orientation or display changes
- Keep inactive windows awake using periodic input
- Manage keep-alive sessions for windows
- Save and restore cohesive workstation profiles across displays, audio, personalization, and taskbars
- Enumerate Core Audio render/capture endpoints, change volume and mute, set default roles, and observe endpoint changes
- Inspect power and interactive-session state; lock, sign out, sleep, hibernate, or hold a bounded keep-awake lease
- Capture and restore personalization snapshots and configure taskbar visibility, position, and auto-hide
- Enumerate, observe, and explicitly enable or disable Wi-Fi, Bluetooth, mobile broadband, and FM radios through the supported Windows radio API
- List saved Windows Wi-Fi profiles and connect an exact saved profile without scanning nearby networks or exposing credentials
- Inspect a window's virtual desktop and move it to a known desktop through the public Windows virtual-desktop API
- Inspect and manage Plug and Play devices and Driver Store packages through documented native Windows APIs
### Wallpaper consumers
Applications such as [PowerBGInfo](https://github.com/EvotecIT/PowerBGInfo) can read monitor geometry,
display scale and the signed-in session user directly from the library:
```csharp
using DesktopManager;
var monitors = new Monitors();
foreach (var display in monitors.GetMonitorSnapshots()) {
Console.WriteLine($"{display.Index}: {display.DeviceName}, " +
$"{display.Width} x {display.Height}, scale {display.Scale}");
}
var user = new DesktopSessionService().GetSessionUser();
Console.WriteLine(user?.AccountName ?? "Unknown");
```
Snapshots contain physical pixel bounds and per-monitor DPI even when the caller is not DPI aware.
The calling thread's DPI context is restored. Indexes describe the current display topology; read
them again after docking or reconnecting. Missing wallpaper device paths on session hosts are retained.
`Monitors.SetLogonWallpaperWithResult(imagePath)` sets the current account's lock screen through
Windows Runtime on .NET Framework, modern .NET and netstandard consumers. The result reports success,
the mechanism used and any failure. Machine policy fallback requires an explicit
`allowPolicyFallback: true` and elevation. The existing `SetLogonWallpaper` method keeps its automatic
policy fallback. See [Wallpaper consumer APIs](Docs/Wallpaper.Consumers.md) for examples, OS requirements,
policy limits and session deployment behavior.
### Desktop state and controls
The workstation profile is the high-level entrypoint when you want to move between repeatable setups. It captures connected monitor modes and placement, monitor wallpaper and supported brightness/HDR state, active audio endpoint state and default roles, personalization, and taskbars. Applying a profile matches monitors by stable identity, stages display changes together, and rolls back the selected sections when a required step fails. Machine-wide personalization policies are captured but only restored when explicitly requested.
```powershell
Save-DesktopWorkstationProfile -Name 'office'
Get-DesktopAudioEndpoint -DataFlow Render -ActiveOnly
Get-DesktopPowerStatus
Get-DesktopSession
Get-DesktopRadio
Get-DesktopWifiProfile
Connect-DesktopWifiProfile -Name 'Corporate WiFi'
Get-DesktopDevice -Present -Problem
Get-DesktopDriverPackage
Restore-DesktopWorkstationProfile -Name 'office' -Confirm
```
The same capabilities are available from the CLI:
```text
desktopmanager workstation save --name office
desktopmanager audio list --flow render --active
desktopmanager system power
desktopmanager system session
desktopmanager radio list
desktopmanager wifi profiles
desktopmanager wifi connect --profile "Corporate WiFi"
desktopmanager device list --present --problem
desktopmanager driver list
desktopmanager workstation apply --name office
```
Radio control has two deliberately separate APIs. `RadioService` and the normal `radio list` / `radio set` commands use the supported `Windows.Devices.Radios` contract for individual radios. Global airplane mode uses an undocumented Windows shell COM contract, so it lives under `ExperimentalAirplaneModeService` and requires `--experimental` in the CLI, `-Experimental` in PowerShell, or `--allow-experimental` in MCP.
Wi-Fi profile connection is separate from radio control. `WifiProfileService`, `Get-DesktopWifiProfile`, and `desktopmanager wifi profiles` enumerate only profiles already stored by Windows. Connecting an exact profile uses the Native Wi-Fi profile API and ACM completion notifications; it does not scan nearby networks, return BSSIDs, expose profile XML or credentials, or query location-sensitive current-connection details. Windows still requires location consent for nearby-network scanning and related Wi-Fi information APIs. See [Changes to API behavior for Wi-Fi access and location](https://learn.microsoft.com/windows/win32/nativewifi/wi-fi-access-location-changes).
`VirtualDesktopService` exposes only the public Windows operations: check whether a top-level window is on the current desktop, get its desktop ID, and move it to a known desktop ID. Desktop enumeration, creation, renaming, switching, and removal are intentionally not presented as supported APIs.
Additional operator-focused docs for the newer CLI and MCP surfaces:
- [Docs/DesktopManager.Cli.md](Docs/DesktopManager.Cli.md)
- [Docs/DesktopManager.Mcp.md](Docs/DesktopManager.Mcp.md)
- [Docs/DesktopManager.Architecture.md](Docs/DesktopManager.Architecture.md)
- [Docs/DesktopManager.DesktopState.md](Docs/DesktopManager.DesktopState.md)
- [Docs/DesktopManager.DeviceManagement.md](Docs/DesktopManager.DeviceManagement.md)
- [Docs/DesktopManager.AutomationBasics.md](Docs/DesktopManager.AutomationBasics.md)
- [Docs/DesktopManager.Plugin.md](Docs/DesktopManager.Plugin.md)
- [.agents/skills/desktopmanager-operator/SKILL.md](.agents/skills/desktopmanager-operator/SKILL.md)
- [.agents/skills/desktopmanager-build/SKILL.md](.agents/skills/desktopmanager-build/SKILL.md)
## Which Surface Should You Use?
| Surface | Best for | Typical entrypoint |
| ------- | -------- | ------------------ |
| C# library | Embedding DesktopManager into your own .NET app or service | `DesktopManager.dll` |
| PowerShell module | Scripts, operators, admin workflows, repeatable task automation | `Install-Module DesktopManager` |
| CLI executable | Manual use, shell automation, JSON output, and MCP hosting | `desktopmanager.exe` |
| MCP server | Agent-driven automation over stdio with an inspect-first safety model | `desktopmanager mcp serve` |
| Windows tray app | Daily hotkeys, monitor bindings, HDR controls, tray runtime, and per-user startup | `DesktopManager.App.exe` |
## How It Fits Together
DesktopManager now has one shared core and several operator-facing surfaces on top of it.
```mermaid
flowchart LR
A["Your .NET app"] --> B["DesktopManager library"]
C["PowerShell module"] --> B
D["desktopmanager.exe CLI"] --> B
E["MCP server"] --> D
J["DesktopManager.App tray app"] --> B
B --> F["Windows desktop APIs"]
F --> G["Monitors / wallpapers / brightness"]
F --> H["Windows / controls / input"]
F --> I["Layouts / snapshots / targets"]
```
If you want the fuller component and request-flow diagrams, see [Docs/DesktopManager.Architecture.md](Docs/DesktopManager.Architecture.md).
The practical way to think about this:
- use the library when DesktopManager is part of your own application
- use PowerShell when you want scriptable admin/operator automation
- use the CLI when you want a human-friendly or JSON-friendly command surface
- use MCP when an agent should inspect first and mutate only through an explicit server session
## Common Entry Points
| Goal | Recommended entrypoint |
| ---- | ---------------------- |
| Add DesktopManager features to your own code | Reference the `DesktopManager` NuGet package |
| Run manual desktop operations from a terminal | `desktopmanager.exe` |
| Run DesktopManager every day from the tray | `DesktopManager.App.exe` or the DesktopManager MSI |
| Use it from PowerShell scripts | `DesktopManager` PowerShell module |
| Connect an agent/tooling layer | `desktopmanager mcp serve` |
| Understand the overall wiring | [Docs/DesktopManager.Architecture.md](Docs/DesktopManager.Architecture.md) |
## Request Flow at a Glance
```mermaid
flowchart LR
A["User, script, or agent"] --> B{"Surface"}
B -->|".NET"| C["DesktopManager library"]
B -->|"PowerShell"| D["Cmdlets"]
B -->|"CLI"| E["desktopmanager.exe"]
B -->|"MCP"| F["desktopmanager mcp serve"]
B -->|"Tray app"| J["DesktopManager.App.exe"]
D --> C
E --> G["DesktopOperations"]
F --> G
J --> C
G --> C
C --> H["Win32, UI Automation, monitor, screenshot, and input services"]
```
The important part is that CLI, MCP, and PowerShell are meant to stay aligned by reusing the same shared C# behavior rather than inventing separate desktop logic in each surface.
### Available PowerShell Cmdlets
| Cmdlet | Description |
| ------ | ----------- |
| Get-DesktopMonitor | Retrieve monitor information with filtering options |
| Get-DesktopWallpaper | Get current wallpaper path for monitors |
| Set-DesktopWallpaper | Apply wallpaper from path, URL or stream |
| Get-DesktopWallpaperHistory | List stored wallpaper history entries |
| Set-DesktopWallpaperHistory | Update or clear wallpaper history file |
| Get-DesktopSlideshow | Inspect slideshow images, state, options and tick interval |
| Set-DesktopSlideshowOptions | Update slideshow shuffle behavior and tick interval |
| Start-DesktopSlideshow | Begin wallpaper slideshow across monitors |
| Stop-DesktopSlideshow | Stop currently running slideshow |
| Step-DesktopSlideshow | Move slideshow forward or backward |
| Get-DesktopBackgroundColor | Read current desktop background color |
| Set-DesktopBackgroundColor | Change desktop background color |
| Get-DesktopBrightness | Read monitor brightness level |
| Set-DesktopBrightness | Set monitor brightness level |
| Set-DesktopPosition | Move a monitor by its top-left coordinates without changing resolution |
| Set-DesktopResolution | Change monitor resolution or orientation |
| Set-DefaultAudioDevice | Set the default audio playback device |
| Get-DesktopAudioEndpoint | List Core Audio render and capture endpoints with volume, mute, and default roles |
| Set-DesktopAudioEndpoint | Set endpoint volume, mute, or selected default roles |
| Register-DesktopAudioEvent | Subscribe to endpoint, state, property, and default-role changes |
| Save-DesktopWorkstationProfile | Capture displays, audio, personalization, and taskbars as one named profile |
| Get-DesktopWorkstationProfile | List or load named workstation profiles |
| Restore-DesktopWorkstationProfile | Apply a named workstation profile with matching and rollback options |
| Remove-DesktopWorkstationProfile | Delete a named workstation profile |
| Get-DesktopPowerStatus | Read AC and battery state |
| Get-DesktopSession | Read current interactive-session identity, connection, lock, and idle state |
| Register-DesktopSessionEvent | Subscribe to meaningful session changes |
| Lock-DesktopSession | Lock the current Windows session |
| Exit-DesktopSession | Sign out the current Windows session with confirmation support |
| Start-DesktopKeepAwake | Hold a bounded system/display keep-awake lease |
| Suspend-DesktopSystem | Request sleep or hibernation with confirmation support |
| Get-DesktopPersonalization | Capture current state or list/load saved personalization snapshots |
| Save-DesktopPersonalization | Save a named personalization snapshot |
| Set-DesktopPersonalization | Apply explicit personalization settings |
| Restore-DesktopPersonalization | Restore a named personalization snapshot |
| Remove-DesktopPersonalization | Delete a named personalization snapshot |
| Get-DesktopTaskbar | List taskbars with monitor mapping, visibility, edge, and bounds |
| Set-DesktopTaskbarAutoHide | Set the explicit taskbar auto-hide state |
| Get-DesktopRadio | List radios through the supported Windows radio API |
| Set-DesktopRadio | Explicitly enable or disable matching supported radios |
| Register-DesktopRadioEvent | Subscribe to supported radio state changes |
| Get-DesktopWifiProfile | List saved Windows Wi-Fi profiles without scanning nearby networks |
| Connect-DesktopWifiProfile | Connect an exact saved Windows Wi-Fi profile |
| Get-DesktopAirplaneMode | Read the experimental global airplane-mode state after explicit acknowledgement |
| Set-DesktopAirplaneMode | Apply and verify experimental global airplane mode after explicit acknowledgement |
| Get-DesktopVirtualDesktop | Inspect the supported virtual-desktop state for a top-level window |
| Move-DesktopWindowToVirtualDesktop | Move a top-level window to a known virtual-desktop ID |
| Get-LogonWallpaper | Get the lock screen wallpaper path |
| Set-LogonWallpaper | Set the lock screen wallpaper |
| Set-TaskbarPosition | Move or hide the taskbar |
| Invoke-DesktopMouseMove | Move the mouse cursor |
| Invoke-DesktopMouseClick | Simulate a mouse click |
| Invoke-DesktopMouseScroll | Scroll the mouse wheel |
| Invoke-DesktopScreenshot | Capture monitor or region screenshots |
| Get-DesktopWindow | Enumerate visible windows |
| Get-DesktopWindowGeometry | Return outer-window and client-area geometry for matched windows |
| Get-DesktopWindowControl | Enumerate Win32 or UIA window controls |
| Get-DesktopWindowControlDiagnostic | Explain Win32 vs UIA control discovery and actionability |
| Get-DesktopWindowProcessInfo | Return process details for matched windows |
| Get-DesktopWindowTarget | List saved window-relative targets or resolve one against live windows |
| Get-DesktopControlTarget | List saved control targets or resolve one against live windows |
| Get-DesktopHostedSessionDiagnostic | Read the latest hosted-session typing diagnostic artifact or a specific one |
| Invoke-DesktopControlClick | Click a matched control using shared control action routing |
| Invoke-DesktopWindowClick | Click a window-relative point or saved window target |
| Invoke-DesktopWindowDrag | Drag between window-relative points or saved targets |
| Invoke-DesktopWindowScroll | Scroll at a window-relative point or saved target |
| Send-DesktopControlKey | Send keys to a matched control without reimplementing key routing |
| Set-DesktopControlTarget | Save a reusable control selector profile |
| Set-DesktopControlText | Write text directly to a specific control |
| Set-DesktopWindow | Move, resize or control windows |
| Set-DesktopWindowSnap | Snap window to common positions |
| Set-DesktopWindowTarget | Save a reusable window-relative point target |
| Set-DesktopWindowText | Paste or type text into a window |
| Wait-DesktopWindow | Wait for a window to appear |
| Wait-DesktopWindowControl | Wait for a control to appear |
| Test-DesktopWindow | Verify that a window exists or matches the active window |
| Test-DesktopWindowControl | Verify that a control exists |
| Start-DesktopWindowKeepAlive | Send periodic input to keep a window awake |
| Stop-DesktopWindowKeepAlive | Stop sending keep-alive input |
| Get-DesktopWindowKeepAlive | List windows with active keep-alive |
| Save-DesktopWindowLayout | Save current window layout to file |
| Restore-DesktopWindowLayout | Restore saved window layout |
| Register-DesktopMonitorEvent | Subscribe to display configuration changes |
| Register-DesktopOrientationEvent | Subscribe to orientation changes |
| Register-DesktopResolutionEvent | Subscribe to resolution changes |
### Cmdlet to C# method map
The table below shows the most relevant API methods behind each PowerShell cmdlet.
| Cmdlet | Main C# methods |
| ------ | --------------- |
| Get-DesktopMonitor | `Monitors.GetMonitors` |
| Get-DesktopWallpaper | `Monitors.GetWallpaper` or `Monitor.GetWallpaper` |
| Set-DesktopWallpaper | `Monitors.SetWallpaper`, `Monitors.SetWallpaperFromUrl` |
| Get-DesktopWallpaperHistory | `WallpaperHistory.GetHistory` |
| Set-DesktopWallpaperHistory | `WallpaperHistory.SetHistory` |
| Get-DesktopSlideshow | `Monitors.GetWallpaperSlideshow` |
| Set-DesktopSlideshowOptions | `Monitors.SetWallpaperSlideshowOptions` |
| Start-DesktopSlideshow | `Monitors.StartWallpaperSlideshow` |
| Stop-DesktopSlideshow | `Monitors.StopWallpaperSlideshow` |
| Step-DesktopSlideshow | `Monitors.AdvanceWallpaperSlide` |
| Get-DesktopBackgroundColor | `Monitors.GetBackgroundColor` |
| Set-DesktopBackgroundColor | `Monitors.SetBackgroundColor` |
| Get-DesktopBrightness | `Monitors.GetMonitorBrightness` |
| Set-DesktopBrightness | `Monitors.SetMonitorBrightness` |
| Set-DesktopPosition | `Monitor.SetMonitorPosition` or `Monitors.SetMonitorPosition` |
| Set-DesktopResolution | `Monitors.SetMonitorResolution`, `Monitors.SetMonitorOrientation` |
| Set-DefaultAudioDevice | `AudioService.SetDefaultAudioDevice` |
| Get-LogonWallpaper | `Monitors.GetLogonWallpaper` |
| Set-LogonWallpaper | `Monitors.SetLogonWallpaper` |
| Set-TaskbarPosition | `TaskbarService.SetTaskbarPosition` / `SetTaskbarVisibility` |
| Invoke-DesktopMouseMove | `WindowManager.MoveMouse` |
| Invoke-DesktopMouseClick | `WindowManager.ClickMouse` |
| Invoke-DesktopMouseScroll | `WindowManager.ScrollMouse` |
| Invoke-DesktopScreenshot | `ScreenshotService.CaptureScreen`, `ScreenshotService.CaptureRegion` |
| Get-DesktopWindow | `WindowManager.GetWindows` / `GetWindowsForProcess` |
| Get-DesktopWindowGeometry | `DesktopAutomationService.GetWindowGeometry` |
| Get-DesktopWindowControl | `DesktopAutomationService.GetControls` |
| Get-DesktopWindowControlDiagnostic | `DesktopAutomationService.GetControlDiagnostics` / `GetControlTargetDiagnostics` |
| Get-DesktopWindowProcessInfo | `WindowManager.GetWindowProcessInfo` |
| Get-DesktopWindowTarget | `DesktopAutomationService.ListWindowTargets`, `GetWindowTarget`, `ResolveWindowTargets` |
| Get-DesktopControlTarget | `DesktopAutomationService.ListControlTargets`, `GetControlTarget`, `ResolveControlTargets` |
| Invoke-DesktopControlClick | `DesktopAutomationService.ClickControls` / `ClickControlTarget` |
| Invoke-DesktopWindowClick | `DesktopAutomationService.ClickWindowPoint` / `ClickWindowTarget` |
| Invoke-DesktopWindowDrag | `DesktopAutomationService.DragWindowPoints` / `DragWindowTargets` |
| Invoke-DesktopWindowScroll | `DesktopAutomationService.ScrollWindowPoint` / `ScrollWindowTarget` |
| Send-DesktopControlKey | `DesktopAutomationService.SendControlKeys` / `SendControlTargetKeys` |
| Set-DesktopControlTarget | `DesktopAutomationService.SaveControlTarget` |
| Set-DesktopControlText | `DesktopAutomationService.SetControlText` |
| Set-DesktopWindow | `WindowManager.SetWindowPosition`, `MoveWindowToMonitor`, etc. |
| Set-DesktopWindowSnap | `WindowManager.SnapWindow` |
| Set-DesktopWindowTarget | `DesktopAutomationService.SaveWindowTarget` |
| Set-DesktopWindowText | `WindowManager.TypeText`, `WindowManager.PasteText`, `WindowInputService` |
| Wait-DesktopWindow | `DesktopAutomationService.WaitForWindows` |
| Wait-DesktopWindowControl | `DesktopAutomationService.WaitForControls` |
| Test-DesktopWindow | `DesktopAutomationService.WindowExists` / `ActiveWindowMatches` |
| Test-DesktopWindowControl | `DesktopAutomationService.ControlExists` |
| Start-DesktopWindowKeepAlive | `WindowKeepAlive.Start` |
| Stop-DesktopWindowKeepAlive | `WindowKeepAlive.Stop` or `StopAll` |
| Get-DesktopWindowKeepAlive | `WindowKeepAlive.ActiveHandles` |
| Save-DesktopWindowLayout | `WindowManager.SaveLayout` |
| Restore-DesktopWindowLayout | `WindowManager.LoadLayout` |
| Register-DesktopMonitorEvent | `MonitorWatcher.DisplaySettingsChanged` |
| Register-DesktopOrientationEvent | `MonitorWatcher.OrientationChanged` |
| Register-DesktopResolutionEvent | `MonitorWatcher.ResolutionChanged` |
### Installation
For using in PowerShell you can install it from PowerShell Gallery
```powershell
Install-Module DesktopManager -Force -Verbose
```
For the daily Windows tray app, use the DesktopManager MSI release artifact when available. The installer creates a Start Menu shortcut, and the app's Startup toggle registers the current user for minimized tray startup.
From source, the app and MSI are built through PowerForge:
```powershell
.\Build\Build-DesktopManagerApp.ps1
.\Build\Build-DesktopManagerApp-MSI.ps1
msiexec.exe /i .\Artefacts\PowerForge\Msi\DesktopManager.App\win-x64\net10.0-windows10.0.19041.0\PortableCompat\output\DesktopManager.msi
```
### Verified mutation examples
When you want more than "the call did not throw", prefer `-Verify` together with `-PassThru` so the cmdlet returns the observed postcondition.
```powershell
# Verify a window move against the observed geometry
Set-DesktopWindow -Name "*Visual Studio Code*" -Left 0 -Top 0 -Width 1600 -Height 1200 -Verify -PassThru
# Verify exact text after a control text update
Set-DesktopControlText -Control $ctrl -Text "Hello world" -Verify -PassThru
# Verify the observed check state
Set-DesktopControlCheck -Control $checkbox -Check $true -Verify -PassThru
# Return a structured record after a control click
Invoke-DesktopControlClick -Control $button -PassThru
# Return a structured record after sending keys
Send-DesktopControlKey -Control $editor -Keys @([DesktopManager.VirtualKey]::VK_RETURN) -PassThru
```
Verification behavior is action-aware:
- window mutations verify geometry, state, topmost, close, or foreground conditions when those postconditions are meaningful
- `Set-DesktopControlText -Verify` verifies the observed text/value when the control can be re-queried
- `Set-DesktopControlCheck -Verify` verifies the observed check state
- control click/key actions return honest presence and foreground evidence when exact semantic verification would be misleading
### Testing notes
UI-impacting tests are opt-in to avoid opening windows or changing the desktop during local development. Use the environment variables below to control behavior:
- `RUN_UI_TESTS=true` (or `DESKTOPMANAGER_RUN_UI_TESTS=true`) enables the top-level UI test pack.
- `RUN_OWNED_WINDOW_UI_TESTS=true` (or `DESKTOPMANAGER_RUN_OWNED_WINDOW_UI_TESTS=true`) enables owned-window UI tests that use repo-created harness windows.
- `RUN_DESTRUCTIVE_UI_TESTS=true` (or `DESKTOPMANAGER_RUN_DESTRUCTIVE_UI_TESTS=true`) enables owned-window mutation tests such as move, resize, hide, snap, and transparency changes against repo-created harness windows.
- `RUN_FOREGROUND_UI_TESTS=true` (or `DESKTOPMANAGER_RUN_FOREGROUND_UI_TESTS=true`) enables tests that intentionally steal foreground focus, even when they only target repo-created harness windows.
- `RUN_SYSTEM_UI_TESTS=true` (or `DESKTOPMANAGER_RUN_SYSTEM_UI_TESTS=true`) enables system-wide desktop changes such as wallpapers, brightness, resolution, and other monitor-level mutations that can affect your current session.
- `RUN_EXTERNAL_UI_TESTS=true` (or `DESKTOPMANAGER_RUN_EXTERNAL_UI_TESTS=true`) enables live external-application UI tests and MCP end-to-end harnesses that launch real desktop apps.
- `RUN_EXPERIMENTAL_UI_TESTS=true` (or `DESKTOPMANAGER_RUN_EXPERIMENTAL_UI_TESTS=true`) enables experimental live UI harnesses that are useful for manual validation but are not part of the stable regression pack.
- `SKIP_UI_TESTS=true` (or `DESKTOPMANAGER_SKIP_UI_TESTS=true`) forces UI tests to skip, even if enabled elsewhere.
Owned WinForms fixtures show without activation. Tests that explicitly activate a window or depend on foreground focus also require the foreground gate. Keep `RUN_FOREGROUND_UI_TESTS` and `DESKTOPMANAGER_RUN_FOREGROUND_UI_TESTS` disabled while using the desktop; enable that slice only during a dedicated interactive run.
The gates compose intentionally:
| Gate | What it permits | Typical impact |
| ---- | --------------- | -------------- |
| `RUN_UI_TESTS` | Top-level UI pack enablement only | Low |
| `RUN_OWNED_WINDOW_UI_TESTS` | Repo-owned WinForms/WPF harness windows | Low to medium |
| `RUN_DESTRUCTIVE_UI_TESTS` | Mutations against owned harness windows | Medium to high |
| `RUN_FOREGROUND_UI_TESTS` | Tests that intentionally take foreground focus | Medium to high |
| `RUN_SYSTEM_UI_TESTS` | System-wide desktop changes | High |
| `RUN_EXTERNAL_UI_TESTS` | Launching real desktop apps | Medium to high |
| `RUN_EXPERIMENTAL_UI_TESTS` | Experimental live harnesses | Varies |
```powershell
# Run owned-window UI tests
$env:RUN_UI_TESTS = "true"
$env:RUN_OWNED_WINDOW_UI_TESTS = "true"
# Run repo-owned window mutation tests
$env:RUN_DESTRUCTIVE_UI_TESTS = "true"
# Run tests that intentionally steal foreground focus
$env:RUN_FOREGROUND_UI_TESTS = "true"
# Run system-wide desktop mutations
$env:RUN_SYSTEM_UI_TESTS = "true"
# Run live external-application harnesses
$env:RUN_EXTERNAL_UI_TESTS = "true"
# Run experimental live UI harnesses
$env:RUN_EXPERIMENTAL_UI_TESTS = "true"
# Force skip
$env:SKIP_UI_TESTS = "true"
# Run the repo-owned window mutation slice
dotnet test Sources/DesktopManager.Tests/DesktopManager.Tests.csproj -f net8.0-windows10.0.19041.0 --no-build --filter "WindowPositionTests|WindowStateHelpersTests|WindowVisibilityTests|WindowTransparencyTests|WindowStyleModificationTests|WindowLayoutTests"
# Run the foreground-window slice
dotnet test Sources/DesktopManager.Tests/DesktopManager.Tests.csproj -f net8.0-windows10.0.19041.0 --no-build --filter "DesktopAutomationAssertionTests|WindowManagerFilterTests|WindowTopMostActivationTests"
# Run the live MCP desktop pack backed by the repo-owned DesktopManager.TestApp harness
dotnet test Sources/DesktopManager.Tests/DesktopManager.Tests.csproj -f net8.0-windows10.0.19041.0 --no-build --filter "McpServer_TestApp"
```
When the hosted-session or experimental live harnesses skip because focus or discoverability never stabilize, they now leave structured diagnostics and artifacts rather than failing silently. See [Docs/Build.Runbook.md](Docs/Build.Runbook.md) for the hosted-session artifact flow and gate guidance.
### Usage
#### Example in C#
The `DesktopManager.Example` project contains additional examples. Its default mode is read-only; mutation demos require the explicit `--run-mutations` option.
```csharp
var monitors = new Monitors();
foreach (var monitor in monitors.GetMonitors(connectedOnly: true)) {
MonitorPosition bounds = monitor.GetMonitorPosition();
Console.WriteLine($"{monitor.Index}: {monitor.DeviceName} at {bounds.Left},{bounds.Top} " +
$"({bounds.Right - bounds.Left}x{bounds.Bottom - bounds.Top})");
}
// Moving a monitor changes only its top-left coordinate. Change resolution separately.
// monitors.SetMonitorPosition(selected.DeviceId, left: -1920, top: 0);
```
#### Example in C# - Getting/Setting Window Position
```csharp
var manager = new WindowManager();
WindowInfo? window = manager.GetWindows(processName: "notepad").FirstOrDefault();
if (window != null) {
manager.SetWindowPosition(window, left: 100, top: 100, width: 1000, height: 700);
manager.ActivateWindow(window);
}
```
### Platform notes
When retrieving a window's style the library uses a helper method that calls
`GetWindowLong` on 32-bit processes and `GetWindowLongPtr` on 64-bit ones. The
method returns an `IntPtr`, so callers should convert the value to the
appropriate numeric type.
#### Example in PowerShell - Getting Monitor Information
```powershell
Get-DesktopMonitor | Format-Table
Get-DesktopWallpaper -Index 0
Set-DesktopWallpaper -Index 1 -WallpaperPath "C:\Wallpapers\Landscape.jpg" -Position Fit
Set-DesktopWallpaper -Index 0 -WallpaperPath "C:\Wallpapers\Portrait.jpg"
Set-DesktopBackgroundColor -Color 0x0000FF
Get-DesktopBackgroundColor
```
#### Example in PowerShell - Wallpaper Slideshow
```powershell
Start-DesktopSlideshow -ImagePath 'C:\Wallpapers\img1.jpg','C:\Wallpapers\img2.jpg' -Shuffle -SlideshowTick 300000
Get-DesktopSlideshow
Set-DesktopSlideshowOptions -NoShuffle -SlideshowTick 600000
Step-DesktopSlideshow -Direction Forward
Stop-DesktopSlideshow
```
```powershell
$Desktop1 = Get-DesktopMonitor
$Desktop1 | Format-Table
$Desktop2 = Get-DesktopMonitor -ConnectedOnly
$Desktop2 | Format-Table
$Desktop3 = Get-DesktopMonitor -PrimaryOnly
$Desktop3 | Format-Table
$Desktop4 = Get-DesktopMonitor -Index 1
$Desktop4 | Format-Table
$Desktop5 = Get-DesktopMonitor -DeviceName "\\.\DISPLAY2"
$Desktop5 | Format-Table
```
#### Example in PowerShell - Setting Monitor Position
```powershell
$Desktop2 = Get-DesktopMonitor -ConnectedOnly
$Desktop2 | Format-Table
Set-DesktopPosition -Index 0 -Left -3840 -Top 0 -WhatIf
Set-DesktopPosition -Index 1 -Left 0 -Top 0 -WhatIf
```
### Example in PowerShell - Getting/Setting Window Position
```powershell
Get-DesktopWindow | Format-Table *
```
```powershell
$np = Start-Process notepad -PassThru
Get-DesktopWindow -ProcessId $np.Id
$np | Stop-Process
```

```powershell
Set-DesktopWindow -Name '*Zadanie - Notepad' -Height 800 -Width 1200 -Left 100
Set-DesktopWindow -Name '*Zadanie - Notepad' -State Maximize
```
```powershell
Set-DesktopWindowText -Name '*Notepad*' -Text 'Hello world'
```
```powershell
# Send Ctrl+S to Notepad
$notepad = Get-DesktopWindow -Name '*Notepad*'
Invoke-DesktopKeyPress -Window $notepad -Keys @(
[DesktopManager.VirtualKey]::VK_CONTROL,
[DesktopManager.VirtualKey]::VK_S
)
# See Examples/KeyboardActions.ps1
```
### Example in PowerShell - Activating and Setting Window Top-Most
```powershell
Set-DesktopWindow -Name '*Notepad*' -TopMost -Activate
```
#### Example in C# - Activating and Setting Window Top-Most
```csharp
var manager = new WindowManager();
var window = manager.GetWindows().First();
manager.SetWindowTopMost(window, true);
manager.ActivateWindow(window);
#### Example in PowerShell - Monitoring Display Changes
Use `Register-DesktopMonitorEvent` to react when monitors are plugged in or the display configuration changes.
```powershell
Register-DesktopMonitorEvent -Duration (New-TimeSpan -Seconds 30) -Action { Write-Host 'Display settings changed' }
```
Use `Register-DesktopOrientationEvent` or `Register-DesktopResolutionEvent` to handle orientation or resolution changes individually.
```powershell
Register-DesktopResolutionEvent -Duration (New-TimeSpan -Seconds 30) -Action { Write-Host 'Resolution changed' }
Register-DesktopOrientationEvent -Duration (New-TimeSpan -Seconds 30) -Action { Write-Host 'Orientation changed' }
```
#### Example in C# - Monitoring Display Changes
Applications can subscribe to the `MonitorWatcher.DisplaySettingsChanged` event.
```csharp
MonitorWatcherExample.Run(TimeSpan.FromSeconds(30));
```
#### Example in PowerShell - Saving and Restoring Window Layout
Window geometry uses physical desktop pixels for enumeration, movement, client coordinates and pointer targeting. A width of 900 on a 150% display means 900 physical pixels; DPI-unaware hosts previously could observe 600 logical pixels for that window. Values read by `Get-DesktopWindow` now use the same units as `Set-DesktopWindow`.
Layout files saved by older versions in a DPI-unaware host can contain logical coordinates without identifying their DPI context. Those files retain their numeric values when restored; no reliable automatic conversion is possible. Re-save layouts on scaled displays after checking their placement with the new version. Existing layouts saved in physical pixels retain their coordinates.
For optional virtual desktop assignments, pins, Explorer folders and explicit
per-desktop wallpaper restoration, see [Workspace recovery](Docs/Workspace.Recovery.md).
```powershell
Save-DesktopWindowLayout -Path './layout.json'
# ... move windows around ...
Restore-DesktopWindowLayout -Path './layout.json' -Validate
```
#### Example in C# - Saving and Restoring Window Layout
```csharp
var manager = new WindowManager();
manager.SaveLayout("layout.json");
// ... move windows around ...
manager.LoadLayout("layout.json", validate: true);
```
Layout restoration uses each live window at most once and skips ambiguous or missing targets. Saved layouts include process and native class names so a restarted application can be matched without relying only on its title. Legacy files retain exact process-ID/title matching; title-only and process-only fallback are disabled. Use `PlanLayout` to inspect matches and `ApplyLayout` to obtain per-window outcomes:
```csharp
var layout = JsonSerializer.Deserialize<WindowLayout>(File.ReadAllText("layout.json"))!;
WindowLayoutPlan plan = manager.PlanLayout(layout);
foreach (WindowLayoutMatch match in plan.Windows) {
Console.WriteLine($"{match.SavedWindow.Title}: {match.Status} ({match.CandidateCount} candidates)");
}
IReadOnlyList<WindowLayoutApplyResult> results = manager.ApplyLayout(layout);
```
#### Examples in PowerShell - Window Keep-Alive Cmdlets
```powershell
# Keep Notepad alive for one minute
Start-DesktopWindowKeepAlive -Name '*Notepad*' -Interval 00:01:00
# List active sessions
Get-DesktopWindowKeepAlive
# Stop the session
Stop-DesktopWindowKeepAlive -Name '*Notepad*'
```
```powershell
# Keep Notepad and Calculator alive
Start-DesktopWindowKeepAlive -Name '*Notepad*'
Start-DesktopWindowKeepAlive -Name '*Calculator*' -Interval 00:00:30
Start-Sleep -Seconds 5
Get-DesktopWindowKeepAlive | Format-Table Title, Handle
Stop-DesktopWindowKeepAlive -All
```
```powershell
# Monitor RDP windows
Start-DesktopWindowKeepAlive -Name '*RDP*' -Interval 00:00:30
1..3 | ForEach-Object {
Start-Sleep -Seconds 10
Get-DesktopWindowKeepAlive | ForEach-Object { "Active: $($_.Title)" }
}
Stop-DesktopWindowKeepAlive -Name '*RDP*'
```
#### Examples in C# - Window Keep-Alive
```csharp
var manager = new WindowManager();
var notepad = manager.GetWindows("*Notepad*").FirstOrDefault();
if (notepad != null) {
WindowKeepAlive.Instance.Start(notepad, TimeSpan.FromMinutes(1));
}
```
```csharp
foreach (var window in new WindowManager().GetWindows("*Chrome*")) {
WindowKeepAlive.Instance.Start(window, TimeSpan.FromSeconds(30));
}
```
```csharp
foreach (var handle in WindowKeepAlive.Instance.ActiveHandles) {
Console.WriteLine($"Keeping {handle} alive");
}
WindowKeepAlive.Instance.StopAll();
```
### C# API Highlights
DesktopManager ships as a .NET library targeting `net472`, `netstandard2.0`, `net8.0-windows10.0.19041.0`, and `net10.0-windows10.0.19041.0`. The main entry points are:
- `Monitors` for monitor enumeration, wallpaper management, brightness, resolution, orientation and slideshow control.
- `WindowManager` for window enumeration, positioning, resizing and layout persistence.
- `MonitorWatcher` to receive events when display settings change.
- `ScreenshotService` to capture the desktop or custom regions.
- `WorkstationProfileService` and `WorkstationProfileStore` for cohesive named workstation state.
- `AudioService` and `AudioEndpointWatcher` for Core Audio endpoints, default roles, volume, mute, and changes.
- `AudioSessionService` for per-application volume, mute and peak levels, and `AudioService.OpenDefaultEndpoint` for a disposable endpoint level session.
- `MediaSessionService` for the current Windows media session, encoded artwork and transport controls; .NET 8 and .NET 10 Windows targets support this capability.
- `DeviceUsageService` for the microphone and camera applications recorded by the Windows privacy indicator.
- `SystemPowerService`, `DesktopSessionService`, and `DesktopSessionWatcher` for power and session state/actions.
- `PersonalizationService`, `PersonalizationStateStore`, and `TaskbarService` for reversible user settings and taskbars.
- `RadioService` for supported per-radio operations and `ExperimentalAirplaneModeService` for the separately acknowledged undocumented global switch.
- `WifiProfileService` for saved-profile inventory and exact profile connection without nearby-network scanning.
- `VirtualDesktopService` for the supported window-to-desktop operations.
### Audio and current media
Audio volumes use percentages from 0 through 100; signal peaks use 0 through 1.
Application readings group sessions by process name, retaining a representative
process ID and a key for subsequent volume or mute changes. Dispose the service
to release its cached session references.
```csharp
using var mixer = new AudioSessionService();
foreach (AudioApplicationInfo app in mixer.GetApplications()) {
Console.WriteLine($"{app.Name}: {app.VolumePercent:0}% (peak {app.PeakValue:0.00})");
}
using var media = new MediaSessionService();
MediaSessionResult current = await media.GetCurrentAsync();
if (current.Succeeded && current.Session is { } track) {
Console.WriteLine($"{track.Artist} — {track.Title}");
}
```
`MediaSessionService.Changed` signals that the current metadata should be read
again. Artwork is encoded image data, independent of any UI framework. Media
session operations report `Unsupported` on the older library targets and report
success with no session when Windows has no current source. Transport methods
request play/pause, next or previous only when called explicitly.
`AudioService.OpenDefaultEndpoint` retains the endpoint selected for a flow and
role when opened; reopen it after a default-device change. `GetState()` reads
volume, mute and the optional meter. `DeviceUsageService` reads Windows' current
privacy-indicator records without changing permissions; these records can lag
actual microphone or camera activity.
### Input, waits, and provider health
`TypeText` and `PasteText` insert at an editable control's current selection. They do not replace its whole value to repair an unexpected result. Background text delivery prefers the target's focused editor and rejects multiple eligible editors when none is focused. Use `WindowControlService.SetText` when replacement is intended. Paste, native clicks, and character messages are delivered once. `InputRetryCount` applies only when `SendInput` confirms zero input was delivered. A `NativeOperationOutcomeUnknownException` means the target may already have changed; observe its state before deciding whether to retry. Requested focus and clipboard-text restoration also runs when delivery fails.
Asynchronous waits support cancellation on every library target. A zero timeout waits indefinitely until a match appears or cancellation is requested:
```csharp
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10));
WindowInfo window = await manager.WaitWindowAsync("*Notepad*", cancellationToken: cancellation.Token);
```
UI Automation uses a separate STA worker per target process, with at most 16 workers and 64 queued operations per worker. Each worker permits up to 64 active event subscriptions with reserved cleanup capacity, so a full operation queue cannot lose cleanup. One blocked provider therefore leaves other processes available while capacity remains. In-flight native calls cannot be safely canceled or replayed and retain their worker until they return. `DesktopAutomationDiagnostics.GetUiAutomationHealth()` reports pending work and blocked calls without contacting the providers. Worker shutdown is bounded, and new work fails when the worker or queue limit is reached.
### Window capture provenance
`CaptureWindow` retains its desktop fallback behavior. `CaptureWindowDetailed` defaults to strict window capture and records the backend, actual screen bounds, dark-pixel observation, and fallback reason. A dark image can be legitimate; strict mode retains those pixels. Desktop fallback may contain a covering application's pixels.
```csharp
using WindowCaptureResult capture = ScreenshotService.CaptureWindowDetailed(window.Handle);
Console.WriteLine($"{capture.Backend}: {capture.ScreenBounds}");
capture.Bitmap.Save("window.png");
```
Pass `new WindowCaptureOptions { AllowDesktopFallback = true }` to opt into visible-desktop fallback. `DesktopAutomationService` capture objects and CLI/MCP screenshot results also expose the capture backend and fallback reason. Sparse dark-image and visual-difference sampling reads locked pixels directly, avoiding full-image managed copies; template matching retains its dense pixel buffer.
### Display requests requiring a restart
`SetMonitorDisplayModeDetailed` returns `RestartRequired` when Windows accepts a display request that needs a system restart. The existing void `SetMonitorDisplayMode` method throws `DisplayRestartRequiredException` for that outcome. Workstation profile results report `Succeeded = false` and `RestartRequired = true` for this request. PowerShell and the tray app display a warning; CLI JSON and MCP results preserve the restart requirement. No API restarts Windows automatically.