# The Microsoft Store screenshots, as recipes rather than as prose.
#
# `take-shots.ps1` beside this takes a set given the recipes for it, and
# `screenshot.ps1` under that is the driver and knows nothing about Filebase.
# Both are generated by ship and the same in every repository with a window to
# photograph. This file is the part that is Filebase's - which document, which
# shot, and what has to happen in the window before the shutter. It is the
# Windows spelling of `packaging/macos/shots.sh`, which is the same split.
#
# Everything a person would change to retake the set lives at the top, and
# running it by hand on the Windows machine is the same command CI runs:
#
# powershell -ExecutionPolicy Bypass -File packaging\windows\shots.ps1
#
# WHAT THE SET HAS TO SHOW, AND WHY
#
# Apple rejected segler's first four under guideline 2.3.3 - *the Mac
# screenshots do not show the actual app in use in the majority of the
# screenshots* - because nothing was selected in any of them, so its element
# pane read *select an element* in all four. A frame of an editor with nothing
# selected is a frame of a viewer, and the Microsoft reviewer reads a listing
# the same way even though the guideline is Apple's.
#
# The same trap here wears different clothes: **a frame of Filebase with no
# query run is a frame of an empty window.** The first thing this application
# draws is a folder bar, an empty table and *Choose a folder, then run a
# query*. So every shot below has a query answered and a row selected, and two
# of them show something a viewer could not do - the notices under the count,
# and a container's whole description beside the table.
#
# The coordinates are measured from the frame's top-left corner at the size
# declared here. Change the size and they all move; that is why the size is a
# constant beside them rather than an argument with a default.
#
# WHAT THE ASSOCIATION MEANS FOR WHAT IS PHOTOGRAPHED
#
# The executable is launched directly with the folder as an argument, so what
# appears is the build in `target\release` and not whatever the shell might
# have registered. Nothing is registered to this application at all: it opens a
# folder, and Windows has no association for that.
#
# Author: David M. Anderson
# Built with AI assistance (Claude, Anthropic)
[CmdletBinding()]
param(
# One plain capture of the document at rest, and nothing else. This is how
# the coordinates below get measured: take it, open it, read the pixel of
# each control off it, and fill the constants in.
[switch] $Reference,
[string] $OutDir,
# Which language's set to take, and the subdirectory it lands in:
# `Take-Shots` turns this into `en-US` or `de-DE`, and the Store files a
# frame by the locale in its path. A set written loose is a set `ship`
# refuses.
#
# Refusing a language this file has no set for, rather than falling back,
# is deliberate: the alternative is a German listing showing an English
# window, which is what happened to Slipcase Desktop's German customers.
[ValidateSet('en', 'de')]
[string] $Lang = 'en'
)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\take-shots.ps1"
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
# --- the configuration ------------------------------------------------------
# 1366x768 is the Microsoft Store's minimum for a desktop screenshot, and a
# window this size looks like a window rather than like an advertisement.
$WIDTH = 1366
$HEIGHT = 768
# The folder the window opens on, built by `packaging/demo-corpus.sh`. A folder
# rather than a document, because that is what this application opens; the
# driver passes it as an argument, since Filebase declares no file type for the
# shell to route.
# Two corpora, because this application's frames are almost entirely a
# container's *flyleaf*: a German window over English titles, owners and tags would be a
# German frame of mostly English pixels. `packaging/demo-corpus.sh` builds both
# and says why the German file names sort where the English ones do, which is
# what lets the row coordinates below serve either set.
$CORPUS = if ($Lang -eq 'de') {
Join-Path $root 'dist\corpus-de'
} else {
Join-Path $root 'dist\corpus'
}
# The executable, launched directly with the folder as an argument rather than
# through the shell. The siblings use `-Launch shell` because their document's
# extension is registered to them; nothing is registered to this one, because it
# opens a folder and Windows has no way to declare that. `-Launch exe` is the
# driver's mode for exactly this, and odox uses it for the same reason.
$EXE = Join-Path $root 'target\release\filebase.exe'
# The process the window belongs to, which the driver stops first so the frame
# holds this run's window and not a previous one's.
$PROCESS = 'filebase'
# Where the shots land. Not committed: dist is where every built artefact goes.
if (-not $OutDir) { $OutDir = Join-Path $root 'dist\screenshots' }
# The controls, by what they do rather than by where they are, so a recipe below
# reads as the thing it is doing. Each is "X,Y" in the frame.
#
# **Measured off a reference frame**, taken by dispatching `windows.yml` with
# `screenshots: reference` at exactly the size declared above. Retake and
# re-measure when the window changes rather than adjusting a number until a shot
# looks right.
#
# These are Windows' own numbers and are one pixel off the macOS set in places:
# the title bar is a different height, so everything below it shifts. That is
# why the two recipes carry their own coordinates rather than sharing a table.
# **The tick box moves with the language, and nothing else here does.** It sits
# just right of the folder button, whose German label is about
# seventeen pixels wider, so the English coordinate lands *inside the button* in
# a German window: measured on 2026-09-21, the button spans 9-116 in German and
# 9-99 in English, and the box 125-138 against 108-121. The first German set
# opened a file chooser with that click, which then swallowed the query and the
# selection - and the frames came back the right size, with a row in them, and
# `Eine Zeile auswaehlen.` in the pane. That is the shape of this defect: it
# does not fail, it photographs the wrong window.
#
# The query box, the first row and the richest row need no such treatment. The
# box is wide enough that 400 is inside it in both, and the rows are below both
# bars at a height the language does not change - `demo-corpus.sh` says why the
# German containers sort into the same five positions.
$RECURSIVE = if ($Lang -eq 'de') { '131,44' } else { '114,44' }
$QUERY_BOX = '400,78' # anywhere inside the query box
$FIRST_ROW = '115,136' # 2025\invoice-1183.pdf.slpc, the first row the scan finds
$RICHEST_ROW = '115,220' # master-services-agreement.pdf.slpc, the fullest description
# The two queries the frames are of. Row order is the scan's and was read off a
# real run: invoice, field-notes, q3-report, renewal, master-services-agreement.
# field-notes is the one with no governance.owner, so its empty cell sits in the
# middle of every frame below.
$QUERY = 'select @path, title, status, governance.owner'
$QUERY_NOTICES = 'select @path, title, pages where pages > 10'
# --- the shots --------------------------------------------------------------
# One shot to a line: name, then the actions that put the window into the state
# being photographed, in the order they are given.
#
# click X,Y press a control
# double X,Y press it twice inside the double-click interval, which is
# how a block in the document pane opens for typing
# type TEXT type
# key NAME one key, optionally with modifiers: ctrl+a, return
function Get-Shots {
# The answer to a question, with a row selected: five rows where the tick
# box was the only thing touched, a column per flyleaf key, and the
# container on the right. This frame has to carry the listing, so it is
# first. A frame of Filebase with no query run is a frame of an empty
# window, which is this application's version of the trap Apple rejected
# segler's first set for.
Shot '01-a-folder-answering' @(
"click $RECURSIVE",
"click $QUERY_BOX", 'key ctrl+a', "type $QUERY", 'key return',
"click $FIRST_ROW"
)
# A container in full: the content file card and the whole description as a
# tree. The richest of the five is selected so the pane is not half empty,
# and field-notes' blank governance.owner is above it in the same frame.
Shot '02-a-container-in-full' @(
"click $RECURSIVE",
"click $QUERY_BOX", 'key ctrl+a', "type $QUERY", 'key return',
"click $RICHEST_ROW"
)
# What the scan could not do, under the count: a file it could not read and
# a comparison that crossed types, from a query producing both at once. The
# fold is left shut, because opening it wants a coordinate that only exists
# once notices are on screen and this file carries no guessed ones.
Shot '03-what-the-scan-noticed' @(
"click $RECURSIVE",
"click $QUERY_BOX", 'key ctrl+a', "type $QUERY_NOTICES", 'key return',
"click $FIRST_ROW"
)
}
# Where the corpus is copied to before it is photographed, and why it is copied
# at all: the folder bar shows the path it was given, and a store screenshot
# reading D:\a\filebase\filebase\dist\corpus tells a customer about a build
# machine. Under Documents it reads like somebody's own folder. `packaging/macos/shots.sh`
# stages for the same reason and into the same-looking place.
#
# Copied as a tree, not as a flat list of files: the corpus is three levels
# deep, which is the whole point of the recursive tick box.
# The folder's own name is translated too. It is the widest thing in the folder
# bar and the first thing read in the frame.
#
# **The German name is built from a code point and this file is pure ASCII.**
# Windows PowerShell reads a `.ps1` as ANSI unless it carries a byte-order
# mark, so a UTF-8 umlaut written as a literal here reaches the folder bar as
# two wrong characters - measured on 2026-09-21 in a German set that was
# correct in every other respect, which is the point: it photographs, it does
# not fail. slipcase-desktop and segler keep their recipes ASCII for this
# reason and this file now does too.
$A_UMLAUT = [char]0x00E4
$STAGED = if ($Lang -eq 'de') {
Join-Path $env:USERPROFILE ("Documents\Vertr" + $A_UMLAUT + "ge")
} else {
Join-Path $env:USERPROFILE 'Documents\Contracts'
}
if (-not (Test-Path $CORPUS)) {
throw "shots.ps1: no corpus at $CORPUS; run packaging/demo-corpus.sh"
}
if (Test-Path $STAGED) { Remove-Item -Recurse -Force $STAGED }
New-Item -ItemType Directory -Force -Path (Split-Path $STAGED) | Out-Null
Copy-Item -Recurse -Force $CORPUS $STAGED
Take-Shots -Launch @('exe', $EXE, $STAGED) -Process $PROCESS `
-Width $WIDTH -Height $HEIGHT -OutDir $OutDir -Lang $Lang -Reference:$Reference