filebase 0.2.1

Query a directory of Slipcase containers by their flyleaf and look at what comes back
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# Photograph an application's own window at a size the Microsoft Store accepts.
# Generated by ship (8001e560); edit internal/generate/templates and run ship sync.
#
# Screenshots were once listed as something no script could do, and that was an
# assumption rather than a measurement. What a script cannot do is decide which
# document to open and whether the result is a good advertisement. What it can
# do is every mechanical part: launch the application, size the window so the
# visible frame is exactly the size asked for, bring it to the front, drive it
# into the state the shot is of, park the pointer off the frame, capture, and
# refuse if what came back is the wrong size.
#
# This file knows nothing about the product it photographs. What to launch and
# what to do to the window come from `shots.ps1` beside it, which is the repo's
# own half and is where the documents and the coordinates live.
#
#   ...\screenshot.ps1 -Launch shell,C:\path\to\demo.dclx -Process segler-desktop `
#       -Out C:\shots\01-window.png
#   ...\screenshot.ps1 -Launch exe,C:\bin\xodt.exe,C:\docs\report.odt -Process xodt `
#       -Out C:\shots\xodt-01.png -Do 'click 423,40'
#
# `-Launch` is how the application starts, and its first element names which of
# three ways:
#
#   shell,<path>                open the path through its association
#   exe,<path>[,<arg>...]       run an executable
#   package,<aumid>[,<arg>...]  run an installed package
#
# `shell` is what a person's machine does, and is the wrong one where an office
# suite would win the association. `package` reaches the packaged build by its
# `<PackageFamilyName>!<ApplicationId>`, which is the only way in to one: a
# package declares no execution alias, and `WindowsApps` refuses `Start-Process`
# with *Access is denied* even to its owner.
#
# A verb rather than a guess from the shape of the first element. A typo in an
# executable's path would otherwise be launched as a document, or looked for as
# an executable that is not there, and both answers are about the wrong thing.
#
# FOUR ACTIONS, IN THE ORDER GIVEN
#
# `-Do` takes them as one ordered list of strings, each a verb and its argument:
#
#   -Do 'click 423,40','double 1020,384','key ctrl+a','type -0:38'
#
# `click X,Y` presses a control. `double X,Y` presses it twice inside the
# system's double-click interval, which is how a block of a document opens for
# typing. `type TEXT` types. `key NAME` sends one key, optionally with
# modifiers: `key ctrl+a`, `key return`, `key ctrl+plus`. X and Y are measured
# from the frame's top-left corner on a shot of the same size, so a coordinate
# read off an earlier shot is the coordinate to give.
#
# One parameter holding an ordered list, rather than the four repeatable flags
# `packaging/macos/screenshot.sh` has: PowerShell binds a parameter once, so a
# second `-Click` is an error rather than a second value, and four separate
# parameters could not say which of them came first.
#
# They exist because a listing wants more than a document at rest: a store
# reviewer reads a frame of an application with nothing selected as a frame of
# a viewer, and driving the window is the only way to get one that is not.
#
# WHAT IT REFUSES ON, AND WHY EACH ONE IS HERE
#
# **It checks the window actually reached the foreground.** `SetForegroundWindow`
# is advisory: Windows refuses it from a process that does not own the
# foreground, and it returns false rather than raising. The capture is
# `CopyFromScreen` over the window's rectangle, so a window that stayed behind
# is photographed as whatever is on top of it. That is not a hypothetical -
# Segler's `03-picture.png` of 2026-09-05 went to the store folder as a picture
# of a terminal, an Explorer ribbon and a sliver of the application, and it got
# there because the script called `SetForegroundWindow` and did not look. It
# sends the ALT tap that releases the foreground lock, retries, and refuses if
# the window is still not in front.
#
# **It waits for a window rather than for a number of seconds, and then waits
# for that window to be the one.** A cold start under a loaded runner takes
# longer than a cold start on a desk, and a fixed sleep is either wasted or
# short. But the first handle a process reports is not always the window a
# person sees: Segler's first cold launch on a runner reported one, this script
# sized and drove it, and the picture came back a third desktop wallpaper with
# the real window offset inside it and the correction never typed. So the
# handle has to be visible, titled, and the same on two reads before anything
# is done to it - and it is read again before the shutter, because actions sent
# to a window that is no longer the one is a photograph of the wrong thing
# reported as a success.
#
# **And it polls the geometry until it stops moving.** A cold start of the
# packaged application is still positioning itself seconds in, and a rect read
# during that produced a window sitting below where it was asked to be with the
# desktop showing along two edges. Two consecutive equal reads, or a refusal.
#
# TWO THINGS MEASURED RATHER THAN ASSUMED, BOTH OF THEM PIXELS
#
# `SetWindowPos` sizes the *window rect*, which carries an invisible resize
# border outside the visible frame: asking for 1366x768 gave a frame of
# 1352x761, which is under the Store's minimum. The visible frame is
# `DWMWA_EXTENDED_FRAME_BOUNDS`, and the difference measured is 14 by 7.
#
# And that frame's top edge is one pixel above what is actually drawn, so a
# capture at exactly the frame rect picks up a sliver of whatever is behind. It
# arrived as a strip of console text across the top of slipcase-desktop's first
# two attempts. The capture is two rows taller than needed and the top two are
# cropped.
#
# Author: David M. Anderson
# Built with AI assistance (Claude, Anthropic)

