dsh-mini 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ # ud-shot.ps1 - screenshot the screen with the DSH HUD kept out of the picture.
2
+ #
3
+ # The HUD is an Edge/Chrome --app window, so "hide the harness window" would hide
4
+ # the wrong thing: what must disappear is the popout itself. This script hides the
5
+ # HUD, grabs the whole virtual screen, then puts the HUD back.
6
+ #
7
+ # Output: one line of JSON.
8
+ # {"status":"ok","data":{"file":"...","bytes":123,"x":..,"y":..,"width":..,"height":..,"hidden":1}}
9
+ # {"status":"error","error":"..."}
10
+ #
11
+ # ASCII-only source (Windows PowerShell 5.1 reads a BOM-less file as ANSI).
12
+
13
+ param(
14
+ [string]$Title = '',
15
+ [string]$TitleB64 = '',
16
+ [int]$OwnerPid = 0,
17
+ [Parameter(Mandatory = $true)]
18
+ [string]$OutFile,
19
+ [switch]$KeepHud
20
+ )
21
+
22
+ if (-not [string]::IsNullOrEmpty($TitleB64)) { $Title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($TitleB64)) }
23
+
24
+ $ErrorActionPreference = 'Stop'
25
+
26
+ Add-Type -AssemblyName System.Drawing
27
+ Add-Type -AssemblyName System.Windows.Forms
28
+
29
+ if (-not ([System.Management.Automation.PSTypeName]'Hud.ShotWin').Type) {
30
+ # -TypeDefinition (not -MemberDefinition): this block needs `using` directives and
31
+ # top-level structs/delegates, which a member-definition class body cannot hold.
32
+ Add-Type -TypeDefinition @'
33
+ using System;
34
+ using System.Text;
35
+ using System.Runtime.InteropServices;
36
+
37
+ namespace Hud {
38
+ public delegate bool ShotEnumProc(IntPtr hWnd, IntPtr lParam);
39
+
40
+ public static class ShotWin {
41
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
42
+ [DllImport("user32.dll")] public static extern bool EnumWindows(ShotEnumProc cb, IntPtr lParam);
43
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr h, StringBuilder s, int n);
44
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextLengthW(IntPtr h);
45
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
46
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
47
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
48
+ [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
49
+ }
50
+ }
51
+ '@
52
+ }
53
+
54
+ $SW_HIDE = 0
55
+ $SW_SHOWNA = 8
56
+ $SWP_NOACTIVATE = 0x0010
57
+ $SWP_NOSIZE = 0x0001
58
+ $SWP_NOMOVE = 0x0002
59
+ $SWP_SHOWWINDOW = 0x0040
60
+
61
+ function Get-Title([IntPtr]$h) {
62
+ $len = [Hud.ShotWin]::GetWindowTextLengthW($h)
63
+ if ($len -le 0) { return '' }
64
+ $sb = New-Object System.Text.StringBuilder ($len + 2)
65
+ [void][Hud.ShotWin]::GetWindowTextW($h, $sb, $sb.Capacity)
66
+ return $sb.ToString()
67
+ }
68
+
69
+ function New-PidTree([int]$root) {
70
+ $set = @{}
71
+ if ($root -le 0) { return $set }
72
+ $procs = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Select-Object ProcessId, ParentProcessId)
73
+ $byParent = @{}
74
+ foreach ($proc in $procs) {
75
+ $parentId = [int]$proc.ParentProcessId
76
+ if (-not $byParent.ContainsKey($parentId)) { $byParent[$parentId] = New-Object System.Collections.ArrayList }
77
+ [void]$byParent[$parentId].Add([int]$proc.ProcessId)
78
+ }
79
+ $queue = New-Object System.Collections.Queue
80
+ $queue.Enqueue($root)
81
+ $set[$root] = $true
82
+ while ($queue.Count -gt 0) {
83
+ $current = [int]$queue.Dequeue()
84
+ if (-not $byParent.ContainsKey($current)) { continue }
85
+ foreach ($childId in $byParent[$current]) {
86
+ if ($set.ContainsKey($childId)) { continue }
87
+ $set[$childId] = $true
88
+ $queue.Enqueue($childId)
89
+ }
90
+ }
91
+ return $set
92
+ }
93
+
94
+ function Find-HudWindows([string]$prefix, [int]$ownerPid) {
95
+ $script:browserPids = @{}
96
+ foreach ($p in (Get-Process -Name msedge, chrome -ErrorAction SilentlyContinue)) { $script:browserPids[[int]$p.Id] = $p.ProcessName }
97
+ $script:pidsInTree = New-PidTree $ownerPid
98
+ $script:titlePrefix = $prefix
99
+ $script:found = New-Object System.Collections.ArrayList
100
+
101
+ $cb = {
102
+ param([IntPtr]$h, [IntPtr]$l)
103
+ if (-not [Hud.ShotWin]::IsWindowVisible($h)) { return $true }
104
+ $processId = [uint32]0
105
+ [void][Hud.ShotWin]::GetWindowThreadProcessId($h, [ref]$processId)
106
+ if (-not $script:browserPids.ContainsKey([int]$processId)) { return $true }
107
+
108
+ $title = Get-Title $h
109
+ $titleHit = (-not [string]::IsNullOrWhiteSpace($script:titlePrefix)) -and
110
+ (-not [string]::IsNullOrWhiteSpace($title)) -and $title.StartsWith($script:titlePrefix)
111
+ $pidHit = $script:pidsInTree.ContainsKey([int]$processId)
112
+ if (-not ($titleHit -or $pidHit)) { return $true }
113
+
114
+ [void]$script:found.Add([int64]$h)
115
+ return $true
116
+ }
117
+ $null = [Hud.ShotWin]::EnumWindows($cb, [IntPtr]::Zero)
118
+ return $script:found
119
+ }
120
+
121
+ $hidden = New-Object System.Collections.ArrayList
122
+ try {
123
+ [void][Hud.ShotWin]::SetProcessDPIAware()
124
+
125
+ if (-not $KeepHud) {
126
+ foreach ($hwnd in @(Find-HudWindows -prefix $Title -ownerPid $OwnerPid)) {
127
+ [void][Hud.ShotWin]::ShowWindow([IntPtr]$hwnd, $SW_HIDE)
128
+ [void]$hidden.Add($hwnd)
129
+ }
130
+ if ($hidden.Count -gt 0) { Start-Sleep -Milliseconds 280 }
131
+ }
132
+
133
+ $v = [System.Windows.Forms.SystemInformation]::VirtualScreen
134
+ if ($v.Width -le 0 -or $v.Height -le 0) { throw "invalid virtual screen $($v.Width)x$($v.Height)" }
135
+
136
+ $bmp = New-Object System.Drawing.Bitmap($v.Width, $v.Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
137
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
138
+ $g.CopyFromScreen($v.X, $v.Y, 0, 0, (New-Object System.Drawing.Size($v.Width, $v.Height)), [System.Drawing.CopyPixelOperation]::SourceCopy)
139
+ $g.Dispose()
140
+
141
+ $dir = Split-Path -Parent $OutFile
142
+ if ($dir -and -not (Test-Path $dir)) { [void](New-Item -ItemType Directory -Path $dir -Force) }
143
+ $bmp.Save($OutFile, [System.Drawing.Imaging.ImageFormat]::Png)
144
+ $bmp.Dispose()
145
+ $bytes = (Get-Item $OutFile).Length
146
+ $safePath = $OutFile
147
+
148
+ Write-Output (([ordered]@{
149
+ status = 'ok'
150
+ data = [ordered]@{
151
+ file = $safePath; bytes = $bytes
152
+ x = $v.X; y = $v.Y; width = $v.Width; height = $v.Height
153
+ hidden = $hidden.Count
154
+ }
155
+ } | ConvertTo-Json -Compress -Depth 4))
156
+ }
157
+ catch {
158
+ Write-Output (([ordered]@{ status = 'error'; error = ($_.Exception.Message -replace '[^\x20-\x7E]', '?') } | ConvertTo-Json -Compress))
159
+ exit 1
160
+ }
161
+ finally {
162
+ # Always bring the HUD back, even when the capture failed.
163
+ foreach ($hwnd in $hidden) {
164
+ $h = [IntPtr]$hwnd
165
+ [void][Hud.ShotWin]::ShowWindow($h, $SW_SHOWNA)
166
+ [void][Hud.ShotWin]::SetWindowPos($h, [IntPtr]0, 0, 0, 0, 0, ($SWP_NOACTIVATE -bor $SWP_SHOWWINDOW -bor $SWP_NOSIZE -bor $SWP_NOMOVE))
167
+ }
168
+ }
@@ -0,0 +1,63 @@
1
+ # Reports or raises the native HUD shell (the DSH popout window).
2
+ # Prints one JSON line so the host's runScript helper can parse it.
3
+ param(
4
+ [ValidateSet('status', 'focus')]
5
+ [string]$Action = 'status',
6
+
7
+ [int]$WaitMs = 0
8
+ )
9
+
10
+ $ErrorActionPreference = 'Continue'
11
+
12
+ # The executable name carries two CJK characters; build it from code points so the
13
+ # script stays readable in any code page.
14
+ $pattern = '*DSH' + [char]0x5C0F + [char]0x7A97 + '.exe'
15
+
16
+ if (-not ('HudNative.Win' -as [type])) {
17
+ Add-Type -Namespace HudNative -Name Win -MemberDefinition @'
18
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(System.IntPtr h);
19
+ [DllImport("user32.dll")] public static extern bool BringWindowToTop(System.IntPtr h);
20
+ [DllImport("user32.dll")] public static extern bool ShowWindow(System.IntPtr h, int c);
21
+ '@
22
+ }
23
+
24
+ function Find-HudWindow {
25
+ $procs = @()
26
+ try {
27
+ $procs = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
28
+ $_.ExecutablePath -and ($_.ExecutablePath -like $pattern) -and ($_.CommandLine -notlike '*--service*')
29
+ })
30
+ } catch {
31
+ $procs = @()
32
+ }
33
+ foreach ($item in $procs) {
34
+ $proc = Get-Process -Id $item.ProcessId -ErrorAction SilentlyContinue
35
+ if ($proc -and $proc.MainWindowHandle -ne 0) { return $proc }
36
+ }
37
+ # A shell without a main window (minimised to tray) still counts as running.
38
+ if ($procs.Count -gt 0) { return Get-Process -Id $procs[0].ProcessId -ErrorAction SilentlyContinue }
39
+ return $null
40
+ }
41
+
42
+ $deadline = [DateTime]::UtcNow.AddMilliseconds([Math]::Max(0, $WaitMs))
43
+ while ($true) {
44
+ $proc = Find-HudWindow
45
+ if ($proc) {
46
+ $hwnd = [int64]$proc.MainWindowHandle
47
+ if ($Action -eq 'focus' -and $hwnd -ne 0) {
48
+ try {
49
+ [void][HudNative.Win]::ShowWindow([System.IntPtr]$hwnd, 9)
50
+ [void][HudNative.Win]::BringWindowToTop([System.IntPtr]$hwnd)
51
+ [void][HudNative.Win]::SetForegroundWindow([System.IntPtr]$hwnd)
52
+ } catch {
53
+ # Focus is best effort; the shell is topmost on its own most of the time.
54
+ }
55
+ }
56
+ Write-Output ('{"status":"ok","data":{"found":true,"pid":' + $proc.Id + ',"hwnd":' + $hwnd + ',"action":"' + $Action + '"}}')
57
+ exit 0
58
+ }
59
+ if ([DateTime]::UtcNow -ge $deadline) { break }
60
+ Start-Sleep -Milliseconds 150
61
+ }
62
+
63
+ Write-Output '{"status":"ok","data":{"found":false}}'
@@ -0,0 +1,264 @@
1
+ # win-hud.ps1 - locate / pin / focus / close the DSH HUD window.
2
+ #
3
+ # The HUD is an Edge or Chrome `--app` window. It is identified two ways, because
4
+ # neither alone is reliable:
5
+ # * by page title prefix -> stable once the HUD page has loaded its own title
6
+ # * by owner process tree -> works even before the page has set its title
7
+ # A window is accepted when it belongs to a browser process AND either test hits.
8
+ #
9
+ # Output: one line of JSON.
10
+ # {"status":"ok","data":{...}} | {"status":"error","error":"..."}
11
+ #
12
+ # ASCII-only source on purpose: Windows PowerShell 5.1 reads a BOM-less file as the
13
+ # ANSI codepage. User-facing text arrives through -Title, and command-line
14
+ # arguments cross the process boundary as UTF-16, so Chinese survives.
15
+
16
+ param(
17
+ [ValidateSet('pin', 'focus', 'close', 'status')]
18
+ [string]$Action = 'pin',
19
+ [string]$Title = '',
20
+ [string]$TitleB64 = '',
21
+ [int]$OwnerPid = 0,
22
+ [int]$Width = 460,
23
+ [int]$Height = 920,
24
+ [int]$Margin = 8,
25
+ [int]$WaitMs = 0,
26
+ [switch]$NoTopmost
27
+ )
28
+
29
+ if (-not [string]::IsNullOrEmpty($TitleB64)) { $Title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($TitleB64)) }
30
+
31
+ $ErrorActionPreference = 'Stop'
32
+
33
+ if (-not ([System.Management.Automation.PSTypeName]'Hud.Win').Type) {
34
+ # -TypeDefinition (not -MemberDefinition): this block needs `using` directives and
35
+ # top-level structs/delegates, which a member-definition class body cannot hold.
36
+ Add-Type -TypeDefinition @'
37
+ using System;
38
+ using System.Text;
39
+ using System.Runtime.InteropServices;
40
+
41
+ namespace Hud {
42
+ public struct HudRect { public int Left, Top, Right, Bottom; }
43
+
44
+ [StructLayout(LayoutKind.Sequential)]
45
+ public struct HudMonitorInfo { public int cbSize; public HudRect rcMonitor; public HudRect rcWork; public uint dwFlags; }
46
+
47
+ public delegate bool HudEnumProc(IntPtr hWnd, IntPtr lParam);
48
+
49
+ public static class Win {
50
+ [DllImport("user32.dll")] public static extern bool EnumWindows(HudEnumProc cb, IntPtr lParam);
51
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr h, StringBuilder s, int n);
52
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextLengthW(IntPtr h);
53
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
54
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
55
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out HudRect r);
56
+ [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
57
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
58
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
59
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
60
+ [DllImport("user32.dll")] public static extern IntPtr SendMessageW(IntPtr h, uint msg, IntPtr w, IntPtr l);
61
+ [DllImport("user32.dll")] public static extern int GetWindowLongW(IntPtr h, int idx);
62
+ [DllImport("user32.dll")] public static extern uint GetDpiForWindow(IntPtr h);
63
+ [DllImport("user32.dll")] public static extern IntPtr MonitorFromWindow(IntPtr h, uint flags);
64
+ [DllImport("user32.dll")] public static extern bool GetMonitorInfoW(IntPtr mon, ref HudMonitorInfo mi);
65
+ }
66
+ }
67
+ '@
68
+ }
69
+
70
+ $HWND_TOPMOST = [IntPtr](-1)
71
+ $HWND_NOTOPMOST = [IntPtr](-2)
72
+ $SWP_NOACTIVATE = 0x0010
73
+ $SWP_SHOWWINDOW = 0x0040
74
+ $SWP_NOSIZE = 0x0001
75
+ $SWP_NOMOVE = 0x0002
76
+ $SW_RESTORE = 9
77
+ $WM_CLOSE = 0x0010
78
+ $WS_EX_TOPMOST = 0x00000008
79
+
80
+ function Get-Title([IntPtr]$h) {
81
+ $len = [Hud.Win]::GetWindowTextLengthW($h)
82
+ if ($len -le 0) { return '' }
83
+ $sb = New-Object System.Text.StringBuilder ($len + 2)
84
+ [void][Hud.Win]::GetWindowTextW($h, $sb, $sb.Capacity)
85
+ return $sb.ToString()
86
+ }
87
+
88
+ function New-PidTree([int]$root) {
89
+ $set = @{}
90
+ if ($root -le 0) { return $set }
91
+ $procs = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Select-Object ProcessId, ParentProcessId)
92
+ $byParent = @{}
93
+ foreach ($proc in $procs) {
94
+ $parentId = [int]$proc.ParentProcessId
95
+ if (-not $byParent.ContainsKey($parentId)) { $byParent[$parentId] = New-Object System.Collections.ArrayList }
96
+ [void]$byParent[$parentId].Add([int]$proc.ProcessId)
97
+ }
98
+ $queue = New-Object System.Collections.Queue
99
+ $queue.Enqueue($root)
100
+ $set[$root] = $true
101
+ while ($queue.Count -gt 0) {
102
+ $current = [int]$queue.Dequeue()
103
+ if (-not $byParent.ContainsKey($current)) { continue }
104
+ foreach ($childId in $byParent[$current]) {
105
+ if ($set.ContainsKey($childId)) { continue }
106
+ $set[$childId] = $true
107
+ $queue.Enqueue($childId)
108
+ }
109
+ }
110
+ return $set
111
+ }
112
+
113
+ function Find-HudWindows([string]$prefix, [int]$ownerPid) {
114
+ # Script scope on purpose: the EnumWindows callback is invoked from native code,
115
+ # so it can only reach variables that live in the script scope.
116
+ $script:browserPids = @{}
117
+ foreach ($p in (Get-Process -Name msedge, chrome -ErrorAction SilentlyContinue)) { $script:browserPids[[int]$p.Id] = $p.ProcessName }
118
+ $script:pidsInTree = New-PidTree $ownerPid
119
+ $script:titlePrefix = $prefix
120
+ $script:found = New-Object System.Collections.ArrayList
121
+
122
+ $cb = {
123
+ param([IntPtr]$h, [IntPtr]$l)
124
+ if (-not [Hud.Win]::IsWindowVisible($h)) { return $true }
125
+ $processId = [uint32]0
126
+ [void][Hud.Win]::GetWindowThreadProcessId($h, [ref]$processId)
127
+ if (-not $script:browserPids.ContainsKey([int]$processId)) { return $true }
128
+
129
+ $title = Get-Title $h
130
+ $titleHit = (-not [string]::IsNullOrWhiteSpace($script:titlePrefix)) -and
131
+ (-not [string]::IsNullOrWhiteSpace($title)) -and $title.StartsWith($script:titlePrefix)
132
+ $pidHit = $script:pidsInTree.ContainsKey([int]$processId)
133
+ if (-not ($titleHit -or $pidHit)) { return $true }
134
+
135
+ [Hud.HudRect]$r = New-Object Hud.HudRect
136
+ [void][Hud.Win]::GetWindowRect($h, [ref]$r)
137
+ [void]$script:found.Add([pscustomobject]@{
138
+ hwnd = [int64]$h
139
+ pid = [int]$processId
140
+ title = $title
141
+ titleHit = [bool]$titleHit
142
+ pidHit = [bool]$pidHit
143
+ x = $r.Left; y = $r.Top
144
+ width = ($r.Right - $r.Left); height = ($r.Bottom - $r.Top)
145
+ })
146
+ return $true
147
+ }
148
+ $null = [Hud.Win]::EnumWindows($cb, [IntPtr]::Zero)
149
+ return $script:found
150
+ }
151
+
152
+ function Get-HudWindow([string]$prefix, [int]$ownerPid, [int]$waitMs) {
153
+ $deadline = (Get-Date).AddMilliseconds($waitMs)
154
+ do {
155
+ $all = @(Find-HudWindows -prefix $prefix -ownerPid $ownerPid)
156
+ if ($all.Count -gt 0) {
157
+ # A window whose title already matches is the HUD itself; prefer it, then the
158
+ # largest area (browser splash windows are small).
159
+ return ($all |
160
+ Sort-Object -Property @{ Expression = { $_.titleHit }; Descending = $true },
161
+ @{ Expression = { $_.width * $_.height }; Descending = $true } |
162
+ Select-Object -First 1)
163
+ }
164
+ if ((Get-Date) -ge $deadline) { break }
165
+ Start-Sleep -Milliseconds 250
166
+ } while ($true)
167
+ return $null
168
+ }
169
+
170
+ function Get-WorkArea([IntPtr]$h) {
171
+ $mon = [Hud.Win]::MonitorFromWindow($h, 2)
172
+ [Hud.HudMonitorInfo]$mi = New-Object Hud.HudMonitorInfo
173
+ $mi.cbSize = [System.Runtime.InteropServices.Marshal]::SizeOf($mi)
174
+ if ($mon -ne [IntPtr]::Zero -and [Hud.Win]::GetMonitorInfoW($mon, [ref]$mi)) {
175
+ return [pscustomobject]@{
176
+ x = $mi.rcWork.Left; y = $mi.rcWork.Top
177
+ width = ($mi.rcWork.Right - $mi.rcWork.Left)
178
+ height = ($mi.rcWork.Bottom - $mi.rcWork.Top)
179
+ }
180
+ }
181
+ Add-Type -AssemblyName System.Windows.Forms
182
+ $wa = [System.Windows.Forms.Screen]::FromHandle($h).WorkingArea
183
+ return [pscustomobject]@{ x = $wa.X; y = $wa.Y; width = $wa.Width; height = $wa.Height }
184
+ }
185
+
186
+ function Get-Rect([IntPtr]$h) {
187
+ [Hud.HudRect]$r = New-Object Hud.HudRect
188
+ [void][Hud.Win]::GetWindowRect($h, [ref]$r)
189
+ return [pscustomobject]@{ x = $r.Left; y = $r.Top; width = ($r.Right - $r.Left); height = ($r.Bottom - $r.Top) }
190
+ }
191
+
192
+ function Test-Topmost([IntPtr]$h) {
193
+ $ex = [Hud.Win]::GetWindowLongW($h, -20) # GWL_EXSTYLE
194
+ return (($ex -band $WS_EX_TOPMOST) -ne 0)
195
+ }
196
+
197
+ function Send-Ok($data) {
198
+ Write-Output (([ordered]@{ status = 'ok'; data = $data } | ConvertTo-Json -Compress -Depth 6))
199
+ }
200
+
201
+ try {
202
+ $target = Get-HudWindow -prefix $Title -ownerPid $OwnerPid -waitMs $WaitMs
203
+ if ($null -eq $target) {
204
+ if ($Action -eq 'status') { Send-Ok ([ordered]@{ found = $false }); exit 0 }
205
+ throw "HUD window not found (title prefix: $Title, owner pid: $OwnerPid)"
206
+ }
207
+ $h = [IntPtr]$target.hwnd
208
+
209
+ if ($Action -eq 'status') {
210
+ Send-Ok ([ordered]@{
211
+ found = $true; hwnd = $target.hwnd; pid = $target.pid; title = $target.title
212
+ rect = (Get-Rect $h); topmost = (Test-Topmost $h); workArea = (Get-WorkArea $h)
213
+ })
214
+ exit 0
215
+ }
216
+
217
+ if ($Action -eq 'close') {
218
+ [void][Hud.Win]::SendMessageW($h, $WM_CLOSE, [IntPtr]::Zero, [IntPtr]::Zero)
219
+ Send-Ok ([ordered]@{ closed = $true; hwnd = $target.hwnd })
220
+ exit 0
221
+ }
222
+
223
+ if ([Hud.Win]::IsIconic($h)) {
224
+ [void][Hud.Win]::ShowWindow($h, $SW_RESTORE)
225
+ Start-Sleep -Milliseconds 200
226
+ }
227
+
228
+ if ($Action -eq 'focus') {
229
+ [void][Hud.Win]::ShowWindow($h, $SW_RESTORE)
230
+ [void][Hud.Win]::SetForegroundWindow($h)
231
+ Send-Ok ([ordered]@{ focused = $true; hwnd = $target.hwnd })
232
+ exit 0
233
+ }
234
+
235
+ # pin: dock to the right edge of whichever monitor the window is on.
236
+ $dpi = [Hud.Win]::GetDpiForWindow($h)
237
+ if ($dpi -lt 96) { $dpi = 96 }
238
+ $scale = $dpi / 96.0
239
+ $wa = Get-WorkArea $h
240
+ $mw = [int][Math]::Round($Margin * $scale)
241
+ $w = [int][Math]::Min([int][Math]::Round($Width * $scale), $wa.width - 2 * $mw)
242
+ $hh = [int][Math]::Min([int][Math]::Round($Height * $scale), $wa.height - 2 * $mw)
243
+ $x = $wa.x + $wa.width - $w - $mw
244
+ $y = $wa.y + $mw
245
+
246
+ $after = if ($NoTopmost) { $HWND_NOTOPMOST } else { $HWND_TOPMOST }
247
+ $flags = $SWP_NOACTIVATE -bor $SWP_SHOWWINDOW
248
+ if (-not [Hud.Win]::SetWindowPos($h, $after, $x, $y, $w, $hh, $flags)) { throw 'SetWindowPos failed' }
249
+ Start-Sleep -Milliseconds 120
250
+ # Some windows re-assert their own z-order right after a resize; a second pass
251
+ # with the position locked keeps the HUD on top.
252
+ [void][Hud.Win]::SetWindowPos($h, $after, 0, 0, 0, 0, ($SWP_NOMOVE -bor $SWP_NOSIZE -bor $SWP_NOACTIVATE))
253
+
254
+ Send-Ok ([ordered]@{
255
+ hwnd = $target.hwnd; pid = $target.pid; title = $target.title
256
+ rect = (Get-Rect $h); topmost = (Test-Topmost $h); dpi = $dpi
257
+ topmostRequested = (-not $NoTopmost)
258
+ })
259
+ }
260
+ catch {
261
+ $err = [ordered]@{ status = 'error'; error = ($_.Exception.Message -replace '[^\x20-\x7E]', '?') }
262
+ Write-Output ($err | ConvertTo-Json -Compress)
263
+ exit 1
264
+ }