dsh-mini 0.0.0-stage → 0.1.1
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.
- package/README.md +230 -2
- package/app/HudWindow.cs +1278 -0
- package/app/RegionCapture.cs +552 -0
- package/app/SelectionWatcher.cs +850 -0
- package/app/Service.cs +412 -0
- package/app/app.manifest +22 -0
- package/app/bin/DSH/345/260/217/347/252/227.exe +0 -0
- package/app/bin/Microsoft.Web.WebView2.Core.dll +0 -0
- package/app/bin/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/app/bin/WebView2Loader.dll +0 -0
- package/app/build-app.ps1 +112 -0
- package/app/dev.ps1 +63 -0
- package/app/hud-page.g.cs +164 -0
- package/app/hud-page.js +157 -0
- package/app/probe/clear-composer.js +8 -0
- package/app/probe/dom-probe.js +40 -0
- package/app/probe/drag.ps1 +35 -0
- package/app/probe/dump-composer.js +5 -0
- package/app/probe/frame-probe.js +21 -0
- package/app/probe/selection-sample.txt +4 -0
- package/app/probe/top-probe.js +39 -0
- package/app/vendor/Microsoft.Web.WebView2.Core.dll +0 -0
- package/app/vendor/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/app/vendor/WebView2Loader.dll +0 -0
- package/cordis.patch.yml +16 -0
- package/lib/client.js +660 -0
- package/lib/index.js +521 -0
- package/package.json +69 -4
- package/scripts/hud-shot.ps1 +168 -0
- package/scripts/native-status.ps1 +63 -0
- package/scripts/win-hud.ps1 +264 -0
- package//345/274/200/345/217/221/346/226/271/346/241/210-WebView2/345/216/237/347/224/237/345/260/217/347/252/227.md +216 -0
|
@@ -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
|
+
}
|