[CmdletBinding()]
param(
    # How the application starts: a verb and its arguments. The header has the
    # three.
    [Parameter(Mandatory = $true)][string[]] $Launch,
    # The processes to stop first, so the window in the frame is this run's and
    # not a previous one's. The first is the one the window belongs to. No .exe.
    [Parameter(Mandatory = $true)][string[]] $Process,
    [Parameter(Mandatory = $true)][string] $Out,
    # The Store's minimum for a desktop screenshot, and the default because a
    # window this size looks like a window rather than like an advertisement.
    [int] $Width = 1366,
    [int] $Height = 768,
    # Where to put the window. Anywhere it fits entirely on screen.
    [int] $X = 200,
    [int] $Y = 100,
    # Seconds to wait for a window to appear before giving up.
    [int] $Appear = 60,
    # Seconds to wait after the window is sized before capturing. The default is
    # enough for a document of a few pages; a large archive decoding its page
    # images wants more.
    [int] $Settle = 3,
    # What to do to the window before the shutter, in the order given. The
    # header lists the four verbs.
    [string[]] $Do = @(),
    # Photograph the window in this language. A listing in two languages needs
    # a set in each, and a German listing showing an English window is a German
    # listing of somebody else's application.
    #
    # It is set as POTEXT_LANG, which every application in this fleet reads
    # before it asks the platform, and `Start-Process` hands this process's
    # environment to what it starts.
    #
    # **The packaged launch is not known to carry it.** `-Launch package` goes
    # through the shell's activation manager rather than starting a child here,
    # and whether the variable survives that has not been measured. Until it
    # has, a set in a second language is taken of the release build, which is
    # what these frames are of anyway because a Store package cannot be
    # launched off the Store.
    [string] $Lang = ''
)

$ErrorActionPreference = 'Stop'

function Refuse([string] $message) { Write-Error "screenshot.ps1: $message" }

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type -Namespace Shot -Name Win -MemberDefinition @'
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
[DllImport("dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr h, int attr, out RECT r, int size);
[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra);
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")] public static extern void mouse_event(uint flags, uint dx, uint dy, uint data, UIntPtr extra);
[DllImport("user32.dll")] public static extern uint GetDoubleClickTime();
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, System.Text.StringBuilder text, int count);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);

public static string TitleOf(IntPtr h) {
    System.Text.StringBuilder title = new System.Text.StringBuilder(512);
    GetWindowText(h, title, title.Capacity);
    return title.ToString();
}
public struct RECT { public int Left, Top, Right, Bottom; }

// What has the foreground, said in full. A refusal that only knows the window
// is not ours sends the next person guessing, and the guesses are all
// plausible: the pointer parked on a hot corner, a shell window raised behind,
// an installer the image runs on a timer. The answer is one call away.
public static string WhatIsInFront() {
    IntPtr h = GetForegroundWindow();
    if (h == IntPtr.Zero) { return "nothing"; }
    System.Text.StringBuilder title = new System.Text.StringBuilder(512);
    GetWindowText(h, title, title.Capacity);
    uint pid;
    GetWindowThreadProcessId(h, out pid);
    string name;
    try { name = System.Diagnostics.Process.GetProcessById((int)pid).ProcessName; }
    catch { name = "pid " + pid; }
    return name + " \"" + title.ToString() + "\"";
}

// Typing goes in as Unicode on a synthetic key event rather than as a key
// code, so a line with punctuation in it needs no layout table and a machine
// on a German keyboard types what it was given. `keybd_event` cannot do this:
// it carries a virtual key, and a virtual key is a position on the current
// layout rather than a character.
//
// The union is declared as a union rather than as a keyboard-shaped struct
// with the difference padded out, and the size is asked for rather than
// written down. `SendInput` refuses any `cbSize` that is not its own, and the
// number is 40 on x64 and 28 on x86; a constant here is a constant that is
// wrong on one of them and silently does nothing, since the call reports how
// many events it sent and nobody was reading that either.
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT { public int dx, dy; public uint mouseData, dwFlags, time; public IntPtr extra; }
[StructLayout(LayoutKind.Sequential)]
public struct KEYBDINPUT { public ushort wVk, wScan; public uint dwFlags, time; public IntPtr extra; }
[StructLayout(LayoutKind.Explicit)]
public struct INPUTUNION { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; }
[StructLayout(LayoutKind.Sequential)]
public struct INPUT { public uint type; public INPUTUNION u; }
[DllImport("user32.dll", SetLastError = true)] static extern uint SendInput(uint n, INPUT[] inputs, int size);

// A surrogate pair is two code units and goes in as two events, which is what
// iterating the string rather than the characters gives for free.
public static void TypeText(string text) {
    int size = Marshal.SizeOf(typeof(INPUT));
    foreach (char unit in text) {
        INPUT[] pair = new INPUT[2];
        for (int i = 0; i < 2; i++) {
            pair[i].type = 1;                              // INPUT_KEYBOARD
            pair[i].u.ki.wScan = unit;
            pair[i].u.ki.dwFlags = (uint)(0x0004 | (i == 1 ? 0x0002 : 0));  // UNICODE | KEYUP
        }
        if (SendInput(2, pair, size) != 2) {
            throw new Exception("SendInput would not send '" + unit + "'");
        }
        // The window redraws between characters, and faster than this drops
        // them the way the Linux lane's xdotool does.
        System.Threading.Thread.Sleep(40);
    }
}
'@

# The visible frame, which is what everything below is measured against.
function Get-Frame([IntPtr] $h) {
    $r = New-Object Shot.Win+RECT
    [void][Shot.Win]::DwmGetWindowAttribute($h, 9, [ref]$r, 16)
    return $r
}

# --- starting it ------------------------------------------------------------

Get-Process $Process -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 1

# The window belongs to the first named process; the rest are stopped because
# they would hold the document open or claim the window.
$window = $Process[0]

if ($Lang) {
    $env:POTEXT_LANG = $Lang
    Write-Host "  language $Lang"
}

$verb = $Launch[0]
# Set by the exe branch only: the shell and a package hand the work to a
# launcher that exits at once, and its exit code says nothing about the
# application.
$started = $null
$launched = $null
$rest = @()
if ($Launch.Count -gt 1) { $rest = $Launch[1..($Launch.Count - 1)] }

switch ($verb) {
    'shell' {
        if ($rest.Count -ne 1) { Refuse "-Launch shell wants one path and was given $($rest.Count)" }
        if (-not (Test-Path -LiteralPath $rest[0])) { Refuse "nothing at $($rest[0])" }
        Start-Process (Resolve-Path -LiteralPath $rest[0]).Path
    }
    'exe' {
        if ($rest.Count -lt 1) { Refuse '-Launch exe wants an executable' }
        if (-not (Test-Path -LiteralPath $rest[0])) { Refuse "no executable at $($rest[0])" }
        $exe = (Resolve-Path -LiteralPath $rest[0]).Path
        # Kept, and its output with it. An executable that starts and stops
        # leaves nothing behind otherwise, and the wait below then spends its
        # whole minute on a process that was gone in the first second.
        # `Start-Process` discards both streams unless asked.
        $launched = Join-Path ([System.IO.Path]::GetTempPath()) "screenshot-launch-$PID"
        $started = if ($rest.Count -gt 1) {
            Start-Process $exe -ArgumentList $rest[1..($rest.Count - 1)] -PassThru `
                -RedirectStandardError "$launched.err" -RedirectStandardOutput "$launched.out"
        } else {
            Start-Process $exe -PassThru `
                -RedirectStandardError "$launched.err" -RedirectStandardOutput "$launched.out"
        }
    }
    'package' {
        if ($rest.Count -lt 1) { Refuse '-Launch package wants a PackageFamilyName!ApplicationId' }
        # Quoted one argument at a time: a document path with a space in it
        # arrives at the package as two arguments otherwise, and the package
        # opens neither.
        $target = "shell:appsFolder\$($rest[0])"
        if ($rest.Count -gt 1) {
            $quoted = @($rest[1..($rest.Count - 1)] | ForEach-Object { '"' + $_ + '"' })
            Start-Process $target -ArgumentList $quoted
        } else {
            Start-Process $target
        }
    }
    default {
        Refuse "no launch called '$verb' - the three are shell, exe and package, each with its arguments after it, as in 'shell,C:\path\to\demo.dclx'"
    }
}

# Polled rather than slept through. A fixed wait is either wasted time on a
# desk or too short on a loaded runner, and the second reads as "nothing opened
# it" when the truth is that it had not opened it yet.
#
# What counts as the window: visible, titled, and the same handle twice running.
# A handle that is none of those is a window on its way to being one.
function Get-Window {
    $app = Get-Process $window -ErrorAction SilentlyContinue | Select-Object -First 1
    if (-not $app) { return [IntPtr]::Zero }
    $app.Refresh()
    $h = $app.MainWindowHandle
    if ($h -eq [IntPtr]::Zero) { return [IntPtr]::Zero }
    if (-not [Shot.Win]::IsWindowVisible($h)) { return [IntPtr]::Zero }
    if ([Shot.Win]::TitleOf($h) -eq '') { return [IntPtr]::Zero }
    return $h
}

# What a launched executable left behind when it did not stay. Said in full:
# an exit code alone rarely names the reason and the streams usually do.
function SaidOnTheWayOut {
    $said = @()
    foreach ($stream in 'err', 'out') {
        $file = "$launched.$stream"
        if (Test-Path -LiteralPath $file) {
            $text = (Get-Content -LiteralPath $file -Raw -ErrorAction SilentlyContinue)
            if ($text) { $said += $text.Trim() }
        }
    }
    if ($said.Count -eq 0) { return 'and said nothing on either stream' }
    return "and said: $($said -join ' / ')"
}

$handle = [IntPtr]::Zero
$seen = [IntPtr]::Zero
$waited = 0
while ($waited -lt $Appear) {
    # An executable that has exited will not grow a window, so the wait ends
    # here rather than a minute later with nothing to show for it.
    if ($null -ne $started -and $started.HasExited) {
        Refuse "$($Launch[1]) exited $($started.ExitCode) after $waited second(s) without opening a window, $(SaidOnTheWayOut)"
    }
    $now = Get-Window
    if ($now -ne [IntPtr]::Zero -and $now -eq $seen) { $handle = $now; break }
    $seen = $now
    Start-Sleep -Milliseconds 700
    $waited += 0.7
}
if ($handle -eq [IntPtr]::Zero) {
    # Which of the three things went wrong, said rather than left to be
    # guessed: nothing started, something started and stopped, or it is running
    # and has no window this script will accept.
    $running = @(Get-Process $window -ErrorAction SilentlyContinue)
    if ($running.Count -eq 0) {
        $others = @(Get-Process -ErrorAction SilentlyContinue |
            Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } |
            ForEach-Object { $_.ProcessName } | Sort-Object -Unique)
        $seenNames = if ($others) { $others -join ', ' } else { 'none' }
        if ($verb -eq 'shell') {
            Refuse "no $window process after $Appear seconds - the shell opened $($rest[0]) with something else, or with nothing. Windows on screen belong to: $seenNames"
        }
        Refuse "no $window process after $Appear seconds - it started and stopped, or never started. Windows on screen belong to: $seenNames"
    }
    $p = $running[0]
    $p.Refresh()
    $h = $p.MainWindowHandle
    $what = if ($h -eq [IntPtr]::Zero) {
        'it reports no main window'
    } elseif (-not [Shot.Win]::IsWindowVisible($h)) {
        'its window is not visible'
    } elseif ([Shot.Win]::TitleOf($h) -eq '') {
        'its window has no title'
    } else {
        'its window never stopped changing'
    }
    Refuse "$window is running after $Appear seconds but $what, so there is nothing to photograph"
}
Write-Host "  $window had a window after $waited second(s): $([Shot.Win]::TitleOf($handle))"

# One more look before anything is done to it. Nothing has been sized or driven
# yet, so a window replaced in this gap is simply the newer one; after the
# actions it would be a refusal instead.
Start-Sleep -Milliseconds 800
$again = Get-Window
if ($again -ne [IntPtr]::Zero -and $again -ne $handle) {
    Write-Host '  it was replaced by another before anything was done to it'
    $handle = $again
}

# --- placing it -------------------------------------------------------------

# The border measured on this platform, and the two spare rows for the crop.
$BORDER_W = 14
$BORDER_H = 7
$TOPMOST = [IntPtr](-1)
$NOTOPMOST = [IntPtr](-2)

[void][Shot.Win]::SetWindowPos(
    $handle, $TOPMOST, $X, $Y, $Width + $BORDER_W, $Height + $BORDER_H + 2, 0)

# Bring it to the front and then look, rather than ask and assume. A tap of ALT
# releases the foreground lock that stops a background process raising a window;
# without it `SetForegroundWindow` returns false and the window stays where it
# was, which is how a picture of a terminal reached the store folder.
function Take-Foreground([int] $tries = 10) {
    foreach ($try in 1..$tries) {
        if ([Shot.Win]::GetForegroundWindow() -eq $handle) { return $true }
        [Shot.Win]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero)
        [Shot.Win]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
        [void][Shot.Win]::SetForegroundWindow($handle)
        Start-Sleep -Milliseconds 400
    }
    return ([Shot.Win]::GetForegroundWindow() -eq $handle)
}

if (-not (Take-Foreground)) {
    Refuse "the window would not come to the foreground - a capture now would photograph whatever is on top of it; $([Shot.Win]::WhatIsInFront()) has it"
}

# And wait for it to stop moving. Two consecutive equal reads of the frame, or
# a refusal: a rect read while a cold start is still positioning itself is a
# rect the capture will not find the window at.
$previous = $null
$still = $false
foreach ($try in 1..40) {
    $now = Get-Frame $handle
    if ($null -ne $previous -and
        $now.Left -eq $previous.Left -and $now.Top -eq $previous.Top -and
        $now.Right -eq $previous.Right -and $now.Bottom -eq $previous.Bottom) {
        $still = $true
        break
    }
    $previous = $now
    Start-Sleep -Milliseconds 250
}
if (-not $still) { Refuse 'the window never stopped moving' }

# --- driving it -------------------------------------------------------------

# The keys `key` knows by name. Anything else is a single character, which is
# its own virtual key on a US layout; a name is how the ones that are not reach
# this. Windows spells the document-wide modifier ctrl, so `cmd` is refused by
# name rather than quietly translated - a recipe carried over from the Mac
# should be read and converted, not assumed to mean the same thing.
$KEYS = @{
    'return' = 0x0D; 'enter' = 0x0D; 'tab' = 0x09; 'escape' = 0x1B; 'esc' = 0x1B
    'space' = 0x20; 'backspace' = 0x08; 'delete' = 0x2E; 'home' = 0x24; 'end' = 0x23
    'left' = 0x25; 'up' = 0x26; 'right' = 0x27; 'down' = 0x28
    'pageup' = 0x21; 'pagedown' = 0x22
    # The zoom pair, by name because the characters cannot be given: `+` is not
    # a virtual key, `ctrl++` splits into an empty key, and the key that carries
    # both is OEM_PLUS. `key ctrl+plus` four times is about 140%, which is what
    # a Store listing wants - the Store renders screenshots small, and egui's
    # default scale puts this fleet's text at around ten pixels in a thumbnail.
    # It is egui's own shortcut, so it does nothing a person could not do, and
    # eframe's persistence is off across the fleet so the next launch is at 100%.
    'plus' = 0xBB; 'minus' = 0xBD
}
$MODIFIERS = @{ 'ctrl' = 0x11; 'control' = 0x11; 'alt' = 0x12; 'shift' = 0x10; 'win' = 0x5B }
$KEYUP = 2

function Send-Key([string] $name) {
    $parts = $name.ToLower() -split '\+'
    $key = $parts[-1]
    # Sliced with Select-Object rather than with a range: `0..-1` in PowerShell
    # counts down and yields 0 then -1, so an unmodified key would be read as
    # its own modifier twice over.
    $mods = @()
    foreach ($m in @($parts | Select-Object -SkipLast 1)) {
        if ($m -eq 'cmd' -or $m -eq 'command') {
            Refuse "key '$name': Windows has no cmd - the modifier here is ctrl, and a recipe carried over from the Mac wants reading rather than translating"
        }
        if (-not $MODIFIERS.ContainsKey($m)) { Refuse "key '$name': no modifier called '$m'" }
        $mods += $MODIFIERS[$m]
    }
    if ($KEYS.ContainsKey($key)) {
        $vk = $KEYS[$key]
    } elseif ($key.Length -eq 1) {
        $vk = [int][char] $key.ToUpper()
    } else {
        Refuse "key '$name': no key called '$key', and it is not a single character"
    }
    foreach ($m in $mods) { [Shot.Win]::keybd_event($m, 0, 0, [UIntPtr]::Zero) }
    [Shot.Win]::keybd_event($vk, 0, 0, [UIntPtr]::Zero)
    [Shot.Win]::keybd_event($vk, 0, $KEYUP, [UIntPtr]::Zero)
    # Released in reverse, and always: a modifier left down is read by every
    # keystroke after it, so one stuck ctrl turns the text that follows into a
    # run of shortcuts that type nothing and may do something else entirely.
    [array]::Reverse($mods)
    foreach ($m in $mods) { [Shot.Win]::keybd_event($m, 0, $KEYUP, [UIntPtr]::Zero) }
}

# A press is a move, then a down and an up a moment apart, which is what egui
# reads as a click. A double press is that twice, with the gap between the two
# comfortably inside the system's double-click interval - that interval is what
# winit measures to tell a second click from a first, and below it a block of a
# document opens for typing rather than merely being selected again.
$interval = [Shot.Win]::GetDoubleClickTime()
$MOUSEDOWN = 0x02
$MOUSEUP = 0x04

function Press-At([int] $x, [int] $y, [int] $times) {
    $frame = Get-Frame $handle
    [void][Shot.Win]::SetCursorPos(($frame.Left + $x), ($frame.Top + 2 + $y))
    Start-Sleep -Milliseconds 300
    for ($n = 1; $n -le $times; $n++) {
        [Shot.Win]::mouse_event($MOUSEDOWN, 0, 0, 0, [UIntPtr]::Zero)
        Start-Sleep -Milliseconds 40
        [Shot.Win]::mouse_event($MOUSEUP, 0, 0, 0, [UIntPtr]::Zero)
        if ($n -lt $times) { Start-Sleep -Milliseconds ([int] ($interval / 4)) }
    }
    Start-Sleep -Milliseconds 700
}

function Get-Point([string] $verb, [string] $argument) {
    $n = @($argument -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
    if ($n.Count -ne 2) { Refuse "$verb wants X,Y and was given '$argument'" }
    foreach ($v in $n) {
        if ($v -notmatch '^-?\d+$') { Refuse "$verb wants two whole numbers and was given '$argument'" }
    }
    return @([int] $n[0], [int] $n[1])
}

# The actions, in the coordinates of the picture this writes. The window is in
# front and settled by now, which matters: the first click on an inactive window
# activates it and is swallowed, so a click sent any earlier would land nowhere
# and leave a frame that shows none of what it was for.
foreach ($action in $Do) {
    $verb, $argument = $action -split ' ', 2
    switch ($verb) {
        'click' {
            $p = Get-Point 'click' $argument
            Press-At $p[0] $p[1] 1
            Write-Host "  clicked $($p[0]),$($p[1])"
        }
        'double' {
            $p = Get-Point 'double' $argument
            Press-At $p[0] $p[1] 2
            Write-Host "  double-clicked $($p[0]),$($p[1])"
        }
        'type' {
            if ($null -eq $argument) { Refuse 'type wants text' }
            [Shot.Win]::TypeText($argument)
            Start-Sleep -Milliseconds 300
            Write-Host "  typed $argument"
        }
        'key' {
            if ($null -eq $argument) { Refuse 'key wants a name' }
            Send-Key $argument
            Start-Sleep -Milliseconds 400
            Write-Host "  key $argument"
        }
        default {
            Refuse "no action called '$verb' - the four are click, double, type and key, each with its argument after a space, as in 'click 423,40'"
        }
    }
}

# --- photographing it -------------------------------------------------------

# The pointer goes somewhere the window is not, because egui draws hover state
# and the capture keeps it. Measured on slipcase-desktop 2026-08-29: a retake
# landed with the mouse resting over a field, which came out highlighted and
# focus-ringed in a picture meant to show the application at rest, and with the
# scroll bar drawn because the pointer was inside the scroll area. Neither is
# wrong and both are noise a shopper reads as an interface doing something.
#
# Bottom right of the virtual screen rather than a constant: the window is
# placed near the top left, and a fixed 1900x1200 is off-screen on a smaller
# display, where Windows clamps it to an edge the window might occupy.
$away = [System.Windows.Forms.SystemInformation]::VirtualScreen
[System.Windows.Forms.Cursor]::Position =
    New-Object System.Drawing.Point(($away.Right - 2), ($away.Bottom - 2))

# Long enough for the window to settle and repaint at its new size. egui draws
# on demand, and a capture taken during the resize catches a half-laid-out frame.
Start-Sleep -Seconds $Settle

# Losing the foreground between the last action and the shutter is taken back
# rather than refused on. A machine that runs its own errands - a shell window
# raised behind, a scheduled task, an image's own housekeeping - takes it for a
# moment and gives it up again, and a set of four that dies on the first is
# worse than a set that says what interrupted it. The guarantee is unchanged:
# the capture happens only with the window verified in front.
if ([Shot.Win]::GetForegroundWindow() -ne $handle) {
    $thief = [Shot.Win]::WhatIsInFront()
    if (-not (Take-Foreground)) {
        Refuse "the window lost the foreground between settling and the capture; $thief has it"
    }
    Write-Host "  $thief took the foreground and it was taken back"
}

# The window is read once more, because everything above was done to a handle
# and the capture is of a rectangle. An application that replaced its window
# between the first click and here has taken the actions with it, and the
# rectangle now holds whatever is at those coordinates - which came back once
# as a third of the desktop wallpaper with the real window offset inside it,
# and was reported as a success.
$current = Get-Window
if ($current -ne [IntPtr]::Zero -and $current -ne $handle) {
    Refuse "the window was replaced between the actions and the capture: $([Shot.Win]::TitleOf($handle)) became $([Shot.Win]::TitleOf($current)), and what was driven is not what would be photographed"
}

$rect = Get-Frame $handle
$frameW = $rect.Right - $rect.Left
$frameH = $rect.Bottom - $rect.Top
if ($frameW -lt $Width -or $frameH -lt $Height + 2) {
    Refuse "the visible frame came back ${frameW}x${frameH}, which cannot yield ${Width}x${Height} - either the desktop is too small to hold a window this size, or the resize border is not what this script measured"
}

New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Out) | Out-Null

$full = New-Object System.Drawing.Bitmap($frameW, $frameH)
$graphics = [System.Drawing.Graphics]::FromImage($full)
$graphics.CopyFromScreen(
    $rect.Left, $rect.Top, 0, 0, (New-Object System.Drawing.Size($frameW, $frameH)))
$graphics.Dispose()

$shot = $full.Clone(
    (New-Object System.Drawing.Rectangle(0, 2, $Width, $Height)), $full.PixelFormat)
$shot.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png)
$shot.Dispose()
$full.Dispose()

[void][Shot.Win]::SetWindowPos($handle, $NOTOPMOST, 0, 0, 0, 0, 0x0003)

# Read back rather than trusted. A screenshot of the wrong size is refused at
# upload, and this is the one property of it a machine can check.
$written = [System.Drawing.Image]::FromFile((Resolve-Path $Out).Path)
$got = "$($written.Width)x$($written.Height)"
$written.Dispose()
if ($got -ne "${Width}x${Height}") {
    Refuse "wrote $got and the Store was asked for ${Width}x${Height}"
}
Write-Host "wrote $Out - $got"
Write-Host 'look at it before it goes anywhere: a correct size is not a good screenshot'