diff --git a/.gitattributes b/.gitattributes index c97ccaa18e4..20fb2465a40 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,11 +10,13 @@ *.swift text eol=lf *.py text eol=lf *.sh text eol=lf +*.ps1 text eol=lf *.glsl text eol=lf *.blp text eol=lf # Config/build files - always LF .gitattributes text eol=lf +flake.lock text eol=lf *.zon text eol=lf *.nix text eol=lf *.md text eol=lf diff --git a/.github/actions/release-preflight/action.yml b/.github/actions/release-preflight/action.yml new file mode 100644 index 00000000000..54e525a48ac --- /dev/null +++ b/.github/actions/release-preflight/action.yml @@ -0,0 +1,46 @@ +name: Release preflight +description: Validate release copy, signing, and package-manager configuration. + +inputs: + version: + description: Release version without a leading v. + required: true + prerelease: + description: Whether stable release-copy validation may be skipped. + required: true + require-package-managers: + description: Whether WinGet and Scoop configuration is required. + required: true + github-token: + description: GitHub token used for release configuration probes. + required: true + scoop-bucket-token: + description: Scoop bucket token whose presence is validated by preflight. + required: true + wingetcreate-token: + description: WinGetCreate token whose presence is validated by preflight. + required: true + +runs: + using: composite + steps: + - name: Validate release configuration + shell: pwsh + env: + GH_TOKEN: ${{ inputs.github-token }} + SCOOP_BUCKET_TOKEN: ${{ inputs.scoop-bucket-token }} + WINGETCREATE_TOKEN: ${{ inputs.wingetcreate-token }} + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_PRERELEASE: ${{ inputs.prerelease }} + REQUIRE_PACKAGE_MANAGERS: ${{ inputs.require-package-managers }} + run: | + $preflightArgs = @{ + Version = $env:RELEASE_VERSION + RequireSigning = $true + RequirePackageManagers = $env:REQUIRE_PACKAGE_MANAGERS -eq 'true' + } + if ($env:RELEASE_PRERELEASE -ne 'true') { + ./scripts/check-release-copy.ps1 -ExpectedVersion $env:RELEASE_VERSION + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + ./scripts/release-preflight.ps1 @preflightArgs diff --git a/.github/workflows/release-readiness.yml b/.github/workflows/release-readiness.yml index f55578c99e3..4b38875c729 100644 --- a/.github/workflows/release-readiness.yml +++ b/.github/workflows/release-readiness.yml @@ -27,12 +27,9 @@ jobs: SCOOP_BUCKET_REPO: ${{ vars.SCOOP_BUCKET_REPO }} SCOOP_BUCKET_BRANCH: ${{ vars.SCOOP_BUCKET_BRANCH }} SCOOP_BUCKET_MANIFEST_PATH: ${{ vars.SCOOP_BUCKET_MANIFEST_PATH }} - WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} - WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }} WINDOWS_CODESIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_CODESIGN_TIMESTAMP_URL }} + WINDOWS_CODESIGN_TRUST_SELF_SIGNED: ${{ vars.WINDOWS_CODESIGN_TRUST_SELF_SIGNED }} WINDOWS_CODESIGN_MIN_VALIDITY_DAYS: ${{ vars.WINDOWS_CODESIGN_MIN_VALIDITY_DAYS }} - SCOOP_BUCKET_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }} - WINGETCREATE_TOKEN: ${{ secrets.WINGETCREATE_TOKEN }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -41,17 +38,14 @@ jobs: persist-credentials: false - name: Validate release configuration + uses: ./.github/actions/release-preflight env: - GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ inputs.version }} - REQUIRE_PACKAGE_MANAGERS: ${{ inputs.require_package_managers }} - shell: pwsh - run: | - $scriptArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $env:REQUIRE_PACKAGE_MANAGERS -eq 'true' - } - ./scripts/check-release-copy.ps1 -ExpectedVersion $env:RELEASE_VERSION - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - ./scripts/release-preflight.ps1 @scriptArgs + WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} + WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }} + with: + version: ${{ inputs.version }} + prerelease: "false" + require-package-managers: ${{ inputs.require_package_managers }} + github-token: ${{ github.token }} + scoop-bucket-token: ${{ secrets.SCOOP_BUCKET_TOKEN }} + wingetcreate-token: ${{ secrets.WINGETCREATE_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 354e5541ba9..c95eb138779 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,13 +30,10 @@ jobs: runs-on: windows-latest environment: release env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" WINGET_PACKAGE_IDENTIFIER: ${{ vars.WINGET_PACKAGE_IDENTIFIER }} SCOOP_BUCKET_REPO: ${{ vars.SCOOP_BUCKET_REPO }} SCOOP_BUCKET_BRANCH: ${{ vars.SCOOP_BUCKET_BRANCH }} SCOOP_BUCKET_MANIFEST_PATH: ${{ vars.SCOOP_BUCKET_MANIFEST_PATH }} - WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} - WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }} WINDOWS_CODESIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_CODESIGN_TIMESTAMP_URL }} WINDOWS_CODESIGN_TRUST_SELF_SIGNED: ${{ vars.WINDOWS_CODESIGN_TRUST_SELF_SIGNED }} WINDOWS_CODESIGN_MIN_VALIDITY_DAYS: ${{ vars.WINDOWS_CODESIGN_MIN_VALIDITY_DAYS }} @@ -46,6 +43,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + persist-credentials: false - name: Require remote default-branch head env: @@ -210,24 +208,17 @@ jobs: "first_fork_patch=$firstForkPatch" >> $env:GITHUB_OUTPUT - name: Release preflight + uses: ./.github/actions/release-preflight env: - GH_TOKEN: ${{ github.token }} - SCOOP_BUCKET_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }} - WINGETCREATE_TOKEN: ${{ secrets.WINGETCREATE_TOKEN }} - RELEASE_PRERELEASE: ${{ steps.meta.outputs.prerelease }} - RELEASE_VERSION: ${{ steps.meta.outputs.version }} - shell: pwsh - run: | - $preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true - } - if ($env:RELEASE_PRERELEASE -ne 'true') { - ./scripts/check-release-copy.ps1 -ExpectedVersion $env:RELEASE_VERSION - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - } - ./scripts/release-preflight.ps1 @preflightArgs + WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} + WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }} + with: + version: ${{ steps.meta.outputs.version }} + prerelease: ${{ steps.meta.outputs.prerelease }} + require-package-managers: "true" + github-token: ${{ github.token }} + scoop-bucket-token: ${{ secrets.SCOOP_BUCKET_TOKEN }} + wingetcreate-token: ${{ secrets.WINGETCREATE_TOKEN }} - name: Setup Zig uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 @@ -250,9 +241,13 @@ jobs: - name: Build and package x64 release artifacts shell: pwsh timeout-minutes: 35 + env: + RELEASE_VERSION: ${{ steps.meta.outputs.version }} + WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} + WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }} run: > ./scripts/build-package-windows.ps1 - -Version ${{ steps.meta.outputs.version }} + -Version $env:RELEASE_VERSION -Architecture x64 -Optimize ReleaseFast -RequireInstaller @@ -262,10 +257,13 @@ jobs: shell: pwsh timeout-minutes: 35 env: + RELEASE_VERSION: ${{ steps.meta.outputs.version }} + WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} + WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }} ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}\.zig-global-cache run: > ./scripts/build-package-windows.ps1 - -Version ${{ steps.meta.outputs.version }} + -Version $env:RELEASE_VERSION -Architecture arm64 -Optimize ReleaseFast -RequireInstaller @@ -273,218 +271,53 @@ jobs: - name: Verify signed release artifacts shell: pwsh - run: | - . (Join-Path $PWD "scripts/windows-architecture.ps1") - . (Join-Path $PWD "scripts/signing-trust.ps1") - $version = "${{ steps.meta.outputs.version }}" - $trustSelfSigned = "${{ env.WINDOWS_CODESIGN_TRUST_SELF_SIGNED }}".Trim().ToLowerInvariant() -in @("true", "1", "yes", "on") - $allowedPins = @(Get-UpdaterPublisherSpkiPins -SourcePath (Join-Path $PWD "src/update/github_releases.zig")) - if ($allowedPins.Count -eq 0) { throw "Updater publisher-pin allowlist is empty." } - - function Assert-ReleaseSignature { - param( - [string]$Path, - [string]$Label - ) - - $sig = Get-AuthenticodeSignature -LiteralPath $Path - $statusAccepted = $sig.Status -eq [System.Management.Automation.SignatureStatus]::Valid - if (-not $statusAccepted -and $trustSelfSigned -and - $sig.SignerCertificate -and - $sig.SignerCertificate.Subject -eq $sig.SignerCertificate.Issuer -and - (Test-SelfSignedTrustStatus -Signature $sig -Path $Path)) { - $statusAccepted = $true - } - if (-not $statusAccepted) { - throw "$Label Authenticode signature is not valid: $($sig.Status) $($sig.StatusMessage)" - } - if (-not $sig.SignerCertificate) { - throw "$Label has no Authenticode signer certificate: $Path" - } - $pin = Get-CertificateSpkiSha256 -Certificate $sig.SignerCertificate - if ($allowedPins -notcontains $pin) { - throw "$Label signer SPKI SHA-256 is not allowed: $pin" - } - } - - foreach ($arch in (Get-WindowsPackageArchitectures)) { - $artifactDir = Join-Path $PWD "dist/artifacts/noctty-$version-windows-$arch" - $setup = Join-Path $artifactDir (New-WindowsPackageArtifactName -Version $version -Architecture $arch -Kind setup) - $zip = Join-Path $artifactDir (New-WindowsPackageArtifactName -Version $version -Architecture $arch -Kind portable) - $checksums = Join-Path $artifactDir (New-WindowsPackageArtifactName -Version $version -Architecture $arch -Kind checksums) - foreach ($path in @($setup, $zip, $checksums)) { - if (-not (Test-Path -LiteralPath $path)) { - throw "Missing required release asset: $path" - } - } - - $checksumText = Get-Content -LiteralPath $checksums - foreach ($path in @($setup, $zip)) { - $name = [System.IO.Path]::GetFileName($path) - $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $path).Hash.ToLowerInvariant() - if ($checksumText -notcontains "$hash *$name") { - throw "$([System.IO.Path]::GetFileName($checksums)) does not contain the expected hash line for $name." - } - } - - Assert-ReleaseSignature -Path $setup -Label "Setup $arch" - - $extractDir = Join-Path $env:RUNNER_TEMP "noctty-release-verify-$arch" - if (Test-Path -LiteralPath $extractDir) { - Remove-Item -LiteralPath $extractDir -Recurse -Force - } - Expand-Archive -LiteralPath $zip -DestinationPath $extractDir - foreach ($relative in @("noctty/noctty.com", "noctty/noctty.exe", "noctty/ghostty-vt.dll")) { - $path = Join-Path $extractDir $relative - Assert-ReleaseSignature -Path $path -Label "$relative $arch" - } - } + env: + RELEASE_VERSION: ${{ steps.meta.outputs.version }} + run: ./scripts/release-verify-artifacts.ps1 -Version $env:RELEASE_VERSION - name: Scan Windows release artifacts with Microsoft Defender shell: pwsh timeout-minutes: 10 - run: | - . (Join-Path $PWD "scripts/windows-architecture.ps1") - $status = Get-MpComputerStatus -ErrorAction Stop - if (-not $status.AMServiceEnabled -or - -not $status.AntivirusEnabled -or - $status.AMRunningMode -ne "Normal") { - throw "Microsoft Defender Antivirus must be active in Normal mode before release publication." - } - - $scannerCandidates = @( - (Join-Path $env:ProgramFiles "Windows Defender/MpCmdRun.exe") - ) - $platformRoot = Join-Path $env:ProgramData "Microsoft/Windows Defender/Platform" - if (Test-Path -LiteralPath $platformRoot) { - $scannerCandidates = @( - Get-ChildItem -LiteralPath $platformRoot -Directory | - ForEach-Object { - $platformVersion = ($_.Name -replace '-\d+$', '') -as [version] - if ($null -ne $platformVersion) { - [pscustomobject]@{ - Version = $platformVersion - Path = Join-Path $_.FullName "MpCmdRun.exe" - } - } - } | - Sort-Object Version -Descending | - ForEach-Object { $_.Path } - ) + $scannerCandidates - } - $scanner = $scannerCandidates | - Where-Object { Test-Path -LiteralPath $_ } | - Select-Object -First 1 - if (-not $scanner) { - throw "MpCmdRun.exe was not found." - } - - & $scanner -SignatureUpdate - if ($LASTEXITCODE -ne 0) { - throw "Microsoft Defender signature update failed with exit code $LASTEXITCODE." - } - - $version = "${{ steps.meta.outputs.version }}" - $portablePayloads = @( - "noctty/noctty.com", - "noctty/noctty.exe", - "noctty/ghostty-vt.dll" - ) - $scanPaths = @( - foreach ($arch in (Get-WindowsPackageArchitectures)) { - $artifactDir = Join-Path $PWD "dist/artifacts/noctty-$version-windows-$arch" - $setupName = New-WindowsPackageArtifactName -Version $version -Architecture $arch -Kind setup - $setupPath = Join-Path $artifactDir $setupName - if (-not (Test-Path -LiteralPath $setupPath)) { - throw "Missing Defender scan input: $setupPath" - } - $setupPath - - $extractDir = Join-Path $env:RUNNER_TEMP "noctty-release-verify-$arch" - foreach ($relative in $portablePayloads) { - $payloadPath = Join-Path $extractDir $relative - if (-not (Test-Path -LiteralPath $payloadPath)) { - throw "Missing Defender scan input: $payloadPath" - } - $payloadPath - } - } - ) - if ($scanPaths.Count -ne 8) { - throw "Expected exactly eight release artifacts for Defender scanning; found $($scanPaths.Count)." - } - - foreach ($scanPath in $scanPaths) { - & $scanner -Scan -ScanType 3 -File $scanPath -DisableRemediation -ReturnHR - if ($LASTEXITCODE -ne 0) { - throw "Microsoft Defender rejected $scanPath with exit code $LASTEXITCODE." - } - } + env: + RELEASE_VERSION: ${{ steps.meta.outputs.version }} + run: ./scripts/release-scan-defender.ps1 -Version $env:RELEASE_VERSION - name: Generate package manager metadata shell: pwsh + env: + RELEASE_VERSION: ${{ steps.meta.outputs.version }} + RELEASE_TAG: ${{ steps.meta.outputs.tag }} + RELEASE_REPOSITORY: ${{ github.repository }} + RELEASE_UPSTREAM_BASE_VERSION: ${{ steps.meta.outputs.upstream_base_version }} + RELEASE_FIRST_FORK_PATCH: ${{ steps.meta.outputs.first_fork_patch }} run: > ./scripts/package-package-managers.ps1 - -Version ${{ steps.meta.outputs.version }} - -Tag ${{ steps.meta.outputs.tag }} - -Repo ${{ github.repository }} + -Version $env:RELEASE_VERSION + -Tag $env:RELEASE_TAG + -Repo $env:RELEASE_REPOSITORY -Architectures @("x64", "arm64") - -UpstreamBaseVersion ${{ steps.meta.outputs.upstream_base_version }} - -FirstForkPatch ${{ steps.meta.outputs.first_fork_patch }} + -UpstreamBaseVersion $env:RELEASE_UPSTREAM_BASE_VERSION + -FirstForkPatch $env:RELEASE_FIRST_FORK_PATCH - name: Publish GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ steps.meta.outputs.tag }} + RELEASE_VERSION: ${{ steps.meta.outputs.version }} + RELEASE_TITLE: ${{ steps.meta.outputs.title }} + RELEASE_VERSION_LINE: ${{ steps.meta.outputs.version_line }} + RELEASE_UPSTREAM_BASE_VERSION: ${{ steps.meta.outputs.upstream_base_version }} + RELEASE_PRERELEASE: ${{ steps.meta.outputs.prerelease }} shell: pwsh run: | - . (Join-Path $PWD "scripts/windows-architecture.ps1") - $tag = "${{ steps.meta.outputs.tag }}" - $version = "${{ steps.meta.outputs.version }}" - $title = "${{ steps.meta.outputs.title }}" - $versionLine = "${{ steps.meta.outputs.version_line }}" - $upstreamBaseVersion = "${{ steps.meta.outputs.upstream_base_version }}" - $architectures = Get-WindowsPackageArchitectures - $artifactDirs = @{} - foreach ($arch in $architectures) { - $artifactDirs[$arch] = Join-Path $PWD "dist/artifacts/noctty-$version-windows-$arch" - } - $artifactDirX64 = $artifactDirs["x64"] - $legacyChecksumsPath = Join-Path $artifactDirX64 (New-WindowsPackageArtifactName -Version $version -Architecture x64 -Kind legacy-checksums) - Copy-Item -LiteralPath (Join-Path $artifactDirX64 (New-WindowsPackageArtifactName -Version $version -Architecture x64 -Kind checksums)) -Destination $legacyChecksumsPath -Force - $assets = @() - foreach ($arch in $architectures) { - $artifactDir = $artifactDirs[$arch] - $assets += @( - (Join-Path $artifactDir (New-WindowsPackageArtifactName -Version $version -Architecture $arch -Kind setup)), - (Join-Path $artifactDir (New-WindowsPackageArtifactName -Version $version -Architecture $arch -Kind portable)), - (Join-Path $artifactDir (New-WindowsPackageArtifactName -Version $version -Architecture $arch -Kind checksums)) - ) - } - $assets += @( - $legacyChecksumsPath, - (Join-Path $artifactDirX64 "noctty-icon.svg") - ) - $notesPrefix = @" - Compatibility line: Ghostty $versionLine - Base upstream release: $upstreamBaseVersion - Versioning: major.minor follow the Ghostty upstream line; patch is the noctty release number on that line. - "@ - - & gh release view $tag --repo $env:GH_REPO *> $null - if ($LASTEXITCODE -ne 0) { - $extra = @() - if ("${{ steps.meta.outputs.prerelease }}" -eq "true") { - $extra += "--prerelease" - } - & gh release create $tag --repo $env:GH_REPO --title $title --generate-notes --notes $notesPrefix @extra @assets - if ($LASTEXITCODE -ne 0) { throw "Failed to create GitHub release $tag." } - } else { - & gh release edit $tag --repo $env:GH_REPO --title $title - if ($LASTEXITCODE -ne 0) { throw "Failed to edit GitHub release $tag." } - & gh release upload $tag --repo $env:GH_REPO @assets --clobber - if ($LASTEXITCODE -ne 0) { throw "Failed to upload assets for GitHub release $tag." } - } + ./scripts/release-publish-github.ps1 ` + -Tag $env:RELEASE_TAG ` + -Version $env:RELEASE_VERSION ` + -Title $env:RELEASE_TITLE ` + -VersionLine $env:RELEASE_VERSION_LINE ` + -UpstreamBaseVersion $env:RELEASE_UPSTREAM_BASE_VERSION ` + -Prerelease $env:RELEASE_PRERELEASE - name: Verify published release copy and assets if: steps.meta.outputs.prerelease != 'true' @@ -499,190 +332,19 @@ jobs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Publish Scoop manifest + if: steps.meta.outputs.prerelease != 'true' shell: pwsh + timeout-minutes: 10 env: GH_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }} - run: | - if (-not $env:GH_TOKEN) { - Write-Host "Skipping Scoop publish: SCOOP_BUCKET_TOKEN is not configured." - return - } - - if (-not $env:SCOOP_BUCKET_REPO) { - Write-Host "Skipping Scoop publish: SCOOP_BUCKET_REPO is not configured." - return - } - - $version = "${{ steps.meta.outputs.version }}" - $artifactDirX64 = Join-Path $PWD "dist/artifacts/noctty-$version-windows-x64" - $metadataPath = Join-Path $artifactDirX64 "package-managers/metadata.json" - $metadata = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json - $manifestRelPath = if ($env:SCOOP_BUCKET_MANIFEST_PATH) { - $env:SCOOP_BUCKET_MANIFEST_PATH - } else { - "bucket/noctty.json" - } - - $bucketDir = Join-Path $env:RUNNER_TEMP "noctty-scoop-bucket" - if (Test-Path -LiteralPath $bucketDir) { - Remove-Item -LiteralPath $bucketDir -Recurse -Force - } - - $cloneArgs = @("repo", "clone", $env:SCOOP_BUCKET_REPO, $bucketDir, "--", "--depth", "1") - if ($env:SCOOP_BUCKET_BRANCH) { - $cloneArgs += @("--branch", $env:SCOOP_BUCKET_BRANCH) - } - & gh @cloneArgs - if ($LASTEXITCODE -ne 0) { - throw "Failed to clone Scoop bucket repo $($env:SCOOP_BUCKET_REPO)." - } - - $destManifestPath = Join-Path $bucketDir $manifestRelPath - $destManifestDir = Split-Path -Parent $destManifestPath - New-Item -ItemType Directory -Path $destManifestDir -Force | Out-Null - Copy-Item -LiteralPath $metadata.scoop.manifestPath -Destination $destManifestPath -Force - - Push-Location $bucketDir - try { - & git remote set-url origin "https://x-access-token:$env:GH_TOKEN@github.com/$($env:SCOOP_BUCKET_REPO).git" - if ($LASTEXITCODE -ne 0) { - throw "git remote set-url failed with exit code $LASTEXITCODE" - } - - & git config user.name "github-actions[bot]" - & git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - & git add -- $manifestRelPath - & git diff --cached --quiet --exit-code - if ($LASTEXITCODE -eq 0) { - Write-Host "Scoop manifest is unchanged; skipping push." - return - } - if ($LASTEXITCODE -ne 1) { - throw "git diff --cached failed with exit code $LASTEXITCODE" - } - - & git commit -m "noctty: update to $version" - if ($LASTEXITCODE -ne 0) { - throw "git commit failed with exit code $LASTEXITCODE" - } - - & git push origin HEAD - if ($LASTEXITCODE -ne 0) { - throw "git push failed with exit code $LASTEXITCODE" - } - } - finally { - Pop-Location - } + RELEASE_VERSION: ${{ steps.meta.outputs.version }} + run: ./scripts/release-publish-scoop.ps1 -Version $env:RELEASE_VERSION - name: Submit WinGet manifest update + if: steps.meta.outputs.prerelease != 'true' shell: pwsh + timeout-minutes: 15 env: - WINGETCREATE_TOKEN: ${{ secrets.WINGETCREATE_TOKEN }} - run: | - if (-not $env:WINGETCREATE_TOKEN) { - Write-Host "Skipping WinGet submit: WINGETCREATE_TOKEN is not configured." - return - } - - if (-not $env:WINGET_PACKAGE_IDENTIFIER) { - Write-Host "Skipping WinGet submit: WINGET_PACKAGE_IDENTIFIER is not configured. Initial WinGet bootstrap is still manual." - return - } - - $version = "${{ steps.meta.outputs.version }}" - $artifactDirX64 = Join-Path $PWD "dist/artifacts/noctty-$version-windows-x64" - $metadataPath = Join-Path $artifactDirX64 "package-managers/metadata.json" - $metadata = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json - $packageIdSegments = $env:WINGET_PACKAGE_IDENTIFIER.Split('.') - $manifestPath = "manifests/{0}/{1}" -f ` - $packageIdSegments[0].Substring(0, 1).ToLowerInvariant(), ` - ($packageIdSegments -join '/') - $manifestApiUrl = "https://api.github.com/repos/microsoft/winget-pkgs/contents/$manifestPath" - - try { - Invoke-WebRequest ` - -Uri $manifestApiUrl ` - -Headers @{ "User-Agent" = "noctty-release-workflow" } ` - -ErrorAction Stop | Out-Null - } - catch { - $statusCode = if ($_.Exception.Response) { - [int]$_.Exception.Response.StatusCode - } else { - $null - } - - if ($statusCode -eq 404) { - Write-Host "Skipping WinGet submit: $($env:WINGET_PACKAGE_IDENTIFIER) is not bootstrapped in microsoft/winget-pkgs ($manifestPath)." - return - } - - throw - } - - $vcLibsPath = Join-Path $env:TEMP "Microsoft.VCLibs.x64.14.00.Desktop.appx" - $bundlePath = Join-Path $env:TEMP "wingetcreate.msixbundle" - Import-Module Appx -UseWindowsPowerShell -ErrorAction Stop - function Install-AppxPackageIfNeeded { - param( - [string]$Path, - [string]$Label - ) - - try { - Add-AppxPackage -Path $Path -ErrorAction Stop - } - catch { - $message = $_.Exception.Message - if ($message -match "0x80073D06" -or $message -match "higher version of this package is already installed") { - Write-Host "$Label already satisfied on this runner; skipping package install." - return - } - - throw - } - } - - Invoke-WebRequest -Uri "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx" -OutFile $vcLibsPath -ErrorAction Stop - Install-AppxPackageIfNeeded -Path $vcLibsPath -Label "Microsoft.VCLibs.x64.14.00.Desktop" - Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest/msixbundle" -OutFile $bundlePath -ErrorAction Stop - Install-AppxPackageIfNeeded -Path $bundlePath -Label "wingetcreate bundle" - - # wingetcreate update documents URL architecture overrides as "|". - $installerUrlArgs = @($metadata.winget.installerUrlArgs | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { [string]$_ }) - $installerArchitectures = @($installerUrlArgs | ForEach-Object { - if ($_ -cnotmatch '\|(?x64|arm64)$') { - throw "WinGet installer URL must end with an explicit |x64 or |arm64 architecture: $_" - } - $Matches.architecture - }) - $expectedInstallerArchitectures = @('arm64', 'x64') - if ($installerUrlArgs.Count -ne 2 -or - (($installerArchitectures | Sort-Object -Unique) -join ',') -ne ($expectedInstallerArchitectures -join ',')) { - throw "WinGet publishing requires exactly one x64 and one arm64 installer URL." - } - - function Invoke-WinGetCreateUpdate { - param([string[]]$InstallerUrlArgs) - - $output = & wingetcreate update $env:WINGET_PACKAGE_IDENTIFIER ` - --version $version ` - --urls $InstallerUrlArgs ` - --release-notes-url $metadata.release.releaseUrl ` - --submit ` - --no-open ` - --token $env:WINGETCREATE_TOKEN 2>&1 - $exitCode = $LASTEXITCODE - $output | ForEach-Object { Write-Host $_ } - - return @{ - ExitCode = $exitCode - Output = (@($output) -join "`n") - } - } - - $result = Invoke-WinGetCreateUpdate -InstallerUrlArgs $installerUrlArgs - if ($result.ExitCode -ne 0) { - throw "wingetcreate update failed with exit code $($result.ExitCode)" - } + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGETCREATE_TOKEN }} + RELEASE_VERSION: ${{ steps.meta.outputs.version }} + run: ./scripts/release-submit-winget.ps1 -Version $env:RELEASE_VERSION diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5e961d8258..a3833e0a6a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,8 @@ name: Test on: - push: {} + push: + branches: [main] pull_request: {} schedule: - cron: "17 6 * * *" @@ -13,6 +14,9 @@ on: default: false type: boolean +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref_name != 'main' && github.ref || github.run_id }} cancel-in-progress: true @@ -56,6 +60,11 @@ jobs: GH_TOKEN: ${{ github.token }} run: ./scripts/check-release-copy.ps1 -CheckRemoteLatest + - name: Setup Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: 22 + - name: Deterministic site asset check shell: pwsh run: | @@ -65,7 +74,7 @@ jobs: - name: Site unit tests shell: pwsh run: | - node --test site/tests/terminal.test.mjs site/tests/build-site-assets.test.mjs + node --test "site/tests/**/*.test.mjs" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Signing trust policy tests diff --git a/AGENTS.md b/AGENTS.md index 569849d0c31..8e2d7741774 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,11 @@ Windows-only Ghostty fork. ## Directory Structure - Shared Zig core: `src/` -- Win32 application runtime: `src/apprt/win32.zig` + `src/apprt/win32_theme.zig` +- Win32 application runtime: `src/apprt/win32.zig` coordinates App, Host, and + Surface behavior; `src/apprt/win32_theme.zig` owns theme policy; focused + support modules under `src/apprt/win32/` own Win32 constants/externs, + labels/input, startup/render tracing, chrome rectangle math, and GDI paint + primitives. - Windows packaging: `dist/windows/` + `scripts/package-windows.ps1` - libghostty-vt headers: `include/ghostty/` @@ -39,6 +43,7 @@ runtime. Do not reintroduce them. ## Self-Correction Log +- 2026-08-20: Correcting the 2026-04-16 palette entry below: `commandPaletteUniqueMatch` no longer powers Enter-to-run, and it has now been deleted. Provenance, since the pieces landed separately: `246e429b7` replaced the `submitOverlay` unique-match resolution with dispatch of the selected ranked row plus action MRU; `dc300a40dc` (#73) added the tab / pane / profile payload arms to `invokePaletteRow`; `3f0a928c5` (#93) added the `palette_list_visible_rows > 0` gate with its `Make the window larger to show and activate palette results.` refusal and removed the last two callers (`buildOverlayAcceptLabel` and `buildCommandPaletteOverlayLabel`); `41061f8c8` deleted `commandPaletteMatchCount` outright and relocated the by-then-orphaned unique helper, and that `commandPaletteMatchCount` absence still holds. In the current runtime Enter invokes `invokePaletteRow(@min(palette_list_selected, palette_list_ranked_count - 1))` only when there are ranked matches AND at least one visible row, and `rebuildPaletteList` resets `palette_list_selected = 0` on every keystroke, so a single-match query with a visible row runs the top-ranked entry; with ranked matches but zero visible rows Enter deliberately refuses rather than activating a selection the user cannot see; parsing the raw query text is only the `palette_list_ranked_count == 0` fallback. Do not resurrect either helper on the strength of the stale 2026-04-16 note, and when a log entry names a function as load-bearing, re-verify the call sites before treating it as live. - 2026-05-02: Lowercasing Zig type aliases to satisfy naming review can collide with same-named declarations or locals in Zig's shared declaration namespace; check for helper/local name conflicts before committing mechanical renames. - 2026-05-01: In this fork, with both `origin` and `upstream` remotes configured, bare `gh pr view ` can resolve the upstream Ghostty PR number; use `-R amanthanvi/noctty` for fork-local PR review/checks. - 2026-05-01: When invoking `scripts/dev-windows.cmd zig build test -Dtest-filter=...`, keep the filter value to one token (quote it or use a no-space substring) or `zig build` treats trailing words as step names. @@ -190,7 +195,7 @@ runtime. Do not reintroduce them. - 2026-04-15: In `scripts/package-windows.ps1`, avoid `Compress-Archive` for the portable ZIP; on Windows it can intermittently fail on staged theme files with spaces (for example `Monokai Classic`) even when staging itself is correct. - 2026-04-15: Fresh GitHub Actions checkouts cannot build or test without `src/unicode/generated_props.zig` and `src/unicode/generated_symbols.zig`; keep them versioned until the stale `uucode` generator path is repaired. - 2026-04-15: `scripts/package-windows.ps1` must build with `-Demit-lib-vt=true`; otherwise warm local trees can mask that fresh CI runners never produced `zig-out/bin/ghostty-vt.dll`. -- 2026-04-15: In this Windows-focused fork, `nix/package.nix` and `nix/libghostty-vt.nix` can silently stale when `dist/linux` is removed; salvage `libghostty-vt`, but make the GTK/Linux app derivation fail fast with an explicit unsupported message. +- 2026-04-15: In this Windows-focused fork, preserve the curated `libghostty-vt`-only Nix layout; do not reintroduce unsupported GTK/Linux app derivations. - 2026-04-16: `apprt.surface.newConfig()` returns a shallow clone; when overriding `Config.command` on that clone, never deinit the inherited command because it is still owned by the source config arena. - 2026-04-16: The Win32 chrome metric constants (`host_tab_height`, `host_overlay_height`, `host_overlay_padding`, `host_pane_divider_width`, etc.) now derive from `win32_theme.ThemeMetrics` defaults. Keep the top-level `const` aliases in place for call-site compatibility; new code should consume `ThemeMetrics` via the `Theme` aggregate. - 2026-04-16: In HC mode, `pane_divider` uses `COLOR_WINDOWFRAME` and `pane_divider_focused` uses `COLOR_HIGHLIGHT`; do not collapse both to `hi_bg` because that erases the focused/unfocused cue required by multi-pane layouts. diff --git a/HACKING.md b/HACKING.md index e21f72bc521..d7f9db19a1a 100644 --- a/HACKING.md +++ b/HACKING.md @@ -94,10 +94,13 @@ manually verify: ## Project layout (quick map) -- `src/apprt/win32.zig`: Win32 application runtime entry point (large - single file; behavior-bearing extractions are in progress). +- `src/apprt/win32.zig`: Win32 application runtime coordinator containing the + coupled App, Host, and Surface behavior. - `src/apprt/win32_theme.zig`: theme tokens, DWM integration, accent helpers, HC handling (extracted from `win32.zig` in `a759eb6`). +- `src/apprt/win32/`: focused runtime support modules for constants/externs, + labels and input, startup/render tracing, native chrome rectangle math, and + Host-independent GDI paint primitives. - `src/update/github_releases.zig`: release checks plus verified installer staging for user-initiated updates. - `src/renderer/OpenGL.zig`: WGL + OpenGL 4.3 renderer backend. diff --git a/scripts/build-package-windows.ps1 b/scripts/build-package-windows.ps1 index 686e7a694dd..4c4f0007b57 100644 --- a/scripts/build-package-windows.ps1 +++ b/scripts/build-package-windows.ps1 @@ -14,11 +14,12 @@ param( ) $ErrorActionPreference = "Stop" +. (Join-Path $PSScriptRoot "common.ps1") . (Join-Path $PSScriptRoot "windows-architecture.ps1") . (Join-Path $PSScriptRoot "windows-build-capabilities.ps1") $archInfo = Get-WindowsPackageArchitecture -Architecture $(if ($Architecture) { $Architecture } else { Get-DefaultWindowsPackageArchitecture }) -$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) +$repoRoot = Get-RepoRoot $zigOutBin = Join-Path $repoRoot "zig-out/bin" $buildCapabilitiesPath = Join-Path $zigOutBin "noctty-build-capabilities.json" $runtimeFiles = @( diff --git a/scripts/build-site-assets.mjs b/scripts/build-site-assets.mjs index ac2a9d15ef0..eecf854ce48 100644 --- a/scripts/build-site-assets.mjs +++ b/scripts/build-site-assets.mjs @@ -3,8 +3,7 @@ * Refresh the static site's integrity metadata: * - the CSP inline-script hash in site/_headers (both pages must carry a * byte-identical inline theme bootstrap, so exactly one hash is pinned) - * - the CSP event-handler hash for the font stylesheet onload attribute - * (hashed from the HTML-decoded value the browser actually executes) + * - the CSP prohibition on inline event handlers (fonts are self-hosted) * - SHA-256 cache keys (?v=...) on local asset references in both pages * * Run: node scripts/build-site-assets.mjs [--check] @@ -17,69 +16,145 @@ import { fileURLToPath, pathToFileURL } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const checkOnly = process.argv.includes("--check"); - -const sha256Hex = (text) => crypto.createHash("sha256").update(text, "utf8").digest("hex"); -const sha256Base64 = (text) => crypto.createHash("sha256").update(text, "utf8").digest("base64"); +const printHeaderContract = process.argv.includes("--print-header-contract"); +const siteDirectoryArgument = process.argv.find((argument) => + argument.startsWith("--site-directory="), +); +const siteRoot = siteDirectoryArgument + ? path.resolve(siteDirectoryArgument.slice("--site-directory=".length)) + : path.join(root, "site"); + +const sha256Hex = (text) => + crypto.createHash("sha256").update(text, "utf8").digest("hex"); +const sha256Base64 = (text) => + crypto.createHash("sha256").update(text, "utf8").digest("base64"); const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -// Browsers execute the HTML-decoded attribute value, and the header contract -// (scripts/get-site-header-contract.ps1) HtmlDecodes before hashing — so an -// encoded handler like onload="this.media='all'" must hash the same -// as its literal form. This is deliberately not a full HTML decoder: any -// reference outside the small supported set fails the build instead of -// risking a hash the browser would disagree with (named references beyond -// these five, out-of-range or surrogate code points, and the 0x80-0x9F range -// browsers remap via windows-1252 all decode differently across parsers). -const NAMED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }; - -export function decodeHtmlEntities(text) { - return text.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, body) => { - if (body[0] === "#") { - const codePoint = body[1] === "x" || body[1] === "X" - ? Number.parseInt(body.slice(2), 16) - : Number.parseInt(body.slice(1), 10); - if (Number.isNaN(codePoint) || - codePoint > 0x10ffff || - (codePoint >= 0xd800 && codePoint <= 0xdfff) || - (codePoint >= 0x80 && codePoint <= 0x9f)) { - throw new Error(`Unsupported HTML character reference "${match}"; use a literal character or one of the plain numeric forms.`); - } - return String.fromCodePoint(codePoint); - } - if (!Object.hasOwn(NAMED_ENTITIES, body)) { - throw new Error(`Unsupported HTML named reference "${match}"; only ${Object.keys(NAMED_ENTITIES).map((name) => `&${name};`).join(" ")} are recognized.`); - } - return NAMED_ENTITIES[body]; - }); -} - -function readSiteFile(relativePath) { - const text = fs.readFileSync(path.join(root, relativePath), "utf8"); +function readSiteFile(relativePath, directory = siteRoot) { + const text = fs.readFileSync(path.join(directory, relativePath), "utf8"); if (/\r/.test(text)) { - throw new Error(`${relativePath} must be LF-normalized (found CR bytes).`); + throw new Error( + `site/${relativePath} must be LF-normalized (found CR bytes).`, + ); } return text; } -function extractSingleMatch(text, pattern, relativePath, what) { - const matches = [...text.matchAll(pattern)]; - if (matches.length !== 1) { - throw new Error(`${relativePath} must contain exactly one ${what}; found ${matches.length}.`); +function getInlineScriptContract(directory = siteRoot) { + let sharedScript; + + for (const htmlName of ["index.html", "404.html"]) { + const html = readSiteFile(htmlName, directory); + const inlineScripts = [ + ...html.matchAll(/[^>]*)>(?.*?)<\/script>/gis), + ].filter((match) => !/\bsrc\s*=/.test(match.groups.attrs)); + if (inlineScripts.length !== 1) { + throw new Error( + `Expected exactly one CSP-hashed inline script in site/${htmlName}.`, + ); + } + const script = inlineScripts[0].groups.body; + + const markupWithoutScripts = html.replace( + /]*>.*?<\/script>/gis, + "", + ); + const eventAttributeCount = [ + ...markupWithoutScripts.matchAll(/\s+on[a-z][a-z0-9_-]*\s*=/gis), + ].length; + if (eventAttributeCount !== 0) { + throw new Error( + `Expected no inline event handler attributes in site/${htmlName}; found ${eventAttributeCount}. The CSP pins script-src-attr 'none'.`, + ); + } + + if (sharedScript !== undefined && script !== sharedScript) { + throw new Error( + "site/index.html and site/404.html inline bootstrap scripts differ; they must be byte-identical.", + ); + } + sharedScript = script; } - return matches[0][1]; + + return { + scriptHashes: [`sha256-${sha256Base64(sharedScript)}`], + }; +} + +export function getHeaderContract(directory = siteRoot) { + const { scriptHashes } = getInlineScriptContract(directory); + const cspDirectives = [ + ["default-src", ["'self'"]], + ["base-uri", ["'none'"]], + ["object-src", ["'none'"]], + ["frame-ancestors", ["'none'"]], + ["form-action", ["'self'"]], + ["script-src", ["'self'", ...scriptHashes.map((hash) => `'${hash}'`)]], + ["script-src-attr", ["'none'"]], + ["style-src", ["'self'"]], + ["font-src", ["'self'"]], + ["connect-src", ["'self'", "https://api.github.com"]], + ["img-src", ["'self'", "data:"]], + ["frame-src", ["'none'"]], + ["worker-src", ["'none'"]], + ["manifest-src", ["'self'"]], + ["upgrade-insecure-requests", []], + ]; + const contentSecurityPolicy = cspDirectives + .map( + ([name, sources]) => + `${name}${sources.length > 0 ? ` ${sources.join(" ")}` : ""}`, + ) + .join("; "); + const cacheControl = "public, max-age=0, must-revalidate"; + const permissionsPolicy = + "accelerometer=(), autoplay=(), camera=(), geolocation=(), gyroscope=(), " + + "magnetometer=(), microphone=(), payment=(), usb=()"; + const headersText = [ + "/*", + ` Content-Security-Policy: ${contentSecurityPolicy}`, + " X-Content-Type-Options: nosniff", + " X-Frame-Options: DENY", + " Referrer-Policy: strict-origin-when-cross-origin", + ` Permissions-Policy: ${permissionsPolicy}`, + ` Cache-Control: ${cacheControl}`, + "", + ].join("\n"); + + return { + generated_headers_base64: Buffer.from(headersText, "utf8").toString( + "base64", + ), + script_hashes: scriptHashes, + root: { + cache_control: cacheControl, + content_security_policy: contentSecurityPolicy, + x_content_type_options: "nosniff", + x_frame_options: "DENY", + referrer_policy: "strict-origin-when-cross-origin", + permissions_policy: permissionsPolicy, + }, + not_found: { cache_control: cacheControl }, + }; } function withAssetCacheKeys(html, htmlPath, assets) { let result = html; for (const [asset, digest] of Object.entries(assets)) { - const pattern = new RegExp(`(["'])(/?)${escapeRegExp(asset)}(?:\\?v=[^"']*)?\\1`, "g"); + const pattern = new RegExp( + `(["'])(/?)${escapeRegExp(asset)}(?:\\?v=[^"']*)?\\1`, + "g", + ); if (!pattern.test(result)) { - throw new Error(`${htmlPath} does not reference required local asset ${asset}.`); + throw new Error( + `${htmlPath} does not reference required local asset ${asset}.`, + ); } pattern.lastIndex = 0; result = result.replace( pattern, - (_match, quote, rootPrefix) => `${quote}${rootPrefix}${asset}?v=${digest}${quote}`, + (_match, quote, rootPrefix) => + `${quote}${rootPrefix}${asset}?v=${digest}${quote}`, ); } return result; @@ -93,39 +168,24 @@ function main() { if (checkOnly) { failures.push(`${relativePath} is stale.`); } else { - fs.writeFileSync(path.join(root, relativePath), expected, "utf8"); - console.log(`Updated ${relativePath}`); - } - } - - const indexHtml = readSiteFile("site/index.html"); - const notFoundHtml = readSiteFile("site/404.html"); - - // The inline theme bootstrap must be byte-identical across pages so the CSP - // pins exactly one script hash. - const inlineScriptPattern = /') | - Where-Object { $_.Groups['attrs'].Value -notmatch '\bsrc\s*=' } - ) - if ($inlineScripts.Count -ne 1) { - throw "Expected exactly one CSP-hashed inline script in $htmlName." - } - [void]$expectedScriptHashes.Add( - (Get-CspSha256Source -Value $inlineScripts[0].Groups['body'].Value) - ) - # Fonts are self-hosted, so no page may carry an inline event handler at - # all. The CSP pins script-src-attr 'none', which is strictly stronger - # than hashing a handler with 'unsafe-hashes'. - $eventAttributeCount = [regex]::Matches( - $html, - '(?is)\s+on[a-z][a-z0-9_-]*\s*=' - ).Count - if ($eventAttributeCount -ne 0) { - throw "Expected no inline event handler attributes in $htmlName; the site CSP pins script-src-attr 'none'." - } -} - $csp = $security['Content-Security-Policy'] -function Get-CspDirectiveSources { - param( - [Parameter(Mandatory)] [string] $Policy, - [Parameter(Mandatory)] [string] $DirectiveName - ) - - $matches = @( - $Policy.Split(';') | - ForEach-Object { $_.Trim() } | - Where-Object { - $_ -match ('^' + [regex]::Escape($DirectiveName) + '(?:\s|$)') - } - ) - if ($matches.Count -ne 1) { - throw "Site CSP must declare $DirectiveName exactly once." - } - return @([regex]::Split($matches[0], '\s+') | Select-Object -Skip 1) -} - -function Assert-ExactCspDirectiveSources { - param( - [Parameter(Mandatory)] [string] $DirectiveName, - [Parameter(Mandatory)] [AllowEmptyCollection()] - [string[]] $Expected - ) - - $declaredTokens = @( - Get-CspDirectiveSources -Policy $csp -DirectiveName $DirectiveName - ) - $declared = [Collections.Generic.HashSet[string]]::new( - [StringComparer]::Ordinal - ) - $expectedSet = [Collections.Generic.HashSet[string]]::new( - [StringComparer]::Ordinal - ) - foreach ($token in $declaredTokens) { - [void]$declared.Add($token) - } - foreach ($token in $Expected) { - [void]$expectedSet.Add($token) - } - if ($declaredTokens.Count -ne $declared.Count -or - $Expected.Count -ne $expectedSet.Count -or - $declared.Count -ne $expectedSet.Count -or - @($expectedSet | Where-Object { -not $declared.Contains($_) }).Count -ne 0) { - throw "Site CSP $DirectiveName sources do not exactly match the site contract." - } -} - -$expectedScriptSources = @("'self'") + @( - $expectedScriptHashes | ForEach-Object { "'$_'" } -) -$expectedScriptAttributeSources = @("'none'") -$expectedCspDirectives = [ordered]@{ - 'default-src' = @("'self'") - 'base-uri' = @("'none'") - 'object-src' = @("'none'") - 'frame-ancestors' = @("'none'") - 'form-action' = @("'self'") - 'script-src' = $expectedScriptSources - 'script-src-attr' = $expectedScriptAttributeSources - 'style-src' = @("'self'") - 'font-src' = @("'self'") - 'connect-src' = @("'self'", 'https://api.github.com') - 'img-src' = @("'self'", 'data:') - 'frame-src' = @("'none'") - 'worker-src' = @("'none'") - 'manifest-src' = @("'self'") - 'upgrade-insecure-requests' = @() -} -$declaredDirectiveNames = [Collections.Generic.HashSet[string]]::new( - [StringComparer]::Ordinal -) -foreach ($directive in $csp.Split(';')) { - $directive = $directive.Trim() - if (-not $directive) { continue } - $directiveName = [regex]::Split($directive, '\s+')[0] - if (-not $declaredDirectiveNames.Add($directiveName)) { - throw "Site CSP declares duplicate directive $directiveName." - } -} -if ($declaredDirectiveNames.Count -ne $expectedCspDirectives.Count -or - @($declaredDirectiveNames | Where-Object { - -not $expectedCspDirectives.Contains($_) - }).Count -ne 0) { - throw 'Site CSP does not declare the exact expected directive set.' -} -foreach ($directiveName in $expectedCspDirectives.Keys) { - Assert-ExactCspDirectiveSources ` - -DirectiveName $directiveName ` - -Expected $expectedCspDirectives[$directiveName] -} - -$expectedHashes = [Collections.Generic.HashSet[string]]::new( - $expectedScriptHashes, - [StringComparer]::Ordinal -) -$expectedHashes.UnionWith($expectedScriptAttributeHashes) -$declaredHashes = [Collections.Generic.HashSet[string]]::new( - [StringComparer]::Ordinal -) -foreach ($match in [regex]::Matches( - $csp, - "'(?sha256-[A-Za-z0-9+/]+=*)'" -)) { - [void]$declaredHashes.Add($match.Groups['hash'].Value) -} -if ($declaredHashes.Count -ne $expectedHashes.Count -or - @($expectedHashes | Where-Object { - -not $declaredHashes.Contains($_) - }).Count -ne 0) { - throw 'Site CSP declares hashes outside the exact HTML source contract.' +Assert-ReviewedContentSecurityPolicy ` + -Policy $csp ` + -ScriptHashes @($derivedHeaderContract.script_hashes) +if ($csp -cne [string] $derivedHeaderContract.root.content_security_policy) { + throw 'Tracked site CSP differs from the HTML-derived source of truth.' } [ordered]@{ root = [ordered]@{ - cache_control = $security['Cache-Control'] - content_security_policy = $csp - x_content_type_options = $security['X-Content-Type-Options'] - x_frame_options = $security['X-Frame-Options'] - referrer_policy = $security['Referrer-Policy'] - permissions_policy = $permissions + cache_control = $derivedHeaderContract.root.cache_control + content_security_policy = $derivedHeaderContract.root.content_security_policy + x_content_type_options = $derivedHeaderContract.root.x_content_type_options + x_frame_options = $derivedHeaderContract.root.x_frame_options + referrer_policy = $derivedHeaderContract.root.referrer_policy + permissions_policy = $derivedHeaderContract.root.permissions_policy } not_found = [ordered]@{ - cache_control = 'no-store' + cache_control = $derivedHeaderContract.not_found.cache_control } } | ConvertTo-Json -Depth 4 -Compress diff --git a/scripts/interactive-win11-lib.ps1 b/scripts/interactive-win11-lib.ps1 index d5d0406fea9..466ef6f78e3 100644 --- a/scripts/interactive-win11-lib.ps1 +++ b/scripts/interactive-win11-lib.ps1 @@ -352,6 +352,32 @@ function Invoke-InteractiveWin11Bootstrap { } } +function Invoke-InteractiveWin11HarnessMain { + param( + [Parameter(Mandatory)] [string] $RepoRoot, + [Parameter(Mandatory)] [string] $LauncherPath, + [Parameter(Mandatory)] [string] $EnvironmentVariable, + [string[]] $ArgumentList = @() + ) + + $bootstrapSentinel = [System.Environment]::GetEnvironmentVariable( + $EnvironmentVariable, + [System.EnvironmentVariableTarget]::Process + ) + if (-not [string]::IsNullOrEmpty($bootstrapSentinel)) { + return + } + + $bootstrapExitCode = 0 + Invoke-InteractiveWin11Bootstrap ` + -RepoRoot $RepoRoot ` + -LauncherPath $LauncherPath ` + -EnvironmentVariable $EnvironmentVariable ` + -ArgumentList $ArgumentList ` + -ExitCode ([ref] $bootstrapExitCode) + exit $bootstrapExitCode +} + function Set-InteractiveWin11Environment { param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Layout, @@ -584,12 +610,32 @@ function Wait-InteractiveWin11Until { [Parameter(Mandatory)] [scriptblock] $Condition, [Parameter(Mandatory)] [string] $Description, [Parameter(Mandatory)] [DateTime] $Deadline, - [System.Diagnostics.Process] $Process + [Alias('Process')] [System.Diagnostics.Process] $WatchProcess, + [ValidateRange(1, [int]::MaxValue)] [int] $PollMilliseconds = 100, + [string] $TimeoutMessage, + [switch] $ConditionFirst ) + if ($ConditionFirst) { + while ($true) { + if ($null -ne $WatchProcess -and $WatchProcess.HasExited) { + throw "noctty exited while waiting for ${Description} (exit code $($WatchProcess.ExitCode))" + } + + if (& $Condition) { + return + } + + Start-Sleep -Milliseconds $PollMilliseconds + if ([DateTime]::UtcNow -ge $Deadline) { + break + } + } + } + else { while ($true) { - if ($null -ne $Process -and $Process.HasExited) { - throw "noctty exited while waiting for ${Description} (exit code $($Process.ExitCode))" + if ($null -ne $WatchProcess -and $WatchProcess.HasExited) { + throw "noctty exited while waiting for ${Description} (exit code $($WatchProcess.ExitCode))" } if ([DateTime]::UtcNow -ge $Deadline) { @@ -600,10 +646,14 @@ function Wait-InteractiveWin11Until { return } - Start-Sleep -Milliseconds 100 + Start-Sleep -Milliseconds $PollMilliseconds + } } - throw "Timed out waiting for $Description" + if ([string]::IsNullOrEmpty($TimeoutMessage)) { + throw "Timed out waiting for $Description" + } + throw $TimeoutMessage } function Get-InteractiveWin11ProcessTreeSnapshot { diff --git a/scripts/interactive-win11-window-lib.ps1 b/scripts/interactive-win11-window-lib.ps1 new file mode 100644 index 00000000000..508d37679d7 --- /dev/null +++ b/scripts/interactive-win11-window-lib.ps1 @@ -0,0 +1,283 @@ +if (-not ('InteractiveWin11WindowNative' -as [type])) { + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +public static class InteractiveWin11WindowNative { + private static int lastSendInputRequested; + private static int lastSendInputReturned; + + public static int LastSendInputRequested { + get { return Volatile.Read(ref lastSendInputRequested); } + } + + public static int LastSendInputReturned { + get { return Volatile.Read(ref lastSendInputReturned); } + } + + public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + public struct POINT { + public int x; + public int y; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT { + public int left; + public int top; + public int right; + public int bottom; + } + + [StructLayout(LayoutKind.Sequential)] + public struct HIGHCONTRAST { + public uint cbSize; + public uint dwFlags; + public IntPtr lpszDefaultScheme; + } + + [StructLayout(LayoutKind.Sequential)] + public struct GUITHREADINFO { + public uint cbSize; + public uint flags; + public IntPtr hwndActive; + public IntPtr hwndFocus; + public IntPtr hwndCapture; + public IntPtr hwndMenuOwner; + public IntPtr hwndMoveSize; + public IntPtr hwndCaret; + public RECT rcCaret; + } + + [StructLayout(LayoutKind.Sequential)] + public struct WINDOWPLACEMENT { + public uint length; + public uint flags; + public uint showCmd; + public POINT ptMinPosition; + public POINT ptMaxPosition; + public RECT rcNormalPosition; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MOUSEINPUT { + public int dx; + public int dy; + public uint mouseData; + public uint dwFlags; + public uint time; + public UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + public struct KEYBDINPUT { + public ushort wVk; + public ushort wScan; + public uint dwFlags; + public uint time; + public UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + public struct HARDWAREINPUT { + public uint uMsg; + public ushort wParamL; + public ushort wParamH; + } + + [StructLayout(LayoutKind.Explicit)] + public struct INPUTUNION { + [FieldOffset(0)] + public MOUSEINPUT mi; + + [FieldOffset(0)] + public KEYBDINPUT ki; + + [FieldOffset(0)] + public HARDWAREINPUT hi; + } + + [StructLayout(LayoutKind.Sequential)] + public struct INPUT { + public uint type; + public INPUTUNION value; + + public INPUTUNION U { + get { return value; } + set { this.value = value; } + } + } + + [DllImport("user32.dll")] + public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + public static extern bool EnumChildWindows(IntPtr hWnd, EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int GetClassNameW(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll")] + public static extern int GetDlgCtrlID(IntPtr hwndCtl); + + [DllImport("user32.dll")] + public static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetWindowPos( + IntPtr hWnd, + IntPtr hWndInsertAfter, + int X, + int Y, + int cx, + int cy, + uint uFlags + ); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll")] + public static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetGUIThreadInfo(uint idThread, ref GUITHREADINFO lpgui); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach); + + [DllImport("user32.dll")] + public static extern bool BringWindowToTop(IntPtr hWnd); + + [DllImport("kernel32.dll")] + public static extern uint GetCurrentThreadId(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern uint SendInput(uint cInputs, INPUT[] pInputs, int cbSize); + + [DllImport("user32.dll")] + public static extern void keybd_event( + byte bVk, + byte bScan, + uint dwFlags, + UIntPtr dwExtraInfo + ); + + [DllImport("user32.dll")] + public static extern uint MapVirtualKeyW(uint uCode, uint uMapType); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern short VkKeyScanW(char ch); + + [DllImport("user32.dll")] + public static extern bool IsChild(IntPtr hWndParent, IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool IsZoomed(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr SetFocus(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern IntPtr GetParent(IntPtr hWnd); + + [DllImport( + "user32.dll", + EntryPoint = "SystemParametersInfoW", + CharSet = CharSet.Unicode, + SetLastError = true + )] + public static extern bool SystemParametersInfo( + uint action, + uint parameter, + ref HIGHCONTRAST value, + uint flags + ); + + private const uint INPUT_KEYBOARD = 1; + private const uint KEYEVENTF_KEYUP = 0x0002; + + private static INPUT Key(ushort virtualKey, uint flags) { + INPUT input = new INPUT(); + input.type = INPUT_KEYBOARD; + input.value.ki.wVk = virtualKey; + input.value.ki.dwFlags = flags; + return input; + } + + public static bool SubmitInputs(INPUT[] inputs) { + int requested = inputs.Length; + int returned = unchecked((int)SendInput( + (uint)requested, + inputs, + Marshal.SizeOf(typeof(INPUT)) + )); + Volatile.Write(ref lastSendInputRequested, requested); + Volatile.Write(ref lastSendInputReturned, returned); + return returned == requested; + } + + public static bool ForceForeground( + IntPtr hWnd, + bool altTap, + bool useSendInputForAltTap + ) { + if (altTap) { + if (useSendInputForAltTap) { + SubmitInputs(new INPUT[] { + Key(0x12, 0), + Key(0x12, KEYEVENTF_KEYUP) + }); + } + else { + keybd_event(0x12, 0, 0, UIntPtr.Zero); + keybd_event(0x12, 0, KEYEVENTF_KEYUP, UIntPtr.Zero); + } + } + + uint ignored; + uint targetThread = GetWindowThreadProcessId(hWnd, out ignored); + IntPtr foreground = GetForegroundWindow(); + uint foregroundThread = foreground == IntPtr.Zero + ? 0 + : GetWindowThreadProcessId(foreground, out ignored); + uint currentThread = GetCurrentThreadId(); + bool attachedForeground = foregroundThread != 0 && + foregroundThread != currentThread && + AttachThreadInput(currentThread, foregroundThread, true); + bool attachedTarget = targetThread != 0 && + targetThread != currentThread && + AttachThreadInput(currentThread, targetThread, true); + try { + BringWindowToTop(hWnd); + SetForegroundWindow(hWnd); + return GetForegroundWindow() == hWnd; + } + finally { + if (attachedTarget) { + AttachThreadInput(currentThread, targetThread, false); + } + if (attachedForeground) { + AttachThreadInput(currentThread, foregroundThread, false); + } + } + } +} +'@ +} diff --git a/scripts/package-package-managers.ps1 b/scripts/package-package-managers.ps1 index 3eab2c0cd70..0a2799e7e63 100644 --- a/scripts/package-package-managers.ps1 +++ b/scripts/package-package-managers.ps1 @@ -23,11 +23,12 @@ param( ) $ErrorActionPreference = "Stop" +. (Join-Path $PSScriptRoot "common.ps1") . (Join-Path $PSScriptRoot "windows-architecture.ps1") $Architectures = @($Architectures | ForEach-Object { (Get-WindowsPackageArchitecture -Architecture $_).Name }) -$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) +$repoRoot = Get-RepoRoot $tagValue = if ($Tag) { $Tag } else { "v$Version" } if ($ArtifactRoot -and $Architectures.Count -ne 1) { throw "-ArtifactRoot can only be used with one architecture." @@ -100,7 +101,7 @@ function Get-AssetChecksum { throw "Missing checksum entry for $AssetName in $ChecksumsPath" } - $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $AssetPath).Hash.ToLowerInvariant() + $actual = Get-FileSha256Lower -Path $AssetPath if ($actual -ne $checksum) { throw "Checksum mismatch for $AssetName. Expected $checksum, got $actual." } diff --git a/scripts/package-windows.ps1 b/scripts/package-windows.ps1 index 327124998ac..49035b87fbc 100644 --- a/scripts/package-windows.ps1 +++ b/scripts/package-windows.ps1 @@ -16,6 +16,7 @@ param( $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.IO.Compression.FileSystem +. (Join-Path $PSScriptRoot "common.ps1") . (Join-Path $PSScriptRoot "windows-architecture.ps1") . (Join-Path $PSScriptRoot "signing-trust.ps1") . (Join-Path $PSScriptRoot "windows-build-capabilities.ps1") @@ -23,7 +24,7 @@ Add-Type -AssemblyName System.IO.Compression.FileSystem $archInfo = Get-WindowsPackageArchitecture -Architecture $(if ($Architecture) { $Architecture } else { Get-DefaultWindowsPackageArchitecture }) $Architecture = $archInfo.Name -$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) +$repoRoot = Get-RepoRoot $outputRootPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputRoot)) $userHome = if ($env:USERPROFILE) { $env:USERPROFILE @@ -555,7 +556,7 @@ try { } $hashLines = foreach ($target in $hashTargets) { - $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $target.Path).Hash.ToLowerInvariant() + $hash = Get-FileSha256Lower -Path $target.Path "$hash *$($target.Name)" } diff --git a/scripts/release-preflight.ps1 b/scripts/release-preflight.ps1 index cac425b0613..c1b5963bea6 100644 --- a/scripts/release-preflight.ps1 +++ b/scripts/release-preflight.ps1 @@ -12,7 +12,8 @@ param( $ErrorActionPreference = "Stop" -$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) +. (Join-Path $PSScriptRoot "common.ps1") +$repoRoot = Get-RepoRoot $releaseMetaPath = Join-Path $repoRoot "dist/windows/release-metadata.json" . (Join-Path $PSScriptRoot 'signing-trust.ps1') diff --git a/scripts/release-publish-github.ps1 b/scripts/release-publish-github.ps1 new file mode 100644 index 00000000000..0cd5f4ebea2 --- /dev/null +++ b/scripts/release-publish-github.ps1 @@ -0,0 +1,116 @@ +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory)] + [ValidatePattern('^v\d+\.\d+\.\d+$')] + [string] $Tag, + + [Parameter(Mandatory)] + [ValidatePattern('^\d+\.\d+\.\d+$')] + [string] $Version, + + [Parameter(Mandatory)] + [string] $Title, + + [Parameter(Mandatory)] + [ValidatePattern('^\d+\.\d+$')] + [string] $VersionLine, + + [Parameter(Mandatory)] + [ValidatePattern('^\d+\.\d+\.\d+$')] + [string] $UpstreamBaseVersion, + + [Parameter(Mandatory)] + [ValidateSet('true', 'false')] + [string] $Prerelease, + + [string] $Repository = $( + if (-not [string]::IsNullOrWhiteSpace($env:GH_REPO)) { + $env:GH_REPO + } elseif (-not [string]::IsNullOrWhiteSpace($env:GITHUB_REPOSITORY)) { + $env:GITHUB_REPOSITORY + } else { + 'amanthanvi/noctty' + } + ), + + [string] $ArtifactRoot +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'common.ps1') +. (Join-Path $PSScriptRoot 'windows-architecture.ps1') + +$repoRoot = Get-RepoRoot +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot 'dist/artifacts' +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) + +if (-not $PSCmdlet.ShouldProcess("$Repository release $Tag", 'publish GitHub release and assets')) { + return +} +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + throw 'GitHub CLI (gh) is required to publish a release.' +} + +$architectures = Get-WindowsPackageArchitectures +$artifactDirectories = @{} +foreach ($architecture in $architectures) { + $artifactDirectories[$architecture] = Join-Path $ArtifactRoot "noctty-$Version-windows-$architecture" +} +$artifactDirectoryX64 = $artifactDirectories['x64'] +$legacyChecksumsPath = Join-Path $artifactDirectoryX64 ( + New-WindowsPackageArtifactName ` + -Version $Version ` + -Architecture x64 ` + -Kind legacy-checksums +) +Copy-Item ` + -LiteralPath (Join-Path $artifactDirectoryX64 ( + New-WindowsPackageArtifactName ` + -Version $Version ` + -Architecture x64 ` + -Kind checksums + )) ` + -Destination $legacyChecksumsPath ` + -Force + +$assets = @() +foreach ($architecture in $architectures) { + $artifactDirectory = $artifactDirectories[$architecture] + $assets += @( + (Join-Path $artifactDirectory (New-WindowsPackageArtifactName -Version $Version -Architecture $architecture -Kind setup)), + (Join-Path $artifactDirectory (New-WindowsPackageArtifactName -Version $Version -Architecture $architecture -Kind portable)), + (Join-Path $artifactDirectory (New-WindowsPackageArtifactName -Version $Version -Architecture $architecture -Kind checksums)) + ) +} +$assets += @( + $legacyChecksumsPath, + (Join-Path $artifactDirectoryX64 'noctty-icon.svg') +) +$notesPrefix = @" +Compatibility line: Ghostty $VersionLine +Base upstream release: $UpstreamBaseVersion +Versioning: major.minor follow the Ghostty upstream line; patch is the noctty release number on that line. +"@ + +& gh release view $Tag --repo $Repository *> $null +if ($LASTEXITCODE -ne 0) { + $extra = @() + if ($Prerelease -eq 'true') { + $extra += '--prerelease' + } + & gh release create $Tag --repo $Repository --title $Title --generate-notes --notes $notesPrefix @extra @assets + if ($LASTEXITCODE -ne 0) { + throw "Failed to create GitHub release $Tag." + } +} else { + & gh release edit $Tag --repo $Repository --title $Title "--prerelease=$Prerelease" + if ($LASTEXITCODE -ne 0) { + throw "Failed to edit GitHub release $Tag." + } + & gh release upload $Tag --repo $Repository @assets --clobber + if ($LASTEXITCODE -ne 0) { + throw "Failed to upload assets for GitHub release $Tag." + } +} diff --git a/scripts/release-publish-scoop.ps1 b/scripts/release-publish-scoop.ps1 new file mode 100644 index 00000000000..f3bcbabac7f --- /dev/null +++ b/scripts/release-publish-scoop.ps1 @@ -0,0 +1,217 @@ +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory)] + [ValidatePattern('^\d+\.\d+\.\d+$')] + [string] $Version, + + [string] $BucketRepository = $env:SCOOP_BUCKET_REPO, + + [string] $BucketBranch = $env:SCOOP_BUCKET_BRANCH, + + [string] $ManifestPath = $env:SCOOP_BUCKET_MANIFEST_PATH, + + [string] $ArtifactRoot, + + [string] $RunnerTemp = $env:RUNNER_TEMP +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'common.ps1') + +if (-not $env:GH_TOKEN) { + Write-Host 'Skipping Scoop publish: SCOOP_BUCKET_TOKEN is not configured.' + return +} +if ([string]::IsNullOrWhiteSpace($BucketRepository)) { + Write-Host 'Skipping Scoop publish: SCOOP_BUCKET_REPO is not configured.' + return +} +if ([string]::IsNullOrWhiteSpace($RunnerTemp)) { + $RunnerTemp = [System.IO.Path]::GetTempPath() +} +$RunnerTemp = [System.IO.Path]::GetFullPath($RunnerTemp) +$bucketDirectory = [System.IO.Path]::GetFullPath( + (Join-Path $RunnerTemp 'noctty-scoop-bucket') +) +$bucketRelativePath = [System.IO.Path]::GetRelativePath( + $RunnerTemp, + $bucketDirectory +) +if ($bucketRelativePath -cne 'noctty-scoop-bucket') { + throw "Refusing to use unexpected Scoop clone path: $bucketDirectory" +} + +$manifestRelativePath = if ([string]::IsNullOrWhiteSpace($ManifestPath)) { + 'bucket/noctty.json' +} else { + $ManifestPath +} +if ([System.IO.Path]::IsPathRooted($manifestRelativePath)) { + throw "Scoop manifest path must be relative to the bucket clone: $manifestRelativePath" +} +$manifestPathSegments = @($manifestRelativePath -split '[\\/]') +if (@($manifestPathSegments | Where-Object { $_ -ceq '..' }).Count -ne 0) { + throw "Scoop manifest path must not contain parent traversal: $manifestRelativePath" +} +if ([string]::IsNullOrWhiteSpace( + [System.IO.Path]::GetFileName($manifestRelativePath) + )) { + throw "Scoop manifest path must name a file: $manifestRelativePath" +} +try { + $destinationManifestPath = [System.IO.Path]::GetFullPath( + (Join-Path $bucketDirectory $manifestRelativePath) + ) +} +catch { + throw "Scoop manifest path is invalid: $manifestRelativePath" +} +$bucketDirectoryPrefix = $bucketDirectory.TrimEnd( + [System.IO.Path]::DirectorySeparatorChar, + [System.IO.Path]::AltDirectorySeparatorChar +) + [System.IO.Path]::DirectorySeparatorChar +if (-not $destinationManifestPath.StartsWith( + $bucketDirectoryPrefix, + [StringComparison]::OrdinalIgnoreCase + )) { + throw "Scoop manifest path escapes the bucket clone: $manifestRelativePath" +} +$manifestRelativePath = [System.IO.Path]::GetRelativePath( + $bucketDirectory, + $destinationManifestPath +) + +function Remove-ValidatedScoopClone { + try { + $cloneItem = Get-Item -LiteralPath $bucketDirectory -Force -ErrorAction Stop + } + catch [System.Management.Automation.ItemNotFoundException] { + return + } + if (-not $cloneItem.PSIsContainer) { + throw "Scoop clone path is not a directory; refusing recursive cleanup: $bucketDirectory" + } + if (($cloneItem.Attributes -band + [System.IO.FileAttributes]::ReparsePoint) -ne 0) { + throw "Scoop clone path is a reparse point; refusing recursive cleanup: $bucketDirectory" + } + Remove-Item -LiteralPath $bucketDirectory -Recurse -Force +} + +if (-not $PSCmdlet.ShouldProcess($BucketRepository, "publish noctty $Version Scoop manifest")) { + return +} + +$repoRoot = Get-RepoRoot +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot 'dist/artifacts' +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) +$artifactDirectoryX64 = Join-Path $ArtifactRoot "noctty-$Version-windows-x64" +$metadataPath = Join-Path $artifactDirectoryX64 'package-managers/metadata.json' +$metadata = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json + +Remove-ValidatedScoopClone + +$gitNetworkBoundArgs = @( + '-c', + 'http.lowSpeedLimit=1', + '-c', + 'http.lowSpeedTime=60' +) +$credentialHelperArgs = @( + '-c', + 'credential.helper=', + '-c', + 'credential.helper=!gh auth git-credential' +) +$cloneArgs = @( + 'clone', + '--depth', + '1' +) +if ($BucketBranch) { + $cloneArgs += @('--branch', $BucketBranch) +} +$cloneArgs += @( + "https://github.com/$BucketRepository.git", + $bucketDirectory +) + +try { + & git @gitNetworkBoundArgs @credentialHelperArgs @cloneArgs + if ($LASTEXITCODE -ne 0) { + throw "Failed to clone Scoop bucket repo $BucketRepository." + } + + $destinationManifestDirectory = Split-Path -Parent $destinationManifestPath + $relativeManifestDirectory = [System.IO.Path]::GetRelativePath( + $bucketDirectory, + $destinationManifestDirectory + ) + $currentManifestDirectory = $bucketDirectory + foreach ($segment in @($relativeManifestDirectory -split '[\\/]')) { + if ($segment -ceq '.') { continue } + $currentManifestDirectory = Join-Path $currentManifestDirectory $segment + if (Test-Path -LiteralPath $currentManifestDirectory) { + $directoryItem = Get-Item -LiteralPath $currentManifestDirectory -Force + if (-not $directoryItem.PSIsContainer -or + ($directoryItem.Attributes -band + [System.IO.FileAttributes]::ReparsePoint) -ne 0) { + throw "Scoop manifest path crosses a non-directory or reparse point: $manifestRelativePath" + } + } + } + if (Test-Path -LiteralPath $destinationManifestPath) { + $destinationItem = Get-Item -LiteralPath $destinationManifestPath -Force + if ($destinationItem.PSIsContainer) { + throw "Scoop manifest path names a directory: $manifestRelativePath" + } + if (($destinationItem.Attributes -band + [System.IO.FileAttributes]::ReparsePoint) -ne 0) { + throw "Scoop manifest path names a reparse point: $manifestRelativePath" + } + } + New-Item -ItemType Directory -Path $destinationManifestDirectory -Force | Out-Null + Copy-Item -LiteralPath $metadata.scoop.manifestPath -Destination $destinationManifestPath -Force + + Push-Location $bucketDirectory + try { + & git config user.name 'github-actions[bot]' + if ($LASTEXITCODE -ne 0) { + throw "git config user.name failed with exit code $LASTEXITCODE" + } + & git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + if ($LASTEXITCODE -ne 0) { + throw "git config user.email failed with exit code $LASTEXITCODE" + } + & git add -- $manifestRelativePath + if ($LASTEXITCODE -ne 0) { + throw "git add failed with exit code $LASTEXITCODE" + } + & git diff --cached --quiet --exit-code + if ($LASTEXITCODE -eq 0) { + Write-Host 'Scoop manifest is unchanged; skipping push.' + return + } + if ($LASTEXITCODE -ne 1) { + throw "git diff --cached failed with exit code $LASTEXITCODE" + } + + & git commit -m "noctty: update to $Version" + if ($LASTEXITCODE -ne 0) { + throw "git commit failed with exit code $LASTEXITCODE" + } + + & git @gitNetworkBoundArgs -c 'credential.helper=' -c 'credential.helper=!gh auth git-credential' push origin HEAD + if ($LASTEXITCODE -ne 0) { + throw "git push failed with exit code $LASTEXITCODE" + } + } + finally { + Pop-Location + } +} +finally { + Remove-ValidatedScoopClone +} diff --git a/scripts/release-scan-defender.ps1 b/scripts/release-scan-defender.ps1 new file mode 100644 index 00000000000..9561a036257 --- /dev/null +++ b/scripts/release-scan-defender.ps1 @@ -0,0 +1,113 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [ValidatePattern('^\d+\.\d+\.\d+$')] + [string] $Version, + + [string] $ArtifactRoot, + + [string] $ExtractionRoot = $env:RUNNER_TEMP, + + [switch] $ValidateParameters +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'common.ps1') +. (Join-Path $PSScriptRoot 'windows-architecture.ps1') + +$repoRoot = Get-RepoRoot +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot 'dist/artifacts' +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) +if ([string]::IsNullOrWhiteSpace($ExtractionRoot)) { + $ExtractionRoot = [System.IO.Path]::GetTempPath() +} +$ExtractionRoot = [System.IO.Path]::GetFullPath($ExtractionRoot) + +if ($ValidateParameters) { + Write-Host "Microsoft Defender scan parameters: PASS ($Version)" -ForegroundColor Green + return +} + +$status = Get-MpComputerStatus -ErrorAction Stop +if (-not $status.AMServiceEnabled -or + -not $status.AntivirusEnabled -or + $status.AMRunningMode -ne 'Normal') { + throw 'Microsoft Defender Antivirus must be active in Normal mode before release publication.' +} + +$scannerCandidates = @( + (Join-Path $env:ProgramFiles 'Windows Defender/MpCmdRun.exe') +) +$platformRoot = Join-Path $env:ProgramData 'Microsoft/Windows Defender/Platform' +if (Test-Path -LiteralPath $platformRoot) { + $scannerCandidates = @( + Get-ChildItem -LiteralPath $platformRoot -Directory | + ForEach-Object { + $platformVersion = ($_.Name -replace '-\d+$', '') -as [version] + if ($null -ne $platformVersion) { + [pscustomobject]@{ + Version = $platformVersion + Path = Join-Path $_.FullName 'MpCmdRun.exe' + } + } + } | + Sort-Object Version -Descending | + ForEach-Object { $_.Path } + ) + $scannerCandidates +} +$scanner = $scannerCandidates | + Where-Object { Test-Path -LiteralPath $_ } | + Select-Object -First 1 +if (-not $scanner) { + throw 'MpCmdRun.exe was not found.' +} + +& $scanner -SignatureUpdate +if ($LASTEXITCODE -ne 0) { + throw "Microsoft Defender signature update failed with exit code $LASTEXITCODE." +} + +$portablePayloads = @( + 'noctty/noctty.com', + 'noctty/noctty.exe', + 'noctty/ghostty-vt.dll' +) +$architectures = @(Get-WindowsPackageArchitectures) +$expectedScanCount = $architectures.Count * (1 + $portablePayloads.Count) +$scanPaths = @( + foreach ($architecture in $architectures) { + $artifactDirectory = Join-Path $ArtifactRoot "noctty-$Version-windows-$architecture" + $setupName = New-WindowsPackageArtifactName ` + -Version $Version ` + -Architecture $architecture ` + -Kind setup + $setupPath = Join-Path $artifactDirectory $setupName + if (-not (Test-Path -LiteralPath $setupPath -PathType Leaf)) { + throw "Missing Defender scan input: $setupPath" + } + $setupPath + + $extractDirectory = Join-Path $ExtractionRoot "noctty-release-verify-$architecture" + foreach ($relativePath in $portablePayloads) { + $payloadPath = Join-Path $extractDirectory $relativePath + if (-not (Test-Path -LiteralPath $payloadPath -PathType Leaf)) { + throw "Missing Defender scan input: $payloadPath" + } + $payloadPath + } + } +) +if ($scanPaths.Count -ne $expectedScanCount) { + throw "Expected exactly $expectedScanCount release artifacts for Defender scanning; found $($scanPaths.Count)." +} + +foreach ($scanPath in $scanPaths) { + & $scanner -Scan -ScanType 3 -File $scanPath -DisableRemediation -ReturnHR + if ($LASTEXITCODE -ne 0) { + throw "Microsoft Defender rejected $scanPath with exit code $LASTEXITCODE." + } +} + +Write-Host "Microsoft Defender release scan: PASS ($Version, $($scanPaths.Count) files)" -ForegroundColor Green diff --git a/scripts/release-submit-winget.ps1 b/scripts/release-submit-winget.ps1 new file mode 100644 index 00000000000..1f4539013a4 --- /dev/null +++ b/scripts/release-submit-winget.ps1 @@ -0,0 +1,257 @@ +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory)] + [ValidatePattern('^\d+\.\d+\.\d+$')] + [string] $Version, + + [string] $PackageIdentifier = $env:WINGET_PACKAGE_IDENTIFIER, + + [string] $ArtifactRoot, + + [string] $TempDirectory = $env:TEMP, + + [ValidateRange(1, 300)] + [int] $NetworkTimeoutSeconds = 60, + + [ValidateRange(1, 1800)] + [int] $WinGetCreateTimeoutSeconds = 600, + + [ValidateRange(1, 120)] + [int] $ProcessTerminationTimeoutSeconds = 30, + + [ValidateRange(1, 120)] + [int] $StreamDrainTimeoutSeconds = 30 +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'common.ps1') + +if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { + Write-Host 'Skipping WinGet submit: WINGET_CREATE_GITHUB_TOKEN is not configured.' + return +} +if ([string]::IsNullOrWhiteSpace($PackageIdentifier)) { + Write-Host 'Skipping WinGet submit: WINGET_PACKAGE_IDENTIFIER is not configured. Initial WinGet bootstrap is still manual.' + return +} +if (-not $PSCmdlet.ShouldProcess($PackageIdentifier, "submit noctty $Version WinGet manifest")) { + return +} + +$repoRoot = Get-RepoRoot +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot 'dist/artifacts' +} +if ([string]::IsNullOrWhiteSpace($TempDirectory)) { + $TempDirectory = [System.IO.Path]::GetTempPath() +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) +$TempDirectory = [System.IO.Path]::GetFullPath($TempDirectory) + +$artifactDirectoryX64 = Join-Path $ArtifactRoot "noctty-$Version-windows-x64" +$metadataPath = Join-Path $artifactDirectoryX64 'package-managers/metadata.json' +$metadata = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json +$packageIdSegments = $PackageIdentifier.Split('.') +$manifestPath = 'manifests/{0}/{1}' -f ` + $packageIdSegments[0].Substring(0, 1).ToLowerInvariant(), ` + ($packageIdSegments -join '/') +$manifestApiUrl = "https://api.github.com/repos/microsoft/winget-pkgs/contents/$manifestPath" + +try { + Invoke-WebRequest ` + -Uri $manifestApiUrl ` + -Headers @{ 'User-Agent' = 'noctty-release-workflow' } ` + -TimeoutSec $NetworkTimeoutSeconds ` + -ErrorAction Stop | Out-Null +} +catch { + $statusCode = if ($_.Exception.Response) { + [int]$_.Exception.Response.StatusCode + } else { + $null + } + + if ($statusCode -eq 404) { + Write-Host "Skipping WinGet submit: $PackageIdentifier is not bootstrapped in microsoft/winget-pkgs ($manifestPath)." + return + } + throw +} + +$vcLibsPath = Join-Path $TempDirectory 'Microsoft.VCLibs.x64.14.00.Desktop.appx' +$bundlePath = Join-Path $TempDirectory 'wingetcreate.msixbundle' +Import-Module Appx -UseWindowsPowerShell -ErrorAction Stop +function Install-AppxPackageIfNeeded { + param( + [string] $Path, + [string] $Label + ) + + try { + Add-AppxPackage -Path $Path -ErrorAction Stop + } + catch { + $message = $_.Exception.Message + # Hosted runners may already contain a newer package. HRESULT + # 0x80073D06 is success for this prerequisite path. + if ($message -match '0x80073D06' -or + $message -match 'higher version of this package is already installed') { + Write-Host "$Label already satisfied on this runner; skipping package install." + return + } + throw + } +} + +Invoke-WebRequest ` + -Uri 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx' ` + -OutFile $vcLibsPath ` + -TimeoutSec $NetworkTimeoutSeconds ` + -ErrorAction Stop +Install-AppxPackageIfNeeded ` + -Path $vcLibsPath ` + -Label 'Microsoft.VCLibs.x64.14.00.Desktop' +Invoke-WebRequest ` + -Uri 'https://aka.ms/wingetcreate/latest/msixbundle' ` + -OutFile $bundlePath ` + -TimeoutSec $NetworkTimeoutSeconds ` + -ErrorAction Stop +Install-AppxPackageIfNeeded -Path $bundlePath -Label 'wingetcreate bundle' + +# wingetcreate update documents URL architecture overrides as "|". +$installerUrlArgs = @( + $metadata.winget.installerUrlArgs | + Where-Object { -not [string]::IsNullOrWhiteSpace([string] $_) } | + ForEach-Object { [string] $_ } +) +$installerArchitectures = @( + $installerUrlArgs | ForEach-Object { + if ($_ -cnotmatch '\|(?x64|arm64)$') { + throw "WinGet installer URL must end with an explicit |x64 or |arm64 architecture: $_" + } + $Matches.architecture + } +) +$expectedInstallerArchitectures = @('arm64', 'x64') +if ($installerUrlArgs.Count -ne 2 -or + (($installerArchitectures | Sort-Object -Unique) -join ',') -ne + ($expectedInstallerArchitectures -join ',')) { + throw 'WinGet publishing requires exactly one x64 and one arm64 installer URL.' +} + +function Invoke-WinGetCreateUpdate { + param([string[]] $InstallerUrlArgs) + + $wingetCreateCommand = Get-Command ` + wingetcreate ` + -CommandType Application ` + -ErrorAction Stop | + Select-Object -First 1 + if ([string]::IsNullOrWhiteSpace($wingetCreateCommand.Source)) { + throw 'Unable to resolve the wingetcreate executable path.' + } + + $startInfo = [System.Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $wingetCreateCommand.Source + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + + $arguments = @( + 'update', + $PackageIdentifier, + '--version', + $Version, + '--urls' + ) + $arguments += $InstallerUrlArgs + $arguments += @( + '--release-notes-url', + [string] $metadata.release.releaseUrl, + '--submit', + '--no-open' + ) + foreach ($argument in $arguments) { + [void] $startInfo.ArgumentList.Add([string] $argument) + } + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $startInfo + $standardOutputTask = $null + $standardErrorTask = $null + $standardOutput = '' + $standardError = '' + $exitCode = $null + $timedOut = $false + $terminationError = $null + try { + if (-not $process.Start()) { + throw 'Failed to start wingetcreate.' + } + $standardOutputTask = $process.StandardOutput.ReadToEndAsync() + $standardErrorTask = $process.StandardError.ReadToEndAsync() + + if (-not $process.WaitForExit($WinGetCreateTimeoutSeconds * 1000)) { + $timedOut = $true + try { + $process.Kill($true) + } + catch { + $terminationError = $_.Exception.Message + } + try { + if (-not $process.WaitForExit($ProcessTerminationTimeoutSeconds * 1000)) { + $terminationError = 'the recorded process did not exit after termination' + } + } + catch { + $terminationError = $_.Exception.Message + } + } + + $streamTasks = [System.Threading.Tasks.Task[]] @( + $standardOutputTask, + $standardErrorTask + ) + if (-not [System.Threading.Tasks.Task]::WaitAll( + $streamTasks, + $StreamDrainTimeoutSeconds * 1000 + )) { + throw "Timed out draining wingetcreate output after $StreamDrainTimeoutSeconds seconds." + } + $standardOutput = $standardOutputTask.GetAwaiter().GetResult() + $standardError = $standardErrorTask.GetAwaiter().GetResult() + if ($process.HasExited) { + $exitCode = $process.ExitCode + } + } + finally { + $process.Dispose() + } + + $output = @($standardOutput, $standardError) -join "`n" + if (-not [string]::IsNullOrEmpty($env:WINGET_CREATE_GITHUB_TOKEN)) { + $output = $output.Replace($env:WINGET_CREATE_GITHUB_TOKEN, '[REDACTED]') + } + if (-not [string]::IsNullOrWhiteSpace($output)) { + $output -split '\r?\n' | ForEach-Object { Write-Host $_ } + } + if ($timedOut) { + $detail = if ($terminationError) { + " Termination detail: $terminationError." + } else { + '' + } + throw "wingetcreate update timed out after $WinGetCreateTimeoutSeconds seconds.$detail" + } + + return @{ + ExitCode = $exitCode + Output = $output + } +} + +$result = Invoke-WinGetCreateUpdate -InstallerUrlArgs $installerUrlArgs +if ($result.ExitCode -ne 0) { + throw "wingetcreate update failed with exit code $($result.ExitCode)" +} diff --git a/scripts/release-verify-artifacts.ps1 b/scripts/release-verify-artifacts.ps1 new file mode 100644 index 00000000000..2d7a935ab0a --- /dev/null +++ b/scripts/release-verify-artifacts.ps1 @@ -0,0 +1,121 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [ValidatePattern('^\d+\.\d+\.\d+$')] + [string] $Version, + + [string] $ArtifactRoot, + + [string] $ExtractionRoot = $env:RUNNER_TEMP, + + [bool] $TrustSelfSigned = ( + ([string] $env:WINDOWS_CODESIGN_TRUST_SELF_SIGNED). + Trim().ToLowerInvariant() -in @('true', '1', 'yes', 'on') + ), + + [switch] $ValidateParameters +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'common.ps1') +. (Join-Path $PSScriptRoot 'windows-architecture.ps1') +. (Join-Path $PSScriptRoot 'signing-trust.ps1') + +$repoRoot = Get-RepoRoot +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot 'dist/artifacts' +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) +if ([string]::IsNullOrWhiteSpace($ExtractionRoot)) { + $ExtractionRoot = [System.IO.Path]::GetTempPath() +} +$ExtractionRoot = [System.IO.Path]::GetFullPath($ExtractionRoot) + +if ($ValidateParameters) { + Write-Host "Release artifact verification parameters: PASS ($Version)" -ForegroundColor Green + return +} + +if (-not (Get-Command Get-AuthenticodeSignature -ErrorAction SilentlyContinue)) { + throw 'Get-AuthenticodeSignature is required to verify release artifacts.' +} + +$allowedPins = @( + Get-UpdaterPublisherSpkiPins ` + -SourcePath (Join-Path $repoRoot 'src/update/github_releases.zig') +) +if ($allowedPins.Count -eq 0) { + throw 'Updater publisher-pin allowlist is empty.' +} + +foreach ($architecture in (Get-WindowsPackageArchitectures)) { + $artifactDirectory = Join-Path $ArtifactRoot "noctty-$Version-windows-$architecture" + $setup = Join-Path $artifactDirectory ( + New-WindowsPackageArtifactName ` + -Version $Version ` + -Architecture $architecture ` + -Kind setup + ) + $portable = Join-Path $artifactDirectory ( + New-WindowsPackageArtifactName ` + -Version $Version ` + -Architecture $architecture ` + -Kind portable + ) + $checksums = Join-Path $artifactDirectory ( + New-WindowsPackageArtifactName ` + -Version $Version ` + -Architecture $architecture ` + -Kind checksums + ) + foreach ($path in @($setup, $portable, $checksums)) { + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + throw "Missing required release asset: $path" + } + } + + $checksumEntries = Get-ChecksumEntries -Path $checksums + $expectedChecksumNames = @( + [System.IO.Path]::GetFileName($setup), + [System.IO.Path]::GetFileName($portable) + ) + if ($checksumEntries.Count -ne $expectedChecksumNames.Count -or + @($expectedChecksumNames | Where-Object { + -not $checksumEntries.Contains($_) + }).Count -gt 0) { + throw "$([System.IO.Path]::GetFileName($checksums)) must contain exactly the setup and portable assets for $architecture." + } + foreach ($path in @($setup, $portable)) { + $name = [System.IO.Path]::GetFileName($path) + $hash = Get-FileSha256Lower -Path $path + if (-not $checksumEntries.Contains($name) -or + $checksumEntries[$name] -cne $hash) { + throw "$([System.IO.Path]::GetFileName($checksums)) does not contain the expected hash line for $name." + } + } + + [void](Assert-ReleaseSignature ` + -Path $setup ` + -Label "Setup $architecture" ` + -AllowedPins $allowedPins ` + -TrustSelfSigned $TrustSelfSigned) + + $extractDirectory = Join-Path $ExtractionRoot "noctty-release-verify-$architecture" + if (Test-Path -LiteralPath $extractDirectory) { + Remove-Item -LiteralPath $extractDirectory -Recurse -Force + } + Expand-Archive -LiteralPath $portable -DestinationPath $extractDirectory + foreach ($relativePath in @( + 'noctty/noctty.com', + 'noctty/noctty.exe', + 'noctty/ghostty-vt.dll' + )) { + [void](Assert-ReleaseSignature ` + -Path (Join-Path $extractDirectory $relativePath) ` + -Label "$relativePath $architecture" ` + -AllowedPins $allowedPins ` + -TrustSelfSigned $TrustSelfSigned) + } +} + +Write-Host "Release artifact signature and checksum verification: PASS ($Version)" -ForegroundColor Green diff --git a/scripts/signing-trust.ps1 b/scripts/signing-trust.ps1 index 1b51ea55261..b0b83ca4cfb 100644 --- a/scripts/signing-trust.ps1 +++ b/scripts/signing-trust.ps1 @@ -279,6 +279,72 @@ function Test-SelfSignedTrustStatus { ) } +function Assert-ReleaseSignature { + param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(Mandatory)] + [string] $Label, + + [Parameter(Mandatory)] + [string[]] $AllowedPins, + + [Parameter(Mandatory)] + [bool] $TrustSelfSigned + ) + + $signature = Get-AuthenticodeSignature -LiteralPath $Path + $certificate = $signature.SignerCertificate + if ($null -eq $certificate) { + throw "$Label has no Authenticode signer certificate: $Path" + } + + $statusAccepted = $signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid + if (-not $statusAccepted -and $TrustSelfSigned -and + $certificate.Subject -eq $certificate.Issuer -and + (Test-SelfSignedTrustStatus -Signature $signature -Path $Path)) { + $statusAccepted = $true + } + if (-not $statusAccepted) { + throw "$Label Authenticode signature is not valid: $($signature.Status) $($signature.StatusMessage)" + } + + $pin = Get-CertificateSpkiSha256 -Certificate $certificate + if ($AllowedPins -notcontains $pin) { + throw "$Label signer SPKI SHA-256 is not allowed: $pin" + } + + return [pscustomobject]@{ + Label = $Label + Thumbprint = $certificate.Thumbprint + SpkiSha256 = $pin + Status = [string]$signature.Status + } +} + +function Get-ChecksumEntries { + param( + [Parameter(Mandatory)] + [string] $Path + ) + + $entries = [ordered]@{} + foreach ($line in @(Get-Content -LiteralPath $Path)) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $match = [regex]::Match($line, '^(?[0-9a-fA-F]{64}) \*(?[^\\/]+)$') + if (-not $match.Success) { + throw "Malformed checksum line in $Path`: $line" + } + $name = $match.Groups['name'].Value + if ($entries.Contains($name)) { + throw "Duplicate checksum entry '$name' in $Path." + } + $entries[$name] = $match.Groups['hash'].Value.ToLowerInvariant() + } + return $entries +} + function Import-CodeSigningCertificate { param( [string] $PfxBase64, diff --git a/scripts/update-release-copy.ps1 b/scripts/update-release-copy.ps1 index 5d80a243268..fc307759dd2 100644 --- a/scripts/update-release-copy.ps1 +++ b/scripts/update-release-copy.ps1 @@ -12,7 +12,8 @@ param( ) $ErrorActionPreference = 'Stop' -$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +. (Join-Path $PSScriptRoot 'common.ps1') +$repoRoot = Get-RepoRoot $readmePath = Join-Path $repoRoot 'README.md' $readme = [System.IO.File]::ReadAllText($readmePath) if ($readme -notmatch 'noctty\s+(?\d+\.\d+\.\d+)\]\(https://github\.com/amanthanvi/noctty/releases/tag/v\k\)') { diff --git a/scripts/verify-cloudflare-pages.ps1 b/scripts/verify-cloudflare-pages.ps1 index b68583eb38f..14c03b925d4 100644 --- a/scripts/verify-cloudflare-pages.ps1 +++ b/scripts/verify-cloudflare-pages.ps1 @@ -30,6 +30,7 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'common.ps1') if ([string]::IsNullOrWhiteSpace($ApiToken)) { throw 'CLOUDFLARE_API_TOKEN is required.' @@ -257,8 +258,7 @@ function Get-ManifestEntries { -not (Test-Path -LiteralPath $localPath -PathType Leaf)) { throw 'Site payload manifest references a missing or unsafe local file.' } - $actualHash = (Get-FileHash -LiteralPath $localPath -Algorithm SHA256). - Hash.ToLowerInvariant() + $actualHash = Get-FileSha256Lower -Path $localPath if ($actualHash -cne $match.Groups['hash'].Value) { throw 'Local site payload does not match its SHA-256 manifest.' } @@ -711,9 +711,8 @@ try { [void]$verifiedHosts.Add('www.noctty.com') } - $manifestHash = (Get-FileHash ` - -LiteralPath ([IO.Path]::GetFullPath($ManifestPath)) ` - -Algorithm SHA256).Hash.ToLowerInvariant() + $manifestHash = Get-FileSha256Lower ` + -Path ([IO.Path]::GetFullPath($ManifestPath)) Write-RedactedJson -Path $ProvenancePath -Value ([ordered]@{ schema_version = 'noctty.cloudflare-pages-provenance.v2' verified_at = [DateTimeOffset]::UtcNow.ToString('o') diff --git a/scripts/verify-published-release.ps1 b/scripts/verify-published-release.ps1 index 7f5c0797034..c2c850e7ce3 100644 --- a/scripts/verify-published-release.ps1 +++ b/scripts/verify-published-release.ps1 @@ -8,65 +8,16 @@ param( ) $ErrorActionPreference = 'Stop' -$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) -$repository = 'amanthanvi/noctty' +. (Join-Path $PSScriptRoot 'common.ps1') +$repoRoot = Get-RepoRoot +$repository = if ([string]::IsNullOrWhiteSpace($env:GITHUB_REPOSITORY)) { + 'amanthanvi/noctty' +} else { + $env:GITHUB_REPOSITORY +} . (Join-Path $PSScriptRoot 'windows-architecture.ps1') . (Join-Path $PSScriptRoot 'signing-trust.ps1') -function Assert-PublishedSignature { - param( - [Parameter(Mandatory)] [string]$Path, - [Parameter(Mandatory)] [string]$Label, - [Parameter(Mandatory)] [string[]]$AllowedPins, - [Parameter(Mandatory)] [bool]$TrustSelfSigned - ) - - $signature = Get-AuthenticodeSignature -LiteralPath $Path - $certificate = $signature.SignerCertificate - if ($null -eq $certificate) { - throw "$Label has no Authenticode signer certificate: $Path" - } - $statusAccepted = $signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid - if (-not $statusAccepted -and $TrustSelfSigned -and - $certificate.Subject -eq $certificate.Issuer -and - (Test-SelfSignedTrustStatus -Signature $signature -Path $Path)) { - $statusAccepted = $true - } - if (-not $statusAccepted) { - throw "$Label Authenticode signature is not valid: $($signature.Status) $($signature.StatusMessage)" - } - - $pin = Get-CertificateSpkiSha256 -Certificate $certificate - if ($AllowedPins -notcontains $pin) { - throw "$Label signer SPKI SHA-256 $pin is absent from the updater publisher-pin allowlist." - } - return [pscustomobject]@{ - Label = $Label - Thumbprint = $certificate.Thumbprint - SpkiSha256 = $pin - Status = [string]$signature.Status - } -} - -function Get-ChecksumEntries { - param([Parameter(Mandatory)] [string]$Path) - - $entries = [ordered]@{} - foreach ($line in @(Get-Content -LiteralPath $Path)) { - if ([string]::IsNullOrWhiteSpace($line)) { continue } - $match = [regex]::Match($line, '^(?[0-9a-fA-F]{64}) \*(?[^\\/]+)$') - if (-not $match.Success) { - throw "Malformed checksum line in $Path`: $line" - } - $name = $match.Groups['name'].Value - if ($entries.Contains($name)) { - throw "Duplicate checksum entry '$name' in $Path." - } - $entries[$name] = $match.Groups['hash'].Value.ToLowerInvariant() - } - return $entries -} - if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { throw 'GitHub CLI (gh) is required to verify a published release.' } @@ -131,7 +82,7 @@ try { if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Downloaded release asset is missing: $path" } - $actualHash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() + $actualHash = Get-FileSha256Lower -Path $path $digest = [string]$asset.digest if ($digest -notmatch '^sha256:[0-9a-fA-F]{64}$') { throw "GitHub did not publish a SHA-256 digest for asset $($asset.name)." @@ -162,13 +113,13 @@ try { throw "$checksumsName must contain exactly the setup and portable assets for $architecture." } foreach ($name in $expectedChecksumNames) { - $actualHash = (Get-FileHash -LiteralPath (Join-Path $DownloadDirectory $name) -Algorithm SHA256).Hash.ToLowerInvariant() + $actualHash = Get-FileSha256Lower -Path (Join-Path $DownloadDirectory $name) if ($checksums[$name] -ne $actualHash) { throw "$checksumsName does not match downloaded asset $name." } } - $signatureEvidence.Add((Assert-PublishedSignature ` + $signatureEvidence.Add((Assert-ReleaseSignature ` -Path (Join-Path $DownloadDirectory $setupName) ` -Label "Setup $architecture" ` -AllowedPins $allowedPins ` @@ -181,7 +132,7 @@ try { if (-not (Test-Path -LiteralPath $binaryPath -PathType Leaf)) { throw "$portableName is missing signed binary $relativePath." } - $signatureEvidence.Add((Assert-PublishedSignature ` + $signatureEvidence.Add((Assert-ReleaseSignature ` -Path $binaryPath ` -Label "$relativePath $architecture" ` -AllowedPins $allowedPins ` diff --git a/scripts/windows-build-capabilities.ps1 b/scripts/windows-build-capabilities.ps1 index 8d52e616e0a..c7d1d269078 100644 --- a/scripts/windows-build-capabilities.ps1 +++ b/scripts/windows-build-capabilities.ps1 @@ -1,3 +1,5 @@ +. (Join-Path $PSScriptRoot 'common.ps1') + function Write-WindowsBuildCapabilitiesManifest { [CmdletBinding()] param( @@ -25,9 +27,7 @@ function Write-WindowsBuildCapabilitiesManifest { throw "Cannot record build capability for missing runtime artifact: $runtimePath" } - $artifactHashes[$runtimeFile] = ( - Get-FileHash -LiteralPath $runtimePath -Algorithm SHA256 - ).Hash.ToLowerInvariant() + $artifactHashes[$runtimeFile] = Get-FileSha256Lower -Path $runtimePath } [ordered] @{ @@ -90,9 +90,7 @@ function Assert-WindowsBuildCapabilitiesManifest { throw "Build capability manifest has no hash for $runtimeFile." } - $actualHash = ( - Get-FileHash -LiteralPath $runtimePath -Algorithm SHA256 - ).Hash.ToLowerInvariant() + $actualHash = Get-FileSha256Lower -Path $runtimePath if ($actualHash -cne [string] $hashProperty.Value) { throw "Build capability manifest hash mismatch for $runtimeFile." } diff --git a/site/tests/build-site-assets.test.mjs b/site/tests/build-site-assets.test.mjs index 3b5412d506d..36d0a0bf701 100644 --- a/site/tests/build-site-assets.test.mjs +++ b/site/tests/build-site-assets.test.mjs @@ -1,31 +1,109 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; -import { decodeHtmlEntities } from '../../scripts/build-site-assets.mjs'; +import { getHeaderContract } from "../../scripts/build-site-assets.mjs"; -test('encoded and literal onload handlers decode to the same value', () => { - assert.equal(decodeHtmlEntities("this.media='all'"), "this.media='all'"); - assert.equal(decodeHtmlEntities("this.media='all'"), "this.media='all'"); +const bootstrap = 'document.documentElement.dataset.theme = "dark";'; + +function htmlWithInlineScripts(scripts, bodyAttributes = "") { + return `${scripts.map((script) => ``).join("")}`; +} + +function createSiteFixture( + t, + indexHtml, + notFoundHtml = htmlWithInlineScripts([bootstrap]), +) { + const siteRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "noctty-site-contract-"), + ); + t.after(() => fs.rmSync(siteRoot, { recursive: true, force: true })); + fs.writeFileSync(path.join(siteRoot, "index.html"), indexHtml, "utf8"); + fs.writeFileSync(path.join(siteRoot, "404.html"), notFoundHtml, "utf8"); + return siteRoot; +} + +test("the generated CSP derives one live script hash and forbids event handlers", () => { + const contract = getHeaderContract(); + assert.equal(contract.script_hashes.length, 1); + assert.match(contract.script_hashes[0], /^sha256-[A-Za-z0-9+/]+={0,2}$/); + assert.deepEqual(Object.keys(contract), [ + "generated_headers_base64", + "script_hashes", + "root", + "not_found", + ]); + assert.match( + contract.root.content_security_policy, + /script-src 'self' 'sha256-[^']+';/, + ); + assert.match( + contract.root.content_security_policy, + /script-src-attr 'none';/, + ); }); -test('named and numeric character references decode', () => { - assert.equal(decodeHtmlEntities('a && b'), 'a && b'); - assert.equal(decodeHtmlEntities('<x> "q" 'a''), ` "q" 'a'`); - assert.equal(decodeHtmlEntities(''hex' 'dec''), "'hex' 'dec'"); +test("the generated CSP keeps stylesheet and font origins self-only", () => { + const policy = getHeaderContract().root.content_security_policy; + assert.match(policy, /style-src 'self'; font-src 'self';/); }); -test('text without character references passes through untouched', () => { - const literal = "this.media='all'; window.x = 1 & 2"; - assert.equal(decodeHtmlEntities(literal), literal); +test("the generated catch-all cache policy covers root and not-found responses", () => { + const contract = getHeaderContract(); + assert.equal( + contract.root.cache_control, + "public, max-age=0, must-revalidate", + ); + assert.equal(contract.not_found.cache_control, contract.root.cache_control); +}); + +for (const [name, scripts] of [ + ["zero", []], + ["multiple", [bootstrap, bootstrap]], +]) { + test(`the generated CSP rejects ${name} inline scripts`, (t) => { + const siteRoot = createSiteFixture(t, htmlWithInlineScripts(scripts)); + assert.throws( + () => getHeaderContract(siteRoot), + /Expected exactly one CSP-hashed inline script in site\/index\.html\./, + ); + }); +} + +test("the generated CSP rejects inline event handler attributes", (t) => { + const siteRoot = createSiteFixture( + t, + htmlWithInlineScripts([bootstrap], ' onload="alert(1)"'), + ); + assert.throws( + () => getHeaderContract(siteRoot), + /Expected no inline event handler attributes in site\/index\.html; found 1\./, + ); }); -test('bare ampersands and non-reference text are left as-is', () => { - assert.equal(decodeHtmlEntities('a & b &#zz; &; &'), 'a & b &#zz; &; &'); +test("the generated CSP ignores JavaScript on-property assignments", (t) => { + const script = "window.onerror = () => {};"; + const siteRoot = createSiteFixture( + t, + htmlWithInlineScripts([script]), + htmlWithInlineScripts([script]), + ); + assert.equal(getHeaderContract(siteRoot).script_hashes.length, 1); }); -test('references beyond the supported set fail the build instead of hashing wrong', () => { - assert.throws(() => decodeHtmlEntities('this.media= '), /Unsupported HTML named reference " "/); - assert.throws(() => decodeHtmlEntities('�'), /Unsupported HTML character reference/); - assert.throws(() => decodeHtmlEntities('�'), /Unsupported HTML character reference/); // lone surrogate - assert.throws(() => decodeHtmlEntities('–'), /Unsupported HTML character reference/); // windows-1252 remap range +test("the generated CSP rejects divergent index and not-found bootstraps", (t) => { + const siteRoot = createSiteFixture( + t, + htmlWithInlineScripts([bootstrap]), + htmlWithInlineScripts([ + 'document.documentElement.dataset.theme = "light";', + ]), + ); + assert.throws( + () => getHeaderContract(siteRoot), + /site\/index\.html and site\/404\.html inline bootstrap scripts differ/, + ); }); diff --git a/src/apprt/win32.zig b/src/apprt/win32.zig index c46f1df9dbe..f0c3eee9942 100644 --- a/src/apprt/win32.zig +++ b/src/apprt/win32.zig @@ -58,13 +58,28 @@ const win32_compositor = @import("win32_compositor.zig"); const win32_compositor_native = @import("win32_compositor_native.zig"); const win32_shell = @import("win32_shell.zig"); const win32_ipc = @import("win32_ipc.zig"); +const render_trace = @import("win32/render_trace.zig"); +const gl_startup = @import("win32/gl_startup.zig"); +const labels = @import("win32/labels.zig"); +const win32_input = @import("win32/input.zig"); +const chrome_layout = @import("win32/chrome_layout.zig"); +const gdi = @import("win32/gdi.zig"); +const c = @import("win32/consts.zig"); +const sys = @import("win32/sys.zig"); + +test { + _ = @import("win32/render_trace.zig"); + _ = @import("win32/gl_startup.zig"); + _ = @import("win32/labels.zig"); + _ = @import("win32/input.zig"); + _ = @import("win32/chrome_layout.zig"); + _ = @import("win32/gdi.zig"); +} -// Re-export types from theme module const ThemeColors = win32_theme.ThemeColors; const ButtonColors = win32_theme.ButtonColors; const HostOverlayMode = win32_theme.HostOverlayMode; -// Re-export functions from theme module const darkTheme = win32_theme.darkTheme; const lightTheme = win32_theme.lightTheme; const adjustColor = win32_theme.adjustColor; @@ -81,342 +96,105 @@ const quickSlotChipColors = win32_theme.quickSlotChipColors; const pinnedChipMarkerColor = win32_theme.pinnedChipMarkerColor; const rgb = win32_theme.rgb; -const log = std.log.scoped(.win32); -const windows = std.os.windows; - -var render_trace_file_claimed = std.atomic.Value(bool).init(false); - -pub const resourcesDir = internal_os.resourcesDir; - -const RenderTrace = struct { - const startup_window_ms: u64 = 1000; - const startup_paint_gap_ceiling_ms: u64 = 1500; - const paint_gap_limit_ms: u64 = 300; - - const PaintGapTargets = struct { - max_gap_ms: *std.atomic.Value(u64), - max_gap_ended_at_ms: *std.atomic.Value(u64), - over_limit_count: *std.atomic.Value(u64), - }; - - path: ?[]const u8 = null, - start_tick_ms: u64 = 0, - renderer_update_frame_count: std.atomic.Value(u64) = .init(0), - renderer_draw_request_count: std.atomic.Value(u64) = .init(0), - wakeup_callback_count: std.atomic.Value(u64) = .init(0), - render_callback_count: std.atomic.Value(u64) = .init(0), - renderer_repaint_accept_count: std.atomic.Value(u64) = .init(0), - renderer_repaint_coalesced_count: std.atomic.Value(u64) = .init(0), - queue_paint_count: std.atomic.Value(u64) = .init(0), - queue_paint_update_now_count: std.atomic.Value(u64) = .init(0), - force_paint_now_count: std.atomic.Value(u64) = .init(0), - paint_draw_count: std.atomic.Value(u64) = .init(0), - paint_retry_count: std.atomic.Value(u64) = .init(0), - swap_buffers_count: std.atomic.Value(u64) = .init(0), - max_renderer_update_gap_ms: std.atomic.Value(u64) = .init(0), - max_paint_gap_ms: std.atomic.Value(u64) = .init(0), - max_swap_gap_ms: std.atomic.Value(u64) = .init(0), - max_renderer_update_gap_ended_at_ms: std.atomic.Value(u64) = .init(0), - max_paint_gap_ended_at_ms: std.atomic.Value(u64) = .init(0), - max_swap_gap_ended_at_ms: std.atomic.Value(u64) = .init(0), - max_sustained_paint_gap_ms: std.atomic.Value(u64) = .init(0), - max_sustained_paint_gap_ended_at_ms: std.atomic.Value(u64) = .init(0), - paint_gap_over_limit_count: std.atomic.Value(u64) = .init(0), - max_paint_draw_duration_ms: std.atomic.Value(u64) = .init(0), - max_paint_draw_duration_at_ms: std.atomic.Value(u64) = .init(0), - paint_draw_duration_over_20ms_count: std.atomic.Value(u64) = .init(0), - paint_draw_duration_over_33ms_count: std.atomic.Value(u64) = .init(0), - last_renderer_update_tick_ms: std.atomic.Value(u64) = .init(0), - last_paint_tick_ms: std.atomic.Value(u64) = .init(0), - last_swap_tick_ms: std.atomic.Value(u64) = .init(0), - first_renderer_update_at_ms: std.atomic.Value(u64) = .init(0), - first_paint_at_ms: std.atomic.Value(u64) = .init(0), - first_swap_at_ms: std.atomic.Value(u64) = .init(0), - - fn init(alloc: Allocator) RenderTrace { - const raw = std.process.getEnvVarOwned( - alloc, - "NOCTTY_RENDER_TRACE_FILE", - ) catch return .{}; - errdefer alloc.free(raw); - - const trimmed = std.mem.trim(u8, raw, " \t\r\n"); - if (trimmed.len == 0) { - alloc.free(raw); - return .{}; - } - - const owned = if (trimmed.len == raw.len) - raw - else - alloc.dupe(u8, trimmed) catch { - alloc.free(raw); - return .{}; - }; - if (trimmed.len != raw.len) alloc.free(raw); - - return initWithClaimedPath(alloc, &render_trace_file_claimed, owned); - } - - fn initWithClaimedPath( - alloc: Allocator, - claimed: *std.atomic.Value(bool), - owned: []const u8, - ) RenderTrace { - // A trace path names one JSON document. The interactive harness waits - // for the initial surface before seeding tabs, so the first claimant - // is deterministic. Keeping that claim for the process lifetime stops - // later tab teardown from overwriting the initial surface's evidence. - const trace_path = claimTracePath(alloc, claimed, owned) orelse - return .{}; - return .{ - .path = trace_path, - .start_tick_ms = GetTickCount64(), - }; - } - - fn deinit(self: *RenderTrace, alloc: Allocator) void { - defer { - if (self.path) |path| alloc.free(path); - self.* = .{}; - } - - if (self.path == null) return; - self.writeSnapshot(); - } - - fn enabled(self: *const RenderTrace) bool { - return self.path != null; - } - - fn noteRendererUpdateFrame(self: *RenderTrace) void { - if (!self.enabled()) return; - self.noteTimedCounter( - &self.renderer_update_frame_count, - &self.last_renderer_update_tick_ms, - &self.max_renderer_update_gap_ms, - &self.max_renderer_update_gap_ended_at_ms, - &self.first_renderer_update_at_ms, - null, - ); - } - - fn noteRendererDrawRequest(self: *RenderTrace) void { - if (!self.enabled()) return; - _ = self.renderer_draw_request_count.fetchAdd(1, .acq_rel); - } - - fn noteRendererWakeupCallback(self: *RenderTrace) void { - if (!self.enabled()) return; - _ = self.wakeup_callback_count.fetchAdd(1, .acq_rel); - } - - fn noteRendererFollowupCallback(self: *RenderTrace) void { - if (!self.enabled()) return; - _ = self.render_callback_count.fetchAdd(1, .acq_rel); - } - - fn noteRendererRepaintAccepted(self: *RenderTrace) void { - if (!self.enabled()) return; - _ = self.renderer_repaint_accept_count.fetchAdd(1, .acq_rel); - } - - fn noteRendererRepaintCoalesced(self: *RenderTrace) void { - if (!self.enabled()) return; - _ = self.renderer_repaint_coalesced_count.fetchAdd(1, .acq_rel); - } - - fn noteQueuePaint(self: *RenderTrace, update_now: bool) void { - if (!self.enabled()) return; - _ = self.queue_paint_count.fetchAdd(1, .acq_rel); - if (update_now) _ = self.queue_paint_update_now_count.fetchAdd(1, .acq_rel); - } - - fn noteForcePaintNow(self: *RenderTrace) void { - if (!self.enabled()) return; - _ = self.force_paint_now_count.fetchAdd(1, .acq_rel); - } - - fn notePaintDraw(self: *RenderTrace) void { - if (!self.enabled()) return; - self.noteTimedCounter( - &self.paint_draw_count, - &self.last_paint_tick_ms, - &self.max_paint_gap_ms, - &self.max_paint_gap_ended_at_ms, - &self.first_paint_at_ms, - .{ - .max_gap_ms = &self.max_sustained_paint_gap_ms, - .max_gap_ended_at_ms = &self.max_sustained_paint_gap_ended_at_ms, - .over_limit_count = &self.paint_gap_over_limit_count, - }, - ); - } - - fn notePaintDrawDuration(self: *RenderTrace, duration_ms: u64) void { - if (!self.enabled()) return; - updateMaxAtomicWithTimestamp( - &self.max_paint_draw_duration_ms, - &self.max_paint_draw_duration_at_ms, - duration_ms, - elapsedTraceMs(self.start_tick_ms, GetTickCount64()), - ); - if (duration_ms > 20) _ = self.paint_draw_duration_over_20ms_count.fetchAdd(1, .acq_rel); - if (duration_ms > 33) _ = self.paint_draw_duration_over_33ms_count.fetchAdd(1, .acq_rel); - } - - fn notePaintRetry(self: *RenderTrace) void { - if (!self.enabled()) return; - _ = self.paint_retry_count.fetchAdd(1, .acq_rel); - } - - fn noteSwapBuffers(self: *RenderTrace) void { - if (!self.enabled()) return; - self.noteTimedCounter( - &self.swap_buffers_count, - &self.last_swap_tick_ms, - &self.max_swap_gap_ms, - &self.max_swap_gap_ended_at_ms, - &self.first_swap_at_ms, - null, - ); - } - - fn noteTimedCounter( - self: *RenderTrace, - counter: *std.atomic.Value(u64), - last_tick_ms: *std.atomic.Value(u64), - max_gap_ms: *std.atomic.Value(u64), - max_gap_ended_at_ms: *std.atomic.Value(u64), - first_at_ms: *std.atomic.Value(u64), - paint_gaps: ?PaintGapTargets, - ) void { - const now = GetTickCount64(); - const elapsed_ms = elapsedTraceMs(self.start_tick_ms, now); - _ = counter.fetchAdd(1, .acq_rel); - _ = first_at_ms.cmpxchgStrong(0, elapsed_ms, .acq_rel, .acquire); - const prev = last_tick_ms.swap(now, .acq_rel); - if (prev == 0 or now <= prev) return; - const gap_ms = now - prev; - updateMaxAtomicWithTimestamp( - max_gap_ms, - max_gap_ended_at_ms, - gap_ms, - elapsed_ms, - ); - if (paint_gaps) |targets| { - if (gapExceedsPaintLimit(gap_ms)) { - _ = targets.over_limit_count.fetchAdd(1, .acq_rel); - } - if (gapIsSustained(elapsed_ms, gap_ms)) { - updateMaxAtomicWithTimestamp( - targets.max_gap_ms, - targets.max_gap_ended_at_ms, - gap_ms, - elapsed_ms, - ); - } - } - } - - fn gapIsSustained(elapsed_ms: u64, gap_ms: u64) bool { - return elapsed_ms -| gap_ms >= startup_window_ms; - } - - fn gapExceedsPaintLimit(gap_ms: u64) bool { - return gap_ms > paint_gap_limit_ms; - } - - fn writeSnapshot(self: *const RenderTrace) void { - const trace_path = self.path orelse return; - const file = std.fs.createFileAbsolute(trace_path, .{ .truncate = true }) catch return; - defer file.close(); - - var buffer: [1024]u8 = undefined; - var writer = file.writer(&buffer); - const stream = &writer.interface; - stream.print("{f}", .{std.json.fmt(.{ - .runtime_ms = GetTickCount64() - self.start_tick_ms, - .startup_window_ms = startup_window_ms, - .startup_paint_gap_ceiling_ms = startup_paint_gap_ceiling_ms, - .paint_gap_limit_ms = paint_gap_limit_ms, - .renderer_update_frame_count = self.renderer_update_frame_count.load(.acquire), - .renderer_draw_request_count = self.renderer_draw_request_count.load(.acquire), - .wakeup_callback_count = self.wakeup_callback_count.load(.acquire), - .render_callback_count = self.render_callback_count.load(.acquire), - .renderer_repaint_accept_count = self.renderer_repaint_accept_count.load(.acquire), - .renderer_repaint_coalesced_count = self.renderer_repaint_coalesced_count.load(.acquire), - .queue_paint_count = self.queue_paint_count.load(.acquire), - .queue_paint_update_now_count = self.queue_paint_update_now_count.load(.acquire), - .force_paint_now_count = self.force_paint_now_count.load(.acquire), - .paint_draw_count = self.paint_draw_count.load(.acquire), - .paint_retry_count = self.paint_retry_count.load(.acquire), - .swap_buffers_count = self.swap_buffers_count.load(.acquire), - .max_renderer_update_gap_ms = self.max_renderer_update_gap_ms.load(.acquire), - .max_paint_gap_ms = self.max_paint_gap_ms.load(.acquire), - .max_swap_gap_ms = self.max_swap_gap_ms.load(.acquire), - .max_renderer_update_gap_ended_at_ms = self.max_renderer_update_gap_ended_at_ms.load(.acquire), - .max_paint_gap_ended_at_ms = self.max_paint_gap_ended_at_ms.load(.acquire), - .max_swap_gap_ended_at_ms = self.max_swap_gap_ended_at_ms.load(.acquire), - .max_sustained_paint_gap_ms = self.max_sustained_paint_gap_ms.load(.acquire), - .max_sustained_paint_gap_ended_at_ms = self.max_sustained_paint_gap_ended_at_ms.load(.acquire), - .paint_gap_over_limit_count = self.paint_gap_over_limit_count.load(.acquire), - .max_paint_draw_duration_ms = self.max_paint_draw_duration_ms.load(.acquire), - .max_paint_draw_duration_at_ms = self.max_paint_draw_duration_at_ms.load(.acquire), - .paint_draw_duration_over_20ms_count = self.paint_draw_duration_over_20ms_count.load(.acquire), - .paint_draw_duration_over_33ms_count = self.paint_draw_duration_over_33ms_count.load(.acquire), - .first_renderer_update_at_ms = self.first_renderer_update_at_ms.load(.acquire), - .first_paint_at_ms = self.first_paint_at_ms.load(.acquire), - .first_swap_at_ms = self.first_swap_at_ms.load(.acquire), - }, .{})}) catch return; - stream.flush() catch return; - } +const rectEquals = chrome_layout.rectEquals; +const childRect = chrome_layout.childRect; +const layoutRectToWin32 = chrome_layout.layoutRectToWin32; +const centeredRect = chrome_layout.centeredRect; +const overlayEditFrameRect = chrome_layout.overlayEditFrameRect; +const overlayLabelReservation = chrome_layout.overlayLabelReservation; +const overlayActionLayoutForWidth = chrome_layout.overlayActionLayoutForWidth; +const overlayEditChildRectFromFrame = chrome_layout.overlayEditChildRectFromFrame; +const blendColorRGB = gdi.blendColorRGB; +const fillSolidRect = gdi.fillSolidRect; +const drawRectBorder = gdi.drawRectBorder; +const textOutWz = gdi.textOutWz; +const drawTextWz = gdi.drawTextWz; +const drawRoundedRect = gdi.drawRoundedRect; +const paintRectVisible = gdi.paintRectVisible; + +const ThemeSurface = enum { + tab_accent, + caption_cluster_bg, + caption_cluster_border, + overlay_panel_bg, + overlay_panel_border, + search_frame_bg, + search_frame_border, + search_frame_underline, + update_open_bg, + update_dismiss_bg, + launcher_lane_bg, + launcher_lane_border, }; -fn claimTraceFile(claimed: *std.atomic.Value(bool)) bool { - return !claimed.swap(true, .acq_rel); +fn themeSurface(theme: *const ThemeColors, surface: ThemeSurface) u32 { + return switch (surface) { + .tab_accent => if (theme.is_dark) + adjustColor(theme.accent, -12, -12, -12) + else + adjustColor(theme.accent, 18, 18, 18), + .caption_cluster_bg => if (theme.is_dark) + adjustColor(theme.chrome_bg, 8, 8, 10) + else + adjustColor(theme.chrome_bg, 6, 6, 6), + .caption_cluster_border => if (theme.is_dark) + adjustColor(theme.chrome_border, 10, 10, 12) + else + adjustColor(theme.chrome_border, -16, -16, -16), + .overlay_panel_bg => if (theme.is_dark) + adjustColor(theme.overlay_bg, 4, 4, 6) + else + adjustColor(theme.overlay_bg, -4, -4, -4), + .overlay_panel_border => if (theme.is_dark) + adjustColor(theme.overlay_border, 10, 10, 12) + else + adjustColor(theme.overlay_border, -18, -18, -18), + .search_frame_bg => if (theme.is_dark) + adjustColor(theme.overlay_bg, 3, 3, 5) + else + adjustColor(theme.overlay_bg, -2, -2, -1), + .search_frame_border => if (theme.is_dark) + adjustColor(theme.overlay_border, 10, 10, 12) + else + adjustColor(theme.overlay_border, -14, -14, -14), + .search_frame_underline => if (theme.is_dark) + adjustColor(theme.overlay_border, -10, -10, -8) + else + adjustColor(theme.overlay_border, 10, 10, 10), + .update_open_bg => if (theme.is_dark) + adjustColor(theme.overlay_bg, 10, 10, 12) + else + adjustColor(theme.overlay_bg, -4, -4, -4), + .update_dismiss_bg => if (theme.is_dark) + adjustColor(theme.status_bg, 10, 10, 12) + else + adjustColor(theme.status_bg, -4, -4, -4), + .launcher_lane_bg => if (theme.is_dark) + adjustColor(theme.status_bg, 8, 8, 10) + else + adjustColor(theme.status_bg, -6, -6, -6), + .launcher_lane_border => if (theme.is_dark) + adjustColor(theme.chrome_border, 12, 12, 14) + else + adjustColor(theme.chrome_border, -18, -18, -18), + }; } -fn claimTracePath( - alloc: Allocator, - claimed: *std.atomic.Value(bool), - owned: []const u8, -) ?[]const u8 { - if (claimTraceFile(claimed)) return owned; - log.debug("render trace path already has a process owner; tracing disabled for this surface", .{}); - alloc.free(owned); - return null; -} +const log = std.log.scoped(.win32); +const windows = std.os.windows; -fn elapsedTraceMs(start_tick_ms: u64, now_tick_ms: u64) u64 { - if (now_tick_ms <= start_tick_ms) return 0; - return now_tick_ms - start_tick_ms; -} +pub const resourcesDir = internal_os.resourcesDir; -fn updateMaxAtomic(value: *std.atomic.Value(u64), candidate: u64) void { - var current = value.load(.acquire); - while (candidate > current) { - current = value.cmpxchgWeak(current, candidate, .acq_rel, .acquire) orelse return; - } -} +const RenderTrace = render_trace.RenderTrace; -fn updateMaxAtomicWithTimestamp( - value: *std.atomic.Value(u64), - at_ms: *std.atomic.Value(u64), - candidate: u64, - candidate_at_ms: u64, -) void { - var current = value.load(.acquire); - while (candidate > current) { - const observed = value.cmpxchgWeak(current, candidate, .acq_rel, .acquire); - if (observed) |next| { - current = next; - continue; - } - at_ms.store(candidate_at_ms, .release); - return; - } -} +pub const StartupLoaderErrorDialogSuppression = gl_startup.StartupLoaderErrorDialogSuppression; +pub const suppressStartupLoaderErrorDialogs = gl_startup.suppressStartupLoaderErrorDialogs; +pub const OpenGLStartupStep = gl_startup.OpenGLStartupStep; +const beginOpenGLStartupDiagnostics = gl_startup.beginOpenGLStartupDiagnostics; +pub const clearOpenGLStartupFailure = gl_startup.clearOpenGLStartupFailure; +const recordOpenGLStartupWin32Failure = gl_startup.recordOpenGLStartupWin32Failure; +pub const recordOpenGLStartupError = gl_startup.recordOpenGLStartupError; +pub const reportStartupFailure = gl_startup.reportStartupFailure; const ATOM = win32_types.ATOM; const LPCWSTR = win32_types.LPCWSTR; @@ -442,124 +220,8 @@ const HWND = win32_types.HWND; const HINSTANCE = win32_types.HINSTANCE; const INTRESOURCE = win32_types.INTRESOURCE; -const CS_OWNDC = 0x0020; -const CW_USEDEFAULT = @as(i32, @bitCast(@as(u32, 0x80000000))); -const GWLP_USERDATA = -21; -const GWLP_WNDPROC = -4; -const GWL_STYLE = -16; -const GWL_EXSTYLE = -20; -const IDC_ARROW = @as(INTRESOURCE, @ptrFromInt(32512)); -const ID_ICON_GHOSTTY = 1; -const IMAGE_ICON = 1; -const LR_SHARED = 0x00008000; -const SM_CXICON = 11; -const SM_CYICON = 12; -const SM_CXSMICON = 49; -const SM_CYSMICON = 50; -const WM_SETICON = 0x0080; -const ICON_SMALL = 0; -const ICON_BIG = 1; -const HTNOWHERE = 0; -const HTCLIENT = 1; -const HTCAPTION = 2; -const HTSYSMENU = 3; -const HTMINBUTTON = 8; -const HTMAXBUTTON = 9; -const HTLEFT = 10; -const HTRIGHT = 11; -const HTTOP = 12; -const HTTOPLEFT = 13; -const HTTOPRIGHT = 14; -const HTBOTTOM = 15; -const HTBOTTOMLEFT = 16; -const HTBOTTOMRIGHT = 17; -const HTCLOSE = 20; const HWND_TOPMOST: ?*anyopaque = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); const HWND_NOTOPMOST: ?*anyopaque = @ptrFromInt(@as(usize, @bitCast(@as(isize, -2)))); -const SW_SHOW = 5; -const SW_SHOWNOACTIVATE = 4; -const SW_RESTORE = 9; -const SW_MAXIMIZE = 3; -const SW_HIDE = 0; -const WM_APP = 0x8000; -const WM_ACTIVATE = 0x0006; -const WA_INACTIVE = 0; -const WM_COMMAND = 0x0111; -const WM_CAPTURECHANGED = 0x0215; -const WM_CLOSE = 0x0010; -const WM_DESTROY = 0x0002; -const WM_DRAWITEM = 0x002B; -const WM_ERASEBKGND = 0x0014; -const WM_GETMINMAXINFO = 0x0024; -const WM_CHAR = 0x0102; -const WM_DEADCHAR = 0x0103; -const WM_HOTKEY = 0x0312; -const WM_IME_SETCONTEXT = 0x0281; -const WM_IME_STARTCOMPOSITION = 0x010D; -const WM_IME_ENDCOMPOSITION = 0x010E; -const WM_IME_COMPOSITION = 0x010F; -const GCS_COMPSTR: u32 = 0x0008; -const GCS_RESULTSTR: u32 = 0x0800; -const CFS_POINT: u32 = 0x0002; -const CFS_EXCLUDE: u32 = 0x0080; -const ISC_SHOWUICOMPOSITIONWINDOW: LPARAM = 0x80000000; -const WM_KILLFOCUS = 0x0008; -const WM_KEYDOWN = 0x0100; -const WM_KEYUP = 0x0101; -const WM_LBUTTONDOWN = 0x0201; -const WM_LBUTTONUP = 0x0202; -const WM_LBUTTONDBLCLK = 0x0203; -const WM_MBUTTONDOWN = 0x0207; -const WM_MBUTTONUP = 0x0208; -const WM_MOUSEHWHEEL = 0x020E; -const WM_MOUSEMOVE = 0x0200; -const WM_MOUSEWHEEL = 0x020A; -const WM_MOUSELEAVE = 0x02A3; -const WM_POINTERHWHEEL = 0x024F; -const WM_POINTERWHEEL = 0x024E; -const WM_NCCREATE = 0x0081; -const WM_NCCALCSIZE = 0x0083; -const WM_NCHITTEST = 0x0084; -const WM_NCMOUSEMOVE = 0x00A0; -const WM_NCLBUTTONDOWN = 0x00A1; -const WM_NCLBUTTONUP = 0x00A2; -const WM_NCMOUSELEAVE = 0x02A2; -const WM_SYSCOMMAND = 0x0112; -const SC_CLOSE: WPARAM = 0xF060; -const SC_MINIMIZE: WPARAM = 0xF020; -const SC_MAXIMIZE: WPARAM = 0xF030; -const SC_RESTORE: WPARAM = 0xF120; -const WM_PAINT = 0x000F; -const WM_QUIT = 0x0012; -const WM_TIMER = 0x0113; -const WM_CTLCOLOREDIT = 0x0133; -const WM_CTLCOLORBTN = 0x0135; -const WM_CTLCOLORSTATIC = 0x0138; -const WM_GETOBJECT: UINT = 0x003D; -const WM_RBUTTONDOWN = 0x0204; -const WM_XBUTTONDOWN = 0x020B; -const WM_XBUTTONUP = 0x020C; -const XBUTTON1: WORD = 0x0001; -const XBUTTON2: WORD = 0x0002; -const WM_RBUTTONUP = 0x0205; -const WM_SETCURSOR = 0x0020; -const WM_SETFOCUS = 0x0007; -const WM_SETTINGCHANGE = 0x001A; -const WM_SIZE = 0x0005; -const WM_ENTERSIZEMOVE = 0x0231; -const WM_EXITSIZEMOVE = 0x0232; -const SIZE_MAXIMIZED: u32 = 2; -const SIZE_MINIMIZED: u32 = 1; -const SIZE_RESTORED: u32 = 0; -const WM_SYSKEYDOWN = 0x0104; -const WM_SYSKEYUP = 0x0105; -const WM_SYSDEADCHAR = 0x0107; -const WM_WINHOSTTY_WAKE = WM_APP + 1; -const WM_WINHOSTTY_UPDATE = WM_APP + 2; -const WM_WINHOSTTY_TOAST_ACTIVATION = WM_APP + 3; -const WM_WINHOSTTY_HOST_NEW_TAB = WM_APP + 4; -const WM_WINHOSTTY_UIA_DISCONNECT = WM_APP + 5; -const WM_WINHOSTTY_UIA_QUERY_REFRESH = WM_APP + 6; const DeferredUiaDisconnect = struct { ctx: *anyopaque, @@ -567,99 +229,14 @@ const DeferredUiaDisconnect = struct { release: *const fn (*anyopaque) void, retries: u8 = 0, }; -const PM_NOREMOVE: UINT = 0x0000; -const PM_REMOVE: UINT = 0x0001; -const WS_OVERLAPPED = 0x00000000; -const WS_CHILD = 0x40000000; -const WS_CLIPCHILDREN = 0x02000000; -const WS_CLIPSIBLINGS = 0x04000000; -const WS_CAPTION = 0x00C00000; -const WS_SYSMENU = 0x00080000; -const WS_THICKFRAME = 0x00040000; -const WS_MINIMIZEBOX = 0x00020000; -const WS_MAXIMIZEBOX = 0x00010000; -const WS_VISIBLE = 0x10000000; -const WS_TABSTOP = 0x00010000; -const WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; -const WS_POPUP = 0x80000000; -const WS_EX_LAYERED = 0x00080000; -const WS_EX_TRANSPARENT = 0x00000020; -const MOD_ALT = 0x0001; -const MOD_CONTROL = 0x0002; -const MOD_SHIFT = 0x0004; -const MOD_WIN = 0x0008; -const TO_UNICODE_NO_STATE_CHANGE: UINT = 0x0004; -const SWP_NOSIZE = 0x0001; -const SWP_NOMOVE = 0x0002; -const SWP_NOZORDER = 0x0004; -const SWP_NOACTIVATE = 0x0010; -const SWP_FRAMECHANGED = 0x0020; -const RDW_INVALIDATE: UINT = 0x0001; -const RDW_INTERNALPAINT: UINT = 0x0002; -const RDW_ERASE: UINT = 0x0004; -const RDW_NOCHILDREN: UINT = 0x0040; -const RDW_ALLCHILDREN: UINT = 0x0080; -const RDW_UPDATENOW: UINT = 0x0100; -const RDW_FRAME: UINT = 0x0400; -const MONITOR_DEFAULTTONEAREST = 0x00000002; -const MONITOR_DEFAULTTOPRIMARY = 0x00000001; -const MONITORINFOF_PRIMARY = 0x00000001; -const COLOR_WINDOW = 5; -const CF_UNICODETEXT = 13; +const WS_OVERLAPPEDWINDOW = c.WS_OVERLAPPED | c.WS_CAPTION | c.WS_SYSMENU | c.WS_THICKFRAME | c.WS_MINIMIZEBOX | c.WS_MAXIMIZEBOX; /// CF_HTML: registered clipboard format name is the literal string /// "HTML Format". The ID is process-local — we cache it on App. const CF_HTML_NAME = std.unicode.utf8ToUtf16LeStringLiteral("HTML Format"); -extern "user32" fn RegisterClipboardFormatW(lpszFormat: [*:0]const u16) callconv(.winapi) UINT; -const GMEM_MOVEABLE = 0x0002; -const GMEM_ZEROINIT = 0x0040; -const LWA_COLORKEY = 0x00000001; -const LWA_ALPHA = 0x00000002; -const TRANSPARENT = 1; -const OPAQUE = 2; -const MB_OK = 0x00000000; -const MB_ICONERROR = 0x00000010; -const MB_ICONINFORMATION = 0x00000040; -const MB_SETFOREGROUND = 0x00010000; -const MK_CONTROL = 0x0008; -const MK_LBUTTON = 0x0001; -const MK_MBUTTON = 0x0010; -const MK_RBUTTON = 0x0002; fn hostCompositionRedrawFlags() UINT { - return RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_NOCHILDREN | RDW_UPDATENOW; -} -const MK_SHIFT = 0x0004; -const MK_XBUTTON1 = 0x0020; -const MK_XBUTTON2 = 0x0040; -const EN_CHANGE = 0x0300; -const BS_OWNERDRAW = 0x0000000B; -const SS_RIGHT = 0x00000002; -const SS_CENTERIMAGE = 0x00000200; -const SS_OWNERDRAW = 0x0000000D; -const ES_AUTOHSCROLL = 0x0080; -const BN_CLICKED = 0; -const BN_SETFOCUS = 6; -const BN_KILLFOCUS = 7; -const EM_SETSEL = 0x00B1; -const EM_GETSEL = 0x00B0; -const EM_CHARFROMPOS = 0x00D7; -const EM_SETMARGINS = 0x00D3; -const EM_SETCUEBANNER = 0x1501; -const EC_LEFTMARGIN: usize = 0x0001; -const EC_RIGHTMARGIN: usize = 0x0002; -const ODT_BUTTON = 4; -const ODT_STATIC = 5; -const ODS_SELECTED = 0x0001; -const ODS_DISABLED = 0x0004; -const ODS_FOCUS = 0x0010; -const TME_LEAVE = 0x00000002; -const TME_NONCLIENT = 0x00000010; -const DT_CENTER = 0x00000001; -const DT_VCENTER = 0x00000004; -const DT_SINGLELINE = 0x00000020; -const DT_NOPREFIX = 0x00000800; -const DT_END_ELLIPSIS = 0x00008000; -const DT_LEFT = 0x00000000; + return c.RDW_INVALIDATE | c.RDW_ERASE | c.RDW_FRAME | c.RDW_NOCHILDREN | c.RDW_UPDATENOW; +} // ── Chrome metric constants ───────────────────────────────────────────── // // These pull from `win32_theme.ThemeMetrics` defaults so there is a single @@ -682,8 +259,6 @@ const host_overlay_cancel_width: i32 = default_metrics.overlay_cancel_width; const host_tab_small_button_width: i32 = default_metrics.tab_small_button_width; const host_tab_overflow_button_width: i32 = default_metrics.tab_overflow_button_width; const host_titlebar_action_button_size: i32 = 32; -const host_tab_label_max_len: usize = default_metrics.tab_label_max_len; -const host_tab_min_button_width: i32 = default_metrics.tab_min_width; /// Non-owning view over the palette's command lists. Points into /// config arena storage — lifetime matches `app.config`. Re-exported /// from `win32_palette.zig` so it can be shared with the bench harness. @@ -700,12 +275,7 @@ const palette_catalog_capacity: usize = 768; const palette_action_capacity: usize = 384; const palette_catalog_label_capacity: usize = 768; const palette_catalog_label_bytes: usize = 128; -const PalettePresentation = struct { - match_count: usize = 0, - title: ?[]const u8 = null, - subtitle: ?[]const u8 = null, - available: bool = false, -}; +const PalettePresentation = labels.PalettePresentation; const PaletteListTransition = struct { relayout: bool, exposes_content: bool, @@ -729,454 +299,58 @@ const PaletteCatalogConfigSource = enum { /// Catalog strings borrow `palette_catalog_retained_config`. retained, }; -const PaletteCompletion = struct { - text: []const u8, - id: PaletteStableId, -}; +const PaletteCompletion = labels.PaletteCompletion; const tokenizePaletteQuery = win32_palette.tokenizeQuery; const rankPaletteEntry = win32_palette.rankEntry; const rankedIndicesForQuery = win32_palette.rankedForQuery; const releases_url = updatepkg.releases_url; -const WM_THEMECHANGED = 0x031A; -const WM_SYSCOLORCHANGE = 0x0015; -const WM_DWMCOLORIZATIONCOLORCHANGED: UINT = 0x0320; -const WM_DPICHANGED: UINT = 0x02E0; -const DWMSBT_NONE: u32 = 1; -/// Mica-tabbed backdrop for main windows with visible tab strips. -/// Win11 22H2+ (build >= 22621). -const DWMSBT_TABBEDWINDOW: u32 = 4; -const OS_BUILD_WIN10_22H2: u32 = 19045; -const OS_BUILD_WIN11_21H2: u32 = 22000; -const OS_BUILD_WIN11_22H2: u32 = 22621; -const DC_BRUSH: i32 = 18; -const DC_PEN: i32 = 19; -const PS_SOLID: i32 = 0; const host_tab_max_button_width: i32 = default_metrics.tab_max_width; const host_tab_close_zone_width: i32 = default_metrics.tab_close_zone; // per-tab close button hit zone width -const SPI_GETHIGHCONTRAST: UINT = 0x0042; -const SPI_GETNONCLIENTMETRICS: UINT = 0x0029; -const SPI_GETCLIENTAREAANIMATION: UINT = 0x1042; - -/// See src/apprt/win32_tween.zig for the scheduler contract. -const TWEEN_TIMER_ID: UINT_PTR = 0x77684701; // "whgT1" in 32-bit hex -const TWEEN_TIMER_INTERVAL_MS: UINT = 16; -const SEARCH_TIMER_ID: UINT_PTR = 0x77684702; // "whgT2" in 32-bit hex -const SEARCH_TIMER_INTERVAL_MS: UINT = 20; -const SCROLLBAR_TIMER_ID: UINT_PTR = 0x77684703; // "whgT3" in 32-bit hex -const SCROLLBAR_TIMER_INTERVAL_MS: UINT = 16; -const RESIZE_SETTLE_TIMER_ID: UINT_PTR = 0x77684704; // "whgT4" in 32-bit hex -const RESIZE_SETTLE_TIMER_INTERVAL_MS: UINT = 16; -const TERMINAL_UIA_TIMER_ID: UINT_PTR = 0x77684705; // "whgT5" in 32-bit hex -const RESIZE_SETTLE_REPAINT_TICKS: u8 = 12; -const FW_NORMAL: i32 = 400; -const DEFAULT_CHARSET: u8 = 1; -const OUT_DEFAULT_PRECIS: u8 = 0; -const CLIP_DEFAULT_PRECIS: u8 = 0; -const CLEARTYPE_QUALITY: u8 = 5; -const DEFAULT_PITCH: u8 = 0; -const FF_DONTCARE: u8 = 0; -const LF_FACESIZE = 32; -const HCF_HIGHCONTRASTON: DWORD = 0x00000001; -const COLOR_WINDOWTEXT = 8; -const COLOR_WINDOWFRAME = 6; -const COLOR_BTNFACE = 15; -const COLOR_BTNTEXT = 18; -const COLOR_GRAYTEXT = 17; -const COLOR_HIGHLIGHT = 13; -const COLOR_HIGHLIGHTTEXT = 14; -const HKEY_CURRENT_USER: usize = 0x80000001; -const KEY_READ: DWORD = 0x20019; -const REG_DWORD: DWORD = 4; -const ERROR_SUCCESS: i32 = 0; -const ERROR_MOD_NOT_FOUND: DWORD = 126; -const PFD_DRAW_TO_WINDOW = 0x00000004; -const PFD_SUPPORT_OPENGL = 0x00000020; -const PFD_DOUBLEBUFFER = 0x00000001; -const PFD_TYPE_RGBA = 0; -const PFD_MAIN_PLANE = 0; -const SEM_FAILCRITICALERRORS: DWORD = 0x0001; -const SEM_NOOPENFILEERRORBOX: DWORD = 0x8000; // Context menu constants -const MF_STRING: UINT = 0x00000000; -const MF_SEPARATOR: UINT = 0x00000800; -const MF_GRAYED: UINT = 0x00000001; -const TPM_LEFTALIGN: UINT = 0x0000; -const TPM_TOPALIGN: UINT = 0x0000; -const TPM_RETURNCMD: UINT = 0x0100; -const TPM_RIGHTBUTTON: UINT = 0x0002; -const WM_NULL: UINT = 0x0000; -const WM_SETFONT: UINT = 0x0030; -const CTX_COPY: usize = 4001; -const CTX_PASTE: usize = 4002; -const CTX_SELECT_ALL: usize = 4003; -const CTX_FIND: usize = 4004; -const CTX_COMMAND_PALETTE: usize = 4005; -const CTX_NEW_TAB: usize = 4006; -const CTX_SPLIT_RIGHT: usize = 4007; -const CTX_NEW_WINDOW: usize = 4008; -const CTX_INSPECTOR: usize = 4009; -const CTX_SPLIT_DOWN: usize = 4010; -const CTX_SPLIT_LEFT: usize = 4011; -const CTX_SPLIT_UP: usize = 4012; -const CTX_TAB_RENAME: usize = 4020; -const CTX_TAB_CLOSE: usize = 4021; -const CTX_TAB_CLOSE_OTHERS: usize = 4022; -const CTX_TAB_MOVE_LEFT: usize = 4023; -const CTX_TAB_MOVE_RIGHT: usize = 4024; -const CTX_PROFILE_BASE: usize = 4100; // profile dropdown items: CTX_PROFILE_BASE + index -const SEARCH_BG_ID: usize = 2100; -const SEARCH_EDIT_ID: usize = 2101; -const SEARCH_PREV_ID: usize = 2102; -const SEARCH_NEXT_ID: usize = 2103; -const SEARCH_REGEX_ID: usize = 2104; -const SEARCH_CASE_ID: usize = 2105; -const SEARCH_WORD_ID: usize = 2106; -const SEARCH_RESULTS_ID: usize = 2107; -const SEARCH_CLOSE_ID: usize = 2108; -const MF_POPUP: UINT = 0x00000010; -const MF_CHECKED: UINT = 0x00000008; const WNDPROC = win32_types.WNDPROC; const SHORT = i16; -const VK_BACK = 0x08; -const VK_TAB = 0x09; -const VK_RETURN = 0x0D; -const VK_SHIFT = 0x10; -const VK_CONTROL = 0x11; -const VK_MENU = 0x12; -const VK_PAUSE = 0x13; -const VK_CAPITAL = 0x14; -const VK_ESCAPE = 0x1B; -const VK_SPACE = 0x20; -const VK_PRIOR = 0x21; -const VK_NEXT = 0x22; -const VK_END = 0x23; -const VK_HOME = 0x24; -const VK_LEFT = 0x25; -const VK_UP = 0x26; -const VK_RIGHT = 0x27; -const VK_DOWN = 0x28; -const VK_SNAPSHOT = 0x2C; -const VK_INSERT = 0x2D; -const VK_DELETE = 0x2E; -const VK_0 = 0x30; -const VK_9 = 0x39; -const VK_A = 0x41; -const VK_Z = 0x5A; -const VK_LWIN = 0x5B; -const VK_RWIN = 0x5C; -const VK_APPS = 0x5D; -const VK_NUMPAD0 = 0x60; -const VK_NUMPAD9 = 0x69; -const VK_MULTIPLY = 0x6A; -const VK_ADD = 0x6B; -const VK_SEPARATOR = 0x6C; -const VK_SUBTRACT = 0x6D; -const VK_DECIMAL = 0x6E; -const VK_DIVIDE = 0x6F; -const VK_F1 = 0x70; -const VK_F3 = 0x72; -const VK_F2 = 0x71; -const VK_F24 = 0x87; -const VK_NUMLOCK = 0x90; -const VK_SCROLL = 0x91; -const VK_LSHIFT = 0xA0; -const VK_RSHIFT = 0xA1; -const VK_LCONTROL = 0xA2; -const VK_RCONTROL = 0xA3; -const VK_LMENU = 0xA4; -const VK_RMENU = 0xA5; -const VK_OEM_1 = 0xBA; -const VK_OEM_PLUS = 0xBB; -const VK_OEM_COMMA = 0xBC; -const VK_OEM_MINUS = 0xBD; -const VK_OEM_PERIOD = 0xBE; -const VK_OEM_2 = 0xBF; -const VK_OEM_3 = 0xC0; -const VK_OEM_4 = 0xDB; -const VK_OEM_5 = 0xDC; -const VK_OEM_6 = 0xDD; -const VK_OEM_7 = 0xDE; -const VK_PACKET = 0xE7; - -const KF_EXTENDED = 1 << 24; -const KF_REPEAT = 1 << 30; -const SPI_GETWHEELSCROLLLINES = 0x0068; -const SPI_GETWHEELSCROLLCHARS = 0x006C; -const WHEEL_DELTA = 120; -const WHEEL_PAGESCROLL = 0xFFFF_FFFF; -const ERROR_FILE_NOT_FOUND = 2; -const PIPE_READMODE_BYTE = 0x00000000; -const PIPE_WAIT = 0x00000000; -const PIPE_ACCESS_DUPLEX = 0x00000003; -const PIPE_UNLIMITED_INSTANCES = 255; const ipc_poll_interval_ns: u64 = 5 * std.time.ns_per_ms; const ipc_pipe_prefix = "\\\\.\\pipe\\noctty."; -const POINT = win32_types.POINT; -const RECT = win32_types.RECT; -const PIXELFORMATDESCRIPTOR = extern struct { - nSize: WORD, - nVersion: WORD, - dwFlags: u32, - iPixelType: BYTE, - cColorBits: BYTE, - cRedBits: BYTE, - cRedShift: BYTE, - cGreenBits: BYTE, - cGreenShift: BYTE, - cBlueBits: BYTE, - cBlueShift: BYTE, - cAlphaBits: BYTE, - cAlphaShift: BYTE, - cAccumBits: BYTE, - cAccumRedBits: BYTE, - cAccumGreenBits: BYTE, - cAccumBlueBits: BYTE, - cAccumAlphaBits: BYTE, - cDepthBits: BYTE, - cStencilBits: BYTE, - cAuxBuffers: BYTE, - iLayerType: BYTE, - bReserved: BYTE, - dwLayerMask: u32, - dwVisibleMask: u32, - dwDamageMask: u32, -}; - -const MSG = extern struct { - hwnd: HWND, - message: UINT, - wParam: WPARAM, - lParam: LPARAM, - time: u32, - pt: POINT, - lPrivate: u32, -}; - -const PAINTSTRUCT = win32_types.PAINTSTRUCT; - -const DRAWITEMSTRUCT = extern struct { - CtlType: UINT, - CtlID: UINT, - itemID: UINT, - itemAction: UINT, - itemState: UINT, - hwndItem: HWND, - hDC: HDC, - rcItem: RECT, - itemData: usize, -}; - -const TRACKMOUSEEVENT = extern struct { - cbSize: DWORD, - dwFlags: DWORD, - hwndTrack: HWND, - dwHoverTime: DWORD, -}; - -const WINDOWPOS = extern struct { - hwnd: HWND, - hwndInsertAfter: ?HWND, - x: i32, - y: i32, - cx: i32, - cy: i32, - flags: UINT, -}; - -const WINDOWPLACEMENT = extern struct { - length: UINT, - flags: UINT, - showCmd: UINT, - ptMinPosition: POINT, - ptMaxPosition: POINT, - rcNormalPosition: RECT, -}; - -const NCCALCSIZE_PARAMS = extern struct { - rgrc: [3]RECT, - lppos: *WINDOWPOS, -}; - -const WNDCLASSEXW = win32_types.WNDCLASSEXW; -const CREATESTRUCTW = win32_types.CREATESTRUCTW; - -const MONITORINFO = extern struct { - cbSize: u32, - rcMonitor: RECT, - rcWork: RECT, - dwFlags: u32, -}; - -const MINMAXINFO = extern struct { - ptReserved: POINT, - ptMaxSize: POINT, - ptMaxPosition: POINT, - ptMinTrackSize: POINT, - ptMaxTrackSize: POINT, -}; - -const LOGFONTW = extern struct { - lfHeight: i32 = 0, - lfWidth: i32 = 0, - lfEscapement: i32 = 0, - lfOrientation: i32 = 0, - lfWeight: i32 = FW_NORMAL, - lfItalic: u8 = 0, - lfUnderline: u8 = 0, - lfStrikeOut: u8 = 0, - lfCharSet: u8 = DEFAULT_CHARSET, - lfOutPrecision: u8 = OUT_DEFAULT_PRECIS, - lfClipPrecision: u8 = CLIP_DEFAULT_PRECIS, - lfQuality: u8 = CLEARTYPE_QUALITY, - lfPitchAndFamily: u8 = DEFAULT_PITCH | FF_DONTCARE, - lfFaceName: [LF_FACESIZE]u16 = [_]u16{0} ** LF_FACESIZE, -}; +const POINT = sys.POINT; +const RECT = sys.RECT; +const PIXELFORMATDESCRIPTOR = sys.PIXELFORMATDESCRIPTOR; +const MSG = sys.MSG; +const PAINTSTRUCT = sys.PAINTSTRUCT; +const DRAWITEMSTRUCT = sys.DRAWITEMSTRUCT; +const TRACKMOUSEEVENT = sys.TRACKMOUSEEVENT; +const WINDOWPOS = sys.WINDOWPOS; +const WINDOWPLACEMENT = sys.WINDOWPLACEMENT; +const NCCALCSIZE_PARAMS = sys.NCCALCSIZE_PARAMS; +const WNDCLASSEXW = sys.WNDCLASSEXW; +const CREATESTRUCTW = sys.CREATESTRUCTW; +const MONITORINFO = sys.MONITORINFO; +const MINMAXINFO = sys.MINMAXINFO; +const LOGFONTW = sys.LOGFONTW; +const NONCLIENTMETRICSW = sys.NONCLIENTMETRICSW; + +fn lastError() std.posix.UnexpectedError { + return windows.unexpectedError(windows.kernel32.GetLastError()); +} + +fn setWindowData(hwnd: HWND, ptr: ?*anyopaque) void { + const raw: LONG_PTR = if (ptr) |value| + @intCast(@intFromPtr(value)) + else + 0; + _ = sys.SetWindowLongPtrW(hwnd, c.GWLP_USERDATA, raw); +} -const NONCLIENTMETRICSW = extern struct { - cbSize: UINT, - iBorderWidth: i32, - iScrollWidth: i32, - iScrollHeight: i32, - iCaptionWidth: i32, - iCaptionHeight: i32, - lfCaptionFont: LOGFONTW, - iSmCaptionWidth: i32, - iSmCaptionHeight: i32, - lfSmCaptionFont: LOGFONTW, - iMenuWidth: i32, - iMenuHeight: i32, - lfMenuFont: LOGFONTW, - lfStatusFont: LOGFONTW, - lfMessageFont: LOGFONTW, - iPaddedBorderWidth: i32, -}; +fn windowData(comptime T: type, hwnd: HWND) ?*T { + const raw = sys.GetWindowLongPtrW(hwnd, c.GWLP_USERDATA); + if (raw == 0) return null; + return @ptrFromInt(@as(usize, @intCast(raw))); +} -extern "user32" fn RegisterClassExW(lpWndClass: *const WNDCLASSEXW) callconv(.winapi) ATOM; -extern "user32" fn CreateWindowExW( - dwExStyle: u32, - lpClassName: LPCWSTR, - lpWindowName: LPCWSTR, - dwStyle: u32, - X: i32, - Y: i32, - nWidth: i32, - nHeight: i32, - hWndParent: ?HWND, - hMenu: HMENU, - hInstance: HINSTANCE, - lpParam: ?*anyopaque, -) callconv(.winapi) ?HWND; -extern "user32" fn CallWindowProcW(lpPrevWndFunc: ?*const anyopaque, hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; -extern "user32" fn DefWindowProcW(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; -extern "user32" fn DestroyWindow(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn DrawTextW(hDC: HDC, lpchText: [*:0]const u16, cchText: i32, lprc: *RECT, format: UINT) callconv(.winapi) i32; -extern "user32" fn DispatchMessageW(lpMsg: *const MSG) callconv(.winapi) LRESULT; -extern "user32" fn GetFocus() callconv(.winapi) ?HWND; -extern "user32" fn GetMessageW(lpMsg: *MSG, hWnd: ?HWND, wMsgFilterMin: UINT, wMsgFilterMax: UINT) callconv(.winapi) i32; -extern "user32" fn MessageBoxW(hwnd: ?HWND, text: LPCWSTR, caption: LPCWSTR, flags: UINT) callconv(.winapi) c_int; -extern "user32" fn GetClientRect(hWnd: HWND, lpRect: *RECT) callconv(.winapi) BOOL; -extern "user32" fn GetKeyState(nVirtKey: i32) callconv(.winapi) SHORT; -extern "user32" fn GetKeyboardState(lpKeyState: *[256]u8) callconv(.winapi) BOOL; -extern "user32" fn GetMonitorInfoW(hMonitor: ?*anyopaque, lpmi: *MONITORINFO) callconv(.winapi) BOOL; -extern "user32" fn GetWindowRect(hWnd: HWND, lpRect: *RECT) callconv(.winapi) BOOL; -extern "user32" fn GetWindowPlacement(hWnd: HWND, lpwndpl: *WINDOWPLACEMENT) callconv(.winapi) BOOL; -extern "user32" fn GetWindowTextLengthW(hWnd: HWND) callconv(.winapi) i32; -extern "user32" fn GetWindowTextW(hWnd: HWND, lpString: [*]u16, nMaxCount: i32) callconv(.winapi) i32; -extern "user32" fn IsWindow(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn IsWindowVisible(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn IsIconic(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn IsZoomed(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn MonitorFromWindow(hwnd: HWND, dwFlags: u32) callconv(.winapi) ?*anyopaque; -extern "user32" fn EnumDisplayMonitors(hdc: HDC, lprcClip: ?*const RECT, lpfnEnum: *const fn (?*anyopaque, HDC, *RECT, LPARAM) callconv(.winapi) BOOL, dwData: LPARAM) callconv(.winapi) BOOL; -extern "user32" fn ReleaseCapture() callconv(.winapi) BOOL; -extern "user32" fn ScreenToClient(hWnd: HWND, lpPoint: *POINT) callconv(.winapi) BOOL; -extern "user32" fn TrackMouseEvent(lpEventTrack: *TRACKMOUSEEVENT) callconv(.winapi) BOOL; -extern "user32" fn BeginPaint(hWnd: HWND, lpPaint: *PAINTSTRUCT) callconv(.winapi) HDC; -extern "user32" fn EndPaint(hWnd: HWND, lpPaint: *const PAINTSTRUCT) callconv(.winapi) BOOL; -extern "user32" fn GetDC(hWnd: HWND) callconv(.winapi) HDC; -extern "gdi32" fn RectVisible(hdc: HDC, lprc: *const RECT) callconv(.winapi) BOOL; -extern "user32" fn OpenClipboard(hWndNewOwner: ?HWND) callconv(.winapi) BOOL; -extern "user32" fn CloseClipboard() callconv(.winapi) BOOL; -extern "user32" fn EmptyClipboard() callconv(.winapi) BOOL; -extern "user32" fn GetClipboardData(uFormat: UINT) callconv(.winapi) ?*anyopaque; -extern "user32" fn SetClipboardData(uFormat: UINT, hMem: ?*anyopaque) callconv(.winapi) ?*anyopaque; -extern "user32" fn IsClipboardFormatAvailable(format: UINT) callconv(.winapi) BOOL; -extern "user32" fn LoadCursorW(hInstance: ?HINSTANCE, lpCursorName: INTRESOURCE) callconv(.winapi) HCURSOR; -extern "user32" fn LoadImageW(hInst: HINSTANCE, name: INTRESOURCE, @"type": UINT, cx: i32, cy: i32, fuLoad: UINT) callconv(.winapi) ?*anyopaque; -extern "user32" fn GetSystemMetrics(nIndex: i32) callconv(.winapi) i32; -extern "user32" fn MessageBeep(uType: UINT) callconv(.winapi) BOOL; -extern "user32" fn InvalidateRect(hWnd: HWND, lpRect: ?*const RECT, bErase: BOOL) callconv(.winapi) BOOL; -extern "user32" fn MoveWindow(hWnd: HWND, X: i32, Y: i32, nWidth: i32, nHeight: i32, bRepaint: BOOL) callconv(.winapi) BOOL; -extern "user32" fn PeekMessageW(lpMsg: *MSG, hWnd: ?HWND, wMsgFilterMin: UINT, wMsgFilterMax: UINT, wRemoveMsg: UINT) callconv(.winapi) BOOL; -extern "user32" fn PostMessageW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) BOOL; -extern "user32" fn PostThreadMessageW(idThread: DWORD, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) BOOL; -extern "user32" fn PostQuitMessage(nExitCode: i32) callconv(.winapi) void; -extern "user32" fn ReleaseDC(hWnd: HWND, hDC: HDC) callconv(.winapi) i32; -extern "user32" fn RegisterHotKey(hWnd: ?HWND, id: i32, fsModifiers: UINT, vk: UINT) callconv(.winapi) BOOL; -extern "user32" fn RedrawWindow(hWnd: HWND, lprcUpdate: ?*const RECT, hrgnUpdate: ?*anyopaque, flags: UINT) callconv(.winapi) BOOL; -extern "user32" fn SetTimer(hWnd: ?HWND, nIDEvent: UINT_PTR, uElapse: UINT, lpTimerFunc: ?*const anyopaque) callconv(.winapi) UINT_PTR; -extern "user32" fn SetCursor(hCursor: HCURSOR) callconv(.winapi) HCURSOR; -extern "user32" fn SetCapture(hWnd: HWND) callconv(.winapi) ?HWND; -extern "user32" fn SetForegroundWindow(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn GetCursorPos(lpPoint: *POINT) callconv(.winapi) BOOL; -extern "user32" fn GetCaretPos(lpPoint: *POINT) callconv(.winapi) BOOL; -extern "user32" fn MonitorFromPoint(pt: POINT, dwFlags: u32) callconv(.winapi) ?*anyopaque; -extern "user32" fn SetLayeredWindowAttributes(hwnd: HWND, crKey: u32, bAlpha: BYTE, dwFlags: u32) callconv(.winapi) BOOL; -extern "user32" fn SetWindowLongPtrW(hWnd: HWND, nIndex: i32, dwNewLong: LONG_PTR) callconv(.winapi) LONG_PTR; -extern "user32" fn SetWindowPos(hWnd: HWND, hWndInsertAfter: ?*anyopaque, X: i32, Y: i32, cx: i32, cy: i32, uFlags: UINT) callconv(.winapi) BOOL; -extern "user32" fn SetFocus(hWnd: HWND) callconv(.winapi) ?HWND; -extern "user32" fn SendMessageW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; -extern "user32" fn SetWindowTextW(hWnd: HWND, lpString: LPCWSTR) callconv(.winapi) BOOL; -extern "user32" fn GetWindowLongPtrW(hWnd: HWND, nIndex: i32) callconv(.winapi) LONG_PTR; -extern "user32" fn ShowWindow(hWnd: HWND, nCmdShow: i32) callconv(.winapi) BOOL; -extern "user32" fn SystemParametersInfoW(uiAction: UINT, uiParam: UINT, pvParam: ?*anyopaque, fWinIni: UINT) callconv(.winapi) BOOL; -extern "user32" fn GetSysColor(nIndex: i32) callconv(.winapi) u32; -extern "user32" fn GetDpiForWindow(hwnd: HWND) callconv(.winapi) UINT; -extern "user32" fn CreatePopupMenu() callconv(.winapi) HMENU; -extern "user32" fn AppendMenuW(hMenu: HMENU, uFlags: UINT, uIDNewItem: usize, lpNewItem: ?LPCWSTR) callconv(.winapi) BOOL; -extern "user32" fn TrackPopupMenu(hMenu: HMENU, uFlags: UINT, x: i32, y: i32, nReserved: i32, hWnd: HWND, prcRect: ?*const RECT) callconv(.winapi) BOOL; -extern "user32" fn DestroyMenu(hMenu: HMENU) callconv(.winapi) BOOL; -extern "user32" fn ClientToScreen(hWnd: HWND, lpPoint: *POINT) callconv(.winapi) BOOL; -extern "user32" fn ToUnicode( - wVirtKey: UINT, - wScanCode: UINT, - lpKeyState: *const [256]u8, - pwszBuff: [*]u16, - cchBuff: i32, - wFlags: UINT, -) callconv(.winapi) i32; -extern "user32" fn TranslateMessage(lpMsg: *const MSG) callconv(.winapi) BOOL; -extern "user32" fn IsDialogMessageW(hDlg: HWND, lpMsg: *MSG) callconv(.winapi) BOOL; -extern "user32" fn UnregisterHotKey(hWnd: ?HWND, id: i32) callconv(.winapi) BOOL; -extern "user32" fn UpdateWindow(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn KillTimer(hWnd: ?HWND, uIDEvent: UINT_PTR) callconv(.winapi) BOOL; -extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.winapi) HINSTANCE; -/// Main-thread COM apartment for in-process STA clients (settings path -/// picker, WinRT toast factory, OLE drag-drop targets). `S_FALSE` means -/// the desired STA already exists. `RPC_E_CHANGED_MODE` means the thread -/// is already in a different apartment and is not safe for STA-only work. -const COINIT_APARTMENTTHREADED: u32 = 0x2; -const RPC_E_CHANGED_MODE: i32 = @bitCast(@as(u32, 0x80010106)); -extern "ole32" fn CoInitializeEx(pvReserved: ?*anyopaque, dwCoInit: u32) callconv(.winapi) i32; -extern "ole32" fn CoUninitialize() callconv(.winapi) void; - -/// RTL_OSVERSIONINFOW for `RtlGetVersion`. `GetVersionExW` shims are -/// manifest-gated and lie about build numbers on anything newer than -/// Win8.1 unless the exe carries a Win10 application-manifest GUID. -/// `RtlGetVersion` is unshimmed and returns the real kernel build. -const RTL_OSVERSIONINFOW = extern struct { - dwOSVersionInfoSize: u32, - dwMajorVersion: u32, - dwMinorVersion: u32, - dwBuildNumber: u32, - dwPlatformId: u32, - szCSDVersion: [128]u16, -}; -extern "ntdll" fn RtlGetVersion(lpVersionInformation: *RTL_OSVERSIONINFOW) callconv(.winapi) i32; +const RTL_OSVERSIONINFOW = sys.RTL_OSVERSIONINFOW; /// Probe the Windows build number once; cache on `App.os_build`. Build /// 22000+ is Win11 (integrated titlebar / Snap Layouts); 22621+ supports @@ -1191,7 +365,7 @@ fn probeWindowsBuild() u32 { .dwPlatformId = 0, .szCSDVersion = .{0} ** 128, }; - const rc = RtlGetVersion(&info); + const rc = sys.RtlGetVersion(&info); if (rc < 0) return 0; return info.dwBuildNumber; } @@ -1200,7 +374,7 @@ fn shouldUseIntegratedTitlebar( os_build: u32, show_tab_bar: configpkg.Config.WindowShowTabBar, ) bool { - return os_build >= OS_BUILD_WIN11_21H2 and show_tab_bar != .never; + return os_build >= c.OS_BUILD_WIN11_21H2 and show_tab_bar != .never; } fn dispatchDwmNcMessage( @@ -1213,137 +387,18 @@ fn dispatchDwmNcMessage( // first so native caption-button affordances keep working where // the compositor can still own them. var result: LRESULT = 0; - return if (DwmDefWindowProc(hwnd, msg, wParam, lParam, &result) != 0) + return if (sys.DwmDefWindowProc(hwnd, msg, wParam, lParam, &result) != 0) result else null; } -/// Atomic replace of an existing file. Used by the settings save path -/// so a crash between write + rename can't corrupt `ghostty.conf`. -/// Returns non-zero on success. `dwReplaceFlags = 0` gives the default -/// (non-write-through) behaviour, which is fine for a user config file. -extern "kernel32" fn ReplaceFileW( - lpReplacedFileName: LPCWSTR, - lpReplacementFileName: LPCWSTR, - lpBackupFileName: ?LPCWSTR, - dwReplaceFlags: DWORD, - lpExclude: ?*anyopaque, - lpReserved: ?*anyopaque, -) callconv(.winapi) BOOL; -/// Fallback when the target doesn't yet exist (first-time save). Unlike -/// `ReplaceFileW`, `MoveFileExW` works on a missing target. -const MOVEFILE_REPLACE_EXISTING: u32 = 0x1; -extern "kernel32" fn MoveFileExW( - lpExistingFileName: LPCWSTR, - lpNewFileName: ?LPCWSTR, - dwFlags: u32, -) callconv(.winapi) BOOL; -extern "kernel32" fn GetCurrentThreadId() callconv(.winapi) DWORD; -extern "kernel32" fn GetCurrentProcessId() callconv(.winapi) DWORD; -extern "kernel32" fn GetTickCount64() callconv(.winapi) u64; -extern "kernel32" fn SetThreadErrorMode(dwNewMode: DWORD, lpOldMode: ?*DWORD) callconv(.winapi) BOOL; -extern "kernel32" fn CreateNamedPipeW( - lpName: LPCWSTR, - dwOpenMode: DWORD, - dwPipeMode: DWORD, - nMaxInstances: DWORD, - nOutBufferSize: DWORD, - nInBufferSize: DWORD, - nDefaultTimeOut: DWORD, - lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, -) callconv(.winapi) windows.HANDLE; -extern "kernel32" fn ConnectNamedPipe( - hNamedPipe: windows.HANDLE, - lpOverlapped: ?*anyopaque, -) callconv(.winapi) BOOL; -extern "kernel32" fn GetProcAddress(hModule: HMODULE, lpProcName: [*:0]const u8) callconv(.winapi) ?*const anyopaque; -extern "kernel32" fn LoadLibraryA(lpLibFileName: [*:0]const u8) callconv(.winapi) HMODULE; -extern "kernel32" fn SetCurrentDirectoryW(lpPathName: LPCWSTR) callconv(.winapi) BOOL; -extern "kernel32" fn WaitNamedPipeW(lpNamedPipeName: LPCWSTR, nTimeOut: DWORD) callconv(.winapi) BOOL; -extern "kernel32" fn GlobalAlloc(uFlags: UINT, dwBytes: usize) callconv(.winapi) ?*anyopaque; -extern "kernel32" fn GlobalFree(hMem: ?*anyopaque) callconv(.winapi) ?*anyopaque; -extern "kernel32" fn GlobalLock(hMem: ?*anyopaque) callconv(.winapi) ?*anyopaque; -extern "kernel32" fn GlobalUnlock(hMem: ?*anyopaque) callconv(.winapi) BOOL; -extern "gdi32" fn ChoosePixelFormat(hdc: HDC, ppfd: *const PIXELFORMATDESCRIPTOR) callconv(.winapi) i32; -extern "gdi32" fn CreateSolidBrush(color: u32) callconv(.winapi) HBRUSH; -extern "gdi32" fn DeleteObject(ho: ?*anyopaque) callconv(.winapi) BOOL; -extern "gdi32" fn FillRect(hdc: HDC, lprc: *const RECT, hbr: HBRUSH) callconv(.winapi) i32; -extern "gdi32" fn SetBkColor(hdc: HDC, color: u32) callconv(.winapi) u32; -extern "gdi32" fn SetBkMode(hdc: HDC, mode: i32) callconv(.winapi) i32; -extern "gdi32" fn SetPixelFormat(hdc: HDC, format: i32, ppfd: *const PIXELFORMATDESCRIPTOR) callconv(.winapi) BOOL; -extern "gdi32" fn SetTextColor(hdc: HDC, color: u32) callconv(.winapi) u32; -extern "gdi32" fn SwapBuffers(hdc: HDC) callconv(.winapi) BOOL; -extern "gdi32" fn TextOutW(hdc: HDC, x: i32, y: i32, lpString: LPCWSTR, c: i32) callconv(.winapi) BOOL; -extern "gdi32" fn CreateFontIndirectW(lplf: *const LOGFONTW) callconv(.winapi) ?*anyopaque; -extern "gdi32" fn SelectObject(hdc: HDC, h: ?*anyopaque) callconv(.winapi) ?*anyopaque; -extern "gdi32" fn GetStockObject(i: i32) callconv(.winapi) HGDIOBJ; -extern "gdi32" fn SetDCBrushColor(hdc: HDC, color: u32) callconv(.winapi) u32; -extern "gdi32" fn SetDCPenColor(hdc: HDC, color: u32) callconv(.winapi) u32; -extern "gdi32" fn RoundRect(hdc: HDC, left: i32, top: i32, right: i32, bottom: i32, width: i32, height: i32) callconv(.winapi) BOOL; -extern "advapi32" fn RegOpenKeyExW(hKey: usize, lpSubKey: LPCWSTR, ulOptions: DWORD, samDesired: DWORD, phkResult: *usize) callconv(.winapi) i32; -extern "advapi32" fn RegQueryValueExW(hKey: usize, lpValueName: LPCWSTR, lpReserved: ?*DWORD, lpType: ?*DWORD, lpData: ?*u8, lpcbData: ?*DWORD) callconv(.winapi) i32; -extern "advapi32" fn RegCloseKey(hKey: usize) callconv(.winapi) i32; -extern "opengl32" fn wglCreateContext(hdc: HDC) callconv(.winapi) HGLRC; -extern "opengl32" fn wglDeleteContext(hglrc: HGLRC) callconv(.winapi) BOOL; -extern "opengl32" fn wglGetCurrentContext() callconv(.winapi) HGLRC; -extern "opengl32" fn wglGetCurrentDC() callconv(.winapi) HDC; -extern "opengl32" fn wglGetProcAddress(lpszProc: [*:0]const u8) callconv(.winapi) ?*const anyopaque; -extern "opengl32" fn wglMakeCurrent(hdc: HDC, hglrc: HGLRC) callconv(.winapi) BOOL; -extern "dwmapi" fn DwmDefWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM, plResult: *LRESULT) callconv(.winapi) BOOL; -extern "imm32" fn ImmGetContext(hWnd: HWND) callconv(.winapi) ?*anyopaque; -extern "imm32" fn ImmReleaseContext(hWnd: HWND, hIMC: ?*anyopaque) callconv(.winapi) BOOL; -extern "imm32" fn ImmGetCompositionStringW(hIMC: *anyopaque, dwIndex: u32, lpBuf: ?[*]u16, dwBufLen: u32) callconv(.winapi) i32; -extern "imm32" fn ImmSetCompositionWindow(hIMC: *anyopaque, lpCompForm: *const COMPOSITIONFORM) callconv(.winapi) BOOL; -extern "imm32" fn ImmSetCandidateWindow(hIMC: *anyopaque, lpCandidate: *const CANDIDATEFORM) callconv(.winapi) BOOL; - -const COMPOSITIONFORM = extern struct { - dwStyle: u32, - ptCurrentPos: POINT, - rcArea: RECT, -}; - -const CANDIDATEFORM = extern struct { - dwIndex: u32, - dwStyle: u32, - ptCurrentPos: POINT, - rcArea: RECT, -}; - -const SystemWheelSettings = struct { - lines: u32 = 3, - chars: u32 = 3, -}; - -const MouseWheelAxis = enum { - horizontal, - vertical, -}; - -const WheelNormalizationContext = struct { - settings: SystemWheelSettings = .{}, - cell_size: apprt.action.CellSize = .{ .width = 0, .height = 0 }, - viewport: apprt.SurfaceSize = .{ .width = 0, .height = 0 }, -}; +const COMPOSITIONFORM = sys.COMPOSITIONFORM; +const CANDIDATEFORM = sys.CANDIDATEFORM; -const NormalizedWheelScroll = struct { - xoff: f64 = 0, - yoff: f64 = 0, - mods: input.ScrollMods = .{}, -}; -extern "shell32" fn ShellExecuteW( - hwnd: ?HWND, - lpOperation: ?LPCWSTR, - lpFile: LPCWSTR, - lpParameters: ?LPCWSTR, - lpDirectory: ?LPCWSTR, - nShowCmd: i32, -) callconv(.winapi) ?*anyopaque; -extern "shell32" fn DragAcceptFiles(hWnd: HWND, fAccept: BOOL) callconv(.winapi) void; -extern "shell32" fn DragQueryFileW(hDrop: *anyopaque, iFile: UINT, lpszFile: ?[*]u16, cch: UINT) callconv(.winapi) UINT; -extern "shell32" fn DragFinish(hDrop: *anyopaque) callconv(.winapi) void; - -const WM_DROPFILES: UINT = 0x0233; +const SystemWheelSettings = win32_input.SystemWheelSettings; +const WheelNormalizationContext = win32_input.WheelNormalizationContext; +const NormalizedWheelScroll = win32_input.NormalizedWheelScroll; const class_name = std.unicode.utf8ToUtf16LeStringLiteral("noctty.win32"); const host_class_name = std.unicode.utf8ToUtf16LeStringLiteral("noctty.win32.host"); @@ -1361,12 +416,6 @@ const prompt_edit_class = std.unicode.utf8ToUtf16LeStringLiteral("EDIT"); const prompt_button_class = std.unicode.utf8ToUtf16LeStringLiteral("BUTTON"); const prompt_ok_label = std.unicode.utf8ToUtf16LeStringLiteral("OK"); const prompt_cancel_label = std.unicode.utf8ToUtf16LeStringLiteral("Cancel"); -const search_prev_label = std.unicode.utf8ToUtf16LeStringLiteral("Prev match"); -const search_next_label = std.unicode.utf8ToUtf16LeStringLiteral("Next match"); -const search_regex_label = std.unicode.utf8ToUtf16LeStringLiteral("Regex"); -const search_case_label = std.unicode.utf8ToUtf16LeStringLiteral("Case sensitive"); -const search_word_label = std.unicode.utf8ToUtf16LeStringLiteral("Whole word"); -const search_close_label = std.unicode.utf8ToUtf16LeStringLiteral("Close search"); const search_edit_cue = std.unicode.utf8ToUtf16LeStringLiteral("Find in scrollback"); const scrollbar_transparent_key = rgb(0xFF, 0x00, 0xFF); const host_overlay_command_palette_label_utf8 = "Command:"; @@ -1384,245 +433,11 @@ const titlebar_glyph_restore = std.unicode.utf8ToUtf16LeStringLiteral("\u{E923}" const titlebar_glyph_close = std.unicode.utf8ToUtf16LeStringLiteral("\u{E8BB}"); const titlebar_glyph_new_tab = std.unicode.utf8ToUtf16LeStringLiteral("\u{E710}"); const titlebar_glyph_dropdown = std.unicode.utf8ToUtf16LeStringLiteral("\u{E70D}"); -const host_banner_inspector_inactive = "Inspector hidden. Terminal view is active."; -const search_results_idle = "Type to search"; -const search_results_pending = "Searching"; -const search_results_none = "No matches"; +const host_banner_inspector_inactive = labels.host_banner_inspector_inactive; const opengl32_name: [*:0]const u8 = "opengl32.dll"; const shell_open: LPCWSTR = std.unicode.utf8ToUtf16LeStringLiteral("open"); const shell_runas: LPCWSTR = std.unicode.utf8ToUtf16LeStringLiteral("runas"); -var opengl32_module: HMODULE = null; - -pub const StartupLoaderErrorDialogSuppression = struct { - previous_mode: DWORD = 0, - active: bool = false, - - pub fn restore(self: *StartupLoaderErrorDialogSuppression) void { - if (!self.active) return; - if (SetThreadErrorMode(self.previous_mode, null) == 0) { - log.warn( - "failed to restore startup loader thread error mode win32_error={d}", - .{@intFromEnum(windows.kernel32.GetLastError())}, - ); - } - self.active = false; - } -}; - -pub fn suppressStartupLoaderErrorDialogs() StartupLoaderErrorDialogSuppression { - // Let WinMain report renderer startup failures with app-specific guidance - // instead of letting LoadLibrary/WGL surface generic Windows dialogs first. - var previous_mode: DWORD = 0; - if (SetThreadErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX, &previous_mode) == 0) { - log.warn( - "failed to suppress startup loader error dialogs win32_error={d}", - .{@intFromEnum(windows.kernel32.GetLastError())}, - ); - return .{}; - } - return .{ .previous_mode = previous_mode, .active = true }; -} - -pub const OpenGLStartupStep = enum { - get_dc, - choose_pixel_format, - set_pixel_format, - create_context, - initial_make_current, - load_opengl32, - make_current, - load_functions, - version_check, - framebuffer_srgb, - - fn label(self: OpenGLStartupStep) []const u8 { - return switch (self) { - .get_dc => "acquiring the window device context", - .choose_pixel_format => "choosing a WGL pixel format", - .set_pixel_format => "setting the WGL pixel format", - .create_context => "creating the WGL context", - .initial_make_current => "making the initial WGL context current", - .load_opengl32 => "loading opengl32.dll", - .make_current => "making the WGL context current", - .load_functions => "loading OpenGL functions", - .version_check => "checking the OpenGL version", - .framebuffer_srgb => "enabling OpenGL sRGB framebuffer support", - }; - } -}; - -const OpenGLStartupFailure = struct { - step: OpenGLStartupStep, - win32_error: ?DWORD = null, - zig_error_name: ?[]const u8 = null, -}; - -var opengl_startup_diagnostics_mutex: std.Thread.Mutex = .{}; -var opengl_startup_diagnostics_active = false; -var last_opengl_startup_failure: ?OpenGLStartupFailure = null; - -fn beginOpenGLStartupDiagnostics() void { - opengl_startup_diagnostics_mutex.lock(); - defer opengl_startup_diagnostics_mutex.unlock(); - - opengl_startup_diagnostics_active = true; - last_opengl_startup_failure = null; -} - -pub fn clearOpenGLStartupFailure() void { - opengl_startup_diagnostics_mutex.lock(); - defer opengl_startup_diagnostics_mutex.unlock(); - - opengl_startup_diagnostics_active = false; - last_opengl_startup_failure = null; -} - -fn recordOpenGLStartupFailure(failure: OpenGLStartupFailure) bool { - opengl_startup_diagnostics_mutex.lock(); - defer opengl_startup_diagnostics_mutex.unlock(); - - if (!opengl_startup_diagnostics_active) return false; - if (last_opengl_startup_failure) |previous| { - if (previous.win32_error != null and failure.win32_error == null) return false; - } - - last_opengl_startup_failure = failure; - return true; -} - -fn openGLStartupDiagnosticsActive() bool { - opengl_startup_diagnostics_mutex.lock(); - defer opengl_startup_diagnostics_mutex.unlock(); - - return opengl_startup_diagnostics_active; -} - -fn currentOpenGLStartupFailure() ?OpenGLStartupFailure { - opengl_startup_diagnostics_mutex.lock(); - defer opengl_startup_diagnostics_mutex.unlock(); - - return last_opengl_startup_failure; -} - -fn recordOpenGLStartupWin32Failure(step: OpenGLStartupStep, win32_error: windows.Win32Error) void { - const code: DWORD = @intFromEnum(win32_error); - if (!recordOpenGLStartupFailure(.{ - .step = step, - .win32_error = code, - })) return; - - log.err( - "Win32 OpenGL startup failed step={s} win32_error={d}", - .{ step.label(), code }, - ); -} - -pub fn recordOpenGLStartupError(step: OpenGLStartupStep, err: anyerror) void { - if (!recordOpenGLStartupFailure(.{ - .step = step, - .zig_error_name = @errorName(err), - })) return; - - log.err("Win32 OpenGL startup failed step={s} error={s}", .{ step.label(), @errorName(err) }); -} - -pub fn reportStartupFailure(err: anyerror) void { - if (comptime builtin.os.tag != .windows) return; - - var buf: [4096]u8 = undefined; - const message = formatStartupFailureMessage(&buf, err); - - const caption = std.unicode.utf8ToUtf16LeStringLiteral("noctty failed"); - const fallback = std.unicode.utf8ToUtf16LeStringLiteral("noctty failed."); - - const message_w = std.unicode.utf8ToUtf16LeAllocZ(std.heap.page_allocator, message) catch { - _ = MessageBoxW(null, fallback, caption, MB_OK | MB_ICONERROR | MB_SETFOREGROUND); - return; - }; - defer std.heap.page_allocator.free(message_w); - - _ = MessageBoxW(null, message_w, caption, MB_OK | MB_ICONERROR | MB_SETFOREGROUND); -} - -fn formatStartupFailureMessage(buf: []u8, err: anyerror) []const u8 { - if (currentOpenGLStartupFailure()) |failure| { - return formatOpenGLStartupFailureMessage(buf, err, failure) catch - "noctty could not initialize the Windows OpenGL renderer."; - } - - return std.fmt.bufPrint( - buf, - "noctty {s} failed: {s}\n\nOpen an issue with the full log if this keeps happening.", - .{ build_config.version_string, @errorName(err) }, - ) catch "noctty failed."; -} - -fn formatOpenGLStartupFailureMessage(buf: []u8, err: anyerror, failure: OpenGLStartupFailure) ![]const u8 { - const zig_error_name = failure.zig_error_name orelse @errorName(err); - - if (failure.win32_error) |win32_error| { - return std.fmt.bufPrint(buf, - \\noctty {s} could not initialize the Windows OpenGL renderer while {s}. - \\ - \\Startup error: {s} - \\Win32 error: {d}{s} - \\ - \\noctty currently uses OpenGL 4.3 through WGL on Windows. This build does not include a DirectX or ANGLE fallback renderer. - \\ - \\{s} - \\ - \\Try updating or reinstalling the OEM AMD graphics driver, then the NVIDIA driver. You can also force noctty.exe to the discrete or integrated GPU in Windows Graphics settings. If it still fails, attach this text and the log to https://github.com/amanthanvi/noctty/issues/64. - , .{ - build_config.version_string, - failure.step.label(), - zig_error_name, - win32_error, - win32ErrorSuffix(win32_error), - openglStartupFailureHint(failure), - }); - } - - return std.fmt.bufPrint(buf, - \\noctty {s} could not initialize the Windows OpenGL renderer while {s}. - \\ - \\Startup error: {s} - \\Win32 error: not reported - \\ - \\noctty currently uses OpenGL 4.3 through WGL on Windows. This build does not include a DirectX or ANGLE fallback renderer. - \\ - \\{s} - \\ - \\Try updating or reinstalling the OEM AMD graphics driver, then the NVIDIA driver. You can also force noctty.exe to the discrete or integrated GPU in Windows Graphics settings. If it still fails, attach this text and the log to https://github.com/amanthanvi/noctty/issues/64. - , .{ - build_config.version_string, - failure.step.label(), - zig_error_name, - openglStartupFailureHint(failure), - }); -} - -fn win32ErrorSuffix(code: DWORD) []const u8 { - return switch (code) { - ERROR_MOD_NOT_FOUND => " (ERROR_MOD_NOT_FOUND)", - else => "", - }; -} - -fn openglStartupFailureHint(failure: OpenGLStartupFailure) []const u8 { - if (failure.win32_error) |code| { - if (code == ERROR_MOD_NOT_FOUND) { - return "Win32 error 126 means Windows could not load a graphics-driver DLL or one of its dependent DLLs. On AMD+NVIDIA hybrid GPU laptops, this can happen while WGL loads the AMD OpenGL ICD from DriverStore."; - } - } - - if (failure.step == .version_check) { - return "The active GPU driver did not expose the required OpenGL 4.3 feature level."; - } - - return "This is usually caused by an unavailable or incompatible OpenGL driver, a stale GPU driver installation, or missing OpenGL 4.3 support."; -} - const ForwardedArgIterator = struct { args: []const [:0]const u8, idx: usize = 0, @@ -1634,17 +449,8 @@ const ForwardedArgIterator = struct { } }; -const GlobalHotkeySpec = struct { - modifiers: UINT, - vk: UINT, -}; - -const RegisteredGlobalHotkey = struct { - id: i32, - trigger: input.Binding.Trigger, - spec: GlobalHotkeySpec, - binding: *const input.Binding.Set.Value, -}; +const GlobalHotkeySpec = win32_input.GlobalHotkeySpec; +const RegisteredGlobalHotkey = win32_input.RegisteredGlobalHotkey; fn decorationsVisibleForConfig(value: configpkg.Config.WindowDecoration) bool { return value != .none; @@ -1660,13 +466,13 @@ fn decorationVisibilityForChromeValue(active_surface_visible: ?bool, cached_visi } fn startupHostWindowStyle(decorations_visible: bool, fullscreen: bool) u32 { - return effectiveHostWindowStyle(decorations_visible, fullscreen, true) & ~@as(u32, WS_VISIBLE); + return effectiveHostWindowStyle(decorations_visible, fullscreen, true) & ~@as(u32, c.WS_VISIBLE); } fn surfaceWindowStyle() u32 { // Keep the terminal child surface hidden until GL + core init complete, // then show it explicitly from Surface.init. - return WS_CHILD | WS_CLIPSIBLINGS; + return c.WS_CHILD | c.WS_CLIPSIBLINGS; } fn effectiveHostWindowStyle( @@ -1676,22 +482,22 @@ fn effectiveHostWindowStyle( ) u32 { if (fullscreen) { return if (hosted) - WS_VISIBLE | WS_POPUP | WS_CLIPCHILDREN + c.WS_VISIBLE | c.WS_POPUP | c.WS_CLIPCHILDREN else - WS_VISIBLE | WS_POPUP; + c.WS_VISIBLE | c.WS_POPUP; } if (decorations_visible) { return if (hosted) - WS_VISIBLE | WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN + c.WS_VISIBLE | WS_OVERLAPPEDWINDOW | c.WS_CLIPCHILDREN else - WS_VISIBLE | WS_OVERLAPPEDWINDOW; + c.WS_VISIBLE | WS_OVERLAPPEDWINDOW; } return if (hosted) - WS_VISIBLE | WS_OVERLAPPED | WS_CLIPCHILDREN + c.WS_VISIBLE | c.WS_OVERLAPPED | c.WS_CLIPCHILDREN else - WS_VISIBLE | WS_OVERLAPPED; + c.WS_VISIBLE | c.WS_OVERLAPPED; } fn sharesHostWindowState(dst_host_id: ?u32, src_host_id: ?u32) bool { @@ -1765,9 +571,9 @@ fn hostPresentShowCommand( is_iconic: bool, is_zoomed: bool, ) ?i32 { - if (is_iconic) return SW_RESTORE; + if (is_iconic) return c.SW_RESTORE; if (is_visible) return null; - return if (is_zoomed) SW_MAXIMIZE else SW_SHOW; + return if (is_zoomed) c.SW_MAXIMIZE else c.SW_SHOW; } fn intResource(id: usize) INTRESOURCE { @@ -1775,28 +581,28 @@ fn intResource(id: usize) INTRESOURCE { } fn appIconResource() INTRESOURCE { - return intResource(ID_ICON_GHOSTTY); + return intResource(c.ID_ICON_GHOSTTY); } fn loadAppIcon(hinstance: HINSTANCE, small: bool) HICON { - const cx = if (small) GetSystemMetrics(SM_CXSMICON) else GetSystemMetrics(SM_CXICON); - const cy = if (small) GetSystemMetrics(SM_CYSMICON) else GetSystemMetrics(SM_CYICON); - return LoadImageW( + const cx = if (small) sys.GetSystemMetrics(c.SM_CXSMICON) else sys.GetSystemMetrics(c.SM_CXICON); + const cy = if (small) sys.GetSystemMetrics(c.SM_CYSMICON) else sys.GetSystemMetrics(c.SM_CYICON); + return sys.LoadImageW( hinstance, appIconResource(), - IMAGE_ICON, + c.IMAGE_ICON, if (cx > 0) cx else if (small) 16 else 32, if (cy > 0) cy else if (small) 16 else 32, - LR_SHARED, + c.LR_SHARED, ); } fn setWindowIcon(hwnd: HWND, hinstance: HINSTANCE) void { if (loadAppIcon(hinstance, false)) |icon| { - _ = SendMessageW(hwnd, WM_SETICON, ICON_BIG, @as(LPARAM, @bitCast(@intFromPtr(icon)))); + _ = sys.SendMessageW(hwnd, c.WM_SETICON, c.ICON_BIG, @as(LPARAM, @bitCast(@intFromPtr(icon)))); } if (loadAppIcon(hinstance, true)) |icon| { - _ = SendMessageW(hwnd, WM_SETICON, ICON_SMALL, @as(LPARAM, @bitCast(@intFromPtr(icon)))); + _ = sys.SendMessageW(hwnd, c.WM_SETICON, c.ICON_SMALL, @as(LPARAM, @bitCast(@intFromPtr(icon)))); } } @@ -1916,9 +722,9 @@ fn sanitizeIpcNamespace(alloc: Allocator, raw: ?[]const u8) ![]const u8 { var buf: std.ArrayList(u8) = .empty; errdefer buf.deinit(alloc); - for (source) |c| { - if (std.ascii.isAlphanumeric(c) or c == '.' or c == '-' or c == '_') { - try buf.append(alloc, c); + for (source) |char| { + if (std.ascii.isAlphanumeric(char) or char == '.' or char == '-' or char == '_') { + try buf.append(alloc, char); } else { try buf.append(alloc, '_'); } @@ -1969,7 +775,7 @@ fn connectToIpcPipe(pipe_name: [:0]const u16) !windows.HANDLE { switch (err) { .FILE_NOT_FOUND => return error.FileNotFound, .PIPE_BUSY => { - if (retries == 0 and WaitNamedPipeW(pipe_name.ptr, 1000) != 0) { + if (retries == 0 and sys.WaitNamedPipeW(pipe_name.ptr, 1000) != 0) { retries += 1; continue; } @@ -2133,11 +939,11 @@ fn ipcServerMain(app: *App) void { const pipe_name = app.ipc_pipe_name orelse return; server: while (!app.ipc_stop_requested.load(.acquire)) { - const pipe = CreateNamedPipeW( + const pipe = sys.CreateNamedPipeW( pipe_name.ptr, - PIPE_ACCESS_DUPLEX, - windows.PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | win32_ipc.pipe_nowait, - PIPE_UNLIMITED_INSTANCES, + c.PIPE_ACCESS_DUPLEX, + windows.PIPE_TYPE_BYTE | c.PIPE_READMODE_BYTE | win32_ipc.pipe_nowait, + c.PIPE_UNLIMITED_INSTANCES, 16 * 1024, 16 * 1024, 0, @@ -2151,7 +957,7 @@ fn ipcServerMain(app: *App) void { } connect: while (!app.ipc_stop_requested.load(.acquire)) { - const connected = ConnectNamedPipe(pipe, null); + const connected = sys.ConnectNamedPipe(pipe, null); if (connected != 0) break :connect; const err = windows.kernel32.GetLastError(); switch (err) { @@ -2305,29 +1111,29 @@ fn requestAutomationAction( } fn waitForAutomationCompletion(app: *const App, completed: *const std.atomic.Value(bool)) !void { - const deadline_ms = GetTickCount64() +| win32_ipc.automation_response_timeout_ms; + const deadline_ms = sys.GetTickCount64() +| win32_ipc.automation_response_timeout_ms; while (!completed.load(.acquire)) { if (app.ipc_stop_requested.load(.acquire)) return error.IPCFailed; - if (GetTickCount64() >= deadline_ms) return error.IpcTimeout; + if (sys.GetTickCount64() >= deadline_ms) return error.IpcTimeout; std.Thread.sleep(std.time.ns_per_ms); } } pub fn getProcAddress(name: [*:0]const u8) callconv(.c) ?*const anyopaque { - if (wglGetProcAddress(name)) |ptr| { + if (sys.wglGetProcAddress(name)) |ptr| { const raw = @intFromPtr(ptr); if (raw > 3 and raw != std.math.maxInt(usize)) return ptr; } - if (opengl32_module == null) { - opengl32_module = LoadLibraryA(opengl32_name); - if (opengl32_module == null) { + if (gl_startup.opengl32_module == null) { + gl_startup.opengl32_module = sys.LoadLibraryA(opengl32_name); + if (gl_startup.opengl32_module == null) { recordOpenGLStartupWin32Failure(.load_opengl32, windows.kernel32.GetLastError()); } } - const module = opengl32_module orelse return null; - return GetProcAddress(module, name); + const module = gl_startup.opengl32_module orelse return null; + return sys.GetProcAddress(module, name); } const UpdateNotice = struct { @@ -2462,32 +1268,6 @@ fn settingsEditedValueMasked( return pending.changed(reloaded, comptime key); } -test "win32 render trace classifies gaps by start time" { - try std.testing.expect(!RenderTrace.gapIsSustained(1250, 578)); - try std.testing.expect(!RenderTrace.gapIsSustained(1299, 300)); - try std.testing.expect(RenderTrace.gapIsSustained(1300, 300)); - try std.testing.expect(!RenderTrace.gapIsSustained(500, 600)); -} - -test "win32 render trace classifies visible paint gaps" { - try std.testing.expect(RenderTrace.startup_paint_gap_ceiling_ms >= RenderTrace.startup_window_ms); - try std.testing.expect(!RenderTrace.gapExceedsPaintLimit(300)); - try std.testing.expect(RenderTrace.gapExceedsPaintLimit(301)); -} - -test "win32 render trace init rejects and frees a second process owner" { - var claimed = std.atomic.Value(bool).init(false); - const first = try std.testing.allocator.dupe(u8, "first.json"); - const first_trace = RenderTrace.initWithClaimedPath(std.testing.allocator, &claimed, first); - defer if (first_trace.path) |path| std.testing.allocator.free(path); - try std.testing.expect(first_trace.path != null); - - const second = try std.testing.allocator.dupe(u8, "second.json"); - const second_trace = RenderTrace.initWithClaimedPath(std.testing.allocator, &claimed, second); - defer if (second_trace.path) |path| std.testing.allocator.free(path); - try std.testing.expect(second_trace.path == null); -} - test "win32 bounded UTF-8 prefix never splits a codepoint" { const value = "123\u{1f680}tail"; try std.testing.expectEqual(@as(usize, 3), utf8PrefixLen(value, 4)); @@ -2572,42 +1352,83 @@ fn writePersistentFileAlloc(alloc: Allocator, path: []const u8, data: []const u8 const temporary_path = try std.fmt.allocPrint( alloc, "{s}.tmp-{x}-{x}", - .{ path, GetCurrentProcessId(), unixMillis() }, + .{ path, sys.GetCurrentProcessId(), unixMillis() }, ); defer alloc.free(temporary_path); try win32_session_persistence.writeFileAtomic(path, temporary_path, data); } -pub const App = struct { - pub const must_draw_from_app_thread = true; +fn sessionStatePolicyAllows(safe_mode: bool, policy: configpkg.Config.WindowSaveState) bool { + return !safe_mode and policy != .never; +} - core_app: *CoreApp, - config: configpkg.Config, - config_revision: u64 = 1, - resolved_theme: ThemeColors = darkTheme(), - hinstance: HINSTANCE, - class_atom: ATOM = 0, - host_class_atom: ATOM = 0, - palette_list_class_atom: ATOM = 0, - scrollbar_class_atom: ATOM = 0, - hosts: std.ArrayListUnmanaged(*Host) = .empty, - windows: std.ArrayListUnmanaged(*Surface) = .empty, - /// Internal test seam so action-path tests can drive real - /// `performAction(.new_tab, ...)` logic without requiring HWND/GL - /// surface bring-up. - test_create_window_surface: ?*const fn ( - app: *App, - config: *const configpkg.Config, - title: LPCWSTR, - opts: SurfaceInitOptions, - ) anyerror!*Surface = null, - /// Test-only interleaving seam for the focus revision race between a - /// structural split-close prepare and commit. - test_before_split_undo_shell_commit: ?*const fn ( - app: *App, - source_pane: win32_shell.model.PaneId, - ) anyerror!void = null, - next_host_id: u32 = 1, +const SessionRestoreTransaction = struct { + app: ?*App = null, + host: ?*Host = null, + committed: bool = false, + + fn noteSurface(self: *SessionRestoreTransaction, surface: *Surface) void { + if (self.host == null) { + self.app = surface.app; + self.host = surface.host; + } + } + + fn commit(self: *SessionRestoreTransaction) void { + self.committed = true; + } + + fn rollback(self: *SessionRestoreTransaction) void { + if (self.committed) return; + const app = self.app orelse return; + const host = self.host orelse return; + app.rollbackSessionRestoreHost(host); + } +}; + +fn sessionRestorePolicyAllows( + safe_mode: bool, + policy: configpkg.Config.WindowSaveState, + initial_window: bool, + has_initial_command: bool, + startup_profile_picker: bool, +) bool { + return sessionStatePolicyAllows(safe_mode, policy) and + initial_window and + !has_initial_command and + !startup_profile_picker; +} + +pub const App = struct { + pub const must_draw_from_app_thread = true; + + core_app: *CoreApp, + config: configpkg.Config, + config_revision: u64 = 1, + resolved_theme: ThemeColors = darkTheme(), + hinstance: HINSTANCE, + class_atom: ATOM = 0, + host_class_atom: ATOM = 0, + palette_list_class_atom: ATOM = 0, + scrollbar_class_atom: ATOM = 0, + hosts: std.ArrayListUnmanaged(*Host) = .empty, + windows: std.ArrayListUnmanaged(*Surface) = .empty, + /// Internal test seam so action-path tests can drive real + /// `performAction(.new_tab, ...)` logic without requiring HWND/GL + /// surface bring-up. + test_create_window_surface: ?*const fn ( + app: *App, + config: *const configpkg.Config, + title: LPCWSTR, + opts: SurfaceInitOptions, + ) anyerror!*Surface = null, + /// Test-only interleaving seam for the focus revision race between a + /// structural split-close prepare and commit. + test_before_split_undo_shell_commit: ?*const fn ( + app: *App, + source_pane: win32_shell.model.PaneId, + ) anyerror!void = null, + next_host_id: u32 = 1, launcher_profile_key: ?[:0]const u8 = null, launcher_profile_hint: ?[:0]const u8 = null, launcher_profile_order_hint: ?[:0]const u8 = null, @@ -2742,7 +1563,7 @@ pub const App = struct { } else try configpkg.Config.load(core_app.alloc), .safe_mode = safe_mode, .recovery_startup = recovery_startup, - .hinstance = GetModuleHandleW(null), + .hinstance = sys.GetModuleHandleW(null), .launcher_profile_hint = detectDefaultProfileHint(core_app.alloc), .launcher_profile_order_hint = windows_shell.profileOrderHint(core_app.alloc), .launcher_profile_target = detectDefaultProfileTarget(core_app.alloc), @@ -2870,10 +1691,10 @@ pub const App = struct { /// `RPC_E_CHANGED_MODE` means "already initialised in a different /// mode" and leaves STA-only consumers disabled. fn initComApartment(self: *App) void { - const hr = CoInitializeEx(null, COINIT_APARTMENTTHREADED); + const hr = sys.CoInitializeEx(null, c.COINIT_APARTMENTTHREADED); switch (hr) { 0, 1 => self.com_initialized = true, // S_OK, S_FALSE - RPC_E_CHANGED_MODE => { + c.RPC_E_CHANGED_MODE => { std.log.warn("COM apartment: already MTA on this thread; STA consumers may fail", .{}); }, else => { @@ -2901,9 +1722,9 @@ pub const App = struct { } self.running = true; - const ui_thread_id = GetCurrentThreadId(); + const ui_thread_id = sys.GetCurrentThreadId(); @atomicStore(DWORD, &self.ui_thread_id, ui_thread_id, .release); - self.ensureMessageQueue(); + ensureMessageQueue(); defer { self.stopUndoPruneTimer(); self.stopQuitTimer(); @@ -2941,11 +1762,11 @@ pub const App = struct { var msg: MSG = undefined; while (true) { - const result = GetMessageW(&msg, null, 0, 0); - if (result == -1) return windows.unexpectedError(windows.kernel32.GetLastError()); + const result = sys.GetMessageW(&msg, null, 0, 0); + if (result == -1) return lastError(); if (result == 0) break; - if (msg.message == WM_WINHOSTTY_WAKE) { + if (msg.message == c.WM_WINHOSTTY_WAKE) { if (self.global_hotkeys_dirty) { self.global_hotkeys_dirty = false; self.syncGlobalHotkeys() catch |err| { @@ -2957,7 +1778,7 @@ pub const App = struct { continue; } - if (msg.message == WM_WINHOSTTY_UPDATE) { + if (msg.message == c.WM_WINHOSTTY_UPDATE) { const completion: *UpdateCheckCompletion = @ptrFromInt(@as(usize, @bitCast(msg.lParam))); defer { completion.deinit(); @@ -2969,7 +1790,7 @@ pub const App = struct { continue; } - if (msg.message == WM_WINHOSTTY_TOAST_ACTIVATION) { + if (msg.message == c.WM_WINHOSTTY_TOAST_ACTIVATION) { const activation: *win32_toast_activation.ActivationTarget = @ptrFromInt(@as(usize, @bitCast(msg.lParam))); defer std.heap.page_allocator.destroy(activation); defer self.toast_activation_post_pending.store(false, .release); @@ -2981,7 +1802,7 @@ pub const App = struct { continue; } - if (msg.message == WM_WINHOSTTY_UIA_DISCONNECT) { + if (msg.message == c.WM_WINHOSTTY_UIA_DISCONNECT) { if (msg.lParam == 0) { log.warn("win32 UIA deferred disconnect message had no context", .{}); continue; @@ -2991,13 +1812,13 @@ pub const App = struct { continue; } - if (msg.message == WM_TIMER) { + if (msg.message == c.WM_TIMER) { if (self.quit_timer_id) |timer_id| { if (msg.wParam == timer_id) { self.stopQuitTimer(); if (self.running and !self.hasLiveUiWindows() and self.config.@"quit-after-last-window-closed") { self.running = false; - PostQuitMessage(0); + sys.PostQuitMessage(0); } continue; } @@ -3011,7 +1832,7 @@ pub const App = struct { } } - if (msg.message == WM_HOTKEY) { + if (msg.message == c.WM_HOTKEY) { self.handleGlobalHotkey(@intCast(msg.wParam)); continue; } @@ -3021,14 +1842,14 @@ pub const App = struct { // Shift+Tab, arrow-key radio navigation, and default buttons use // standard Win32 accessibility semantics. if (self.settings_window.hwnd) |settings_hwnd| { - if (IsDialogMessageW(settings_hwnd, &msg) != 0) { + if (sys.IsDialogMessageW(settings_hwnd, &msg) != 0) { try self.core_app.tick(self); continue; } } - _ = TranslateMessage(&msg); - _ = DispatchMessageW(&msg); + _ = sys.TranslateMessage(&msg); + _ = sys.DispatchMessageW(&msg); if (self.global_hotkeys_dirty) { self.global_hotkeys_dirty = false; @@ -3103,7 +1924,7 @@ pub const App = struct { deinitImeWindowFormsTracePath(self.core_app.alloc); self.config.deinit(); if (self.com_initialized) { - CoUninitialize(); + sys.CoUninitialize(); self.com_initialized = false; } } @@ -3331,7 +2152,7 @@ pub const App = struct { if (host == null) { host = tab_surface.host; if (host) |created_host| { - self.applyRestoredWindowPlacement(created_host, window) catch |err| { + applyRestoredWindowPlacement(created_host, window) catch |err| { log.warn("win32 session restore: window placement failed err={}", .{err}); }; } @@ -3465,25 +2286,23 @@ pub const App = struct { } fn applyRestoredWindowPlacement( - self: *App, host: *Host, window: win32_session_state.Window, ) !void { - _ = self; const hwnd = host.hwnd orelse return; if (try sessionStateWindowRect(window)) |rect| { - if (SetWindowPos( + if (sys.SetWindowPos( hwnd, null, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, - SWP_NOZORDER | SWP_NOACTIVATE, - ) == 0) return windows.unexpectedError(windows.kernel32.GetLastError()); + c.SWP_NOZORDER | c.SWP_NOACTIVATE, + ) == 0) return lastError(); } if ((window.state orelse .normal) == .maximized) { - _ = ShowWindow(hwnd, SW_MAXIMIZE); + _ = sys.ShowWindow(hwnd, c.SW_MAXIMIZE); } } @@ -3534,7 +2353,7 @@ pub const App = struct { var built: usize = 0; for (self.hosts.items) |host| { if (hostSessionTabCount(host) == 0) continue; - windows_state[built] = try self.buildSessionWindow(alloc, host); + windows_state[built] = try buildSessionWindow(alloc, host); built += 1; } @@ -3542,7 +2361,6 @@ pub const App = struct { } fn buildSessionWindow( - self: *const App, alloc: Allocator, host: *Host, ) !win32_session_state.Window { @@ -3568,9 +2386,8 @@ pub const App = struct { window.height = rect.bottom - rect.top; } if (host.hwnd) |hwnd| { - window.state = if (IsZoomed(hwnd) != 0) .maximized else .normal; + window.state = if (sys.IsZoomed(hwnd) != 0) .maximized else .normal; } - _ = self; return window; } @@ -3762,7 +2579,7 @@ pub const App = struct { fn sessionWindowRect(host: *Host) ?RECT { const hwnd = host.hwnd orelse return null; var rect: RECT = undefined; - if (IsZoomed(hwnd) != 0 or IsIconic(hwnd) != 0) { + if (sys.IsZoomed(hwnd) != 0 or sys.IsIconic(hwnd) != 0) { var placement: WINDOWPLACEMENT = .{ .length = @sizeOf(WINDOWPLACEMENT), .flags = 0, @@ -3771,12 +2588,12 @@ pub const App = struct { .ptMaxPosition = .{ .x = 0, .y = 0 }, .rcNormalPosition = .{ .left = 0, .top = 0, .right = 0, .bottom = 0 }, }; - if (GetWindowPlacement(hwnd, &placement) != 0) { + if (sys.GetWindowPlacement(hwnd, &placement) != 0) { rect = placement.rcNormalPosition; - } else if (GetWindowRect(hwnd, &rect) == 0) { + } else if (sys.GetWindowRect(hwnd, &rect) == 0) { return null; } - } else if (GetWindowRect(hwnd, &rect) == 0) { + } else if (sys.GetWindowRect(hwnd, &rect) == 0) { return null; } @@ -3888,7 +2705,7 @@ pub const App = struct { fn unregisterGlobalHotkeys(self: *App) void { for (self.global_hotkeys.items) |hotkey| { - _ = UnregisterHotKey(null, hotkey.id); + _ = sys.UnregisterHotKey(null, hotkey.id); } self.global_hotkeys.clearRetainingCapacity(); } @@ -3921,7 +2738,7 @@ pub const App = struct { continue; } - if (RegisterHotKey(null, next_id, spec.modifiers, spec.vk) == 0) { + if (sys.RegisterHotKey(null, next_id, spec.modifiers, spec.vk) == 0) { const err = windows.kernel32.GetLastError(); log.warn("failed to register win32 global hotkey trigger={f} id={} mods=0x{x} vk=0x{x} err={} reason={s}", .{ entry.key_ptr.*, @@ -3972,12 +2789,12 @@ pub const App = struct { pub fn wakeup(self: *App) void { if (self.windows.items.len > 0) { const hwnd = self.windows.items[0].hwnd orelse return; - _ = PostMessageW(hwnd, WM_WINHOSTTY_WAKE, 0, 0); + _ = sys.PostMessageW(hwnd, c.WM_WINHOSTTY_WAKE, 0, 0); return; } if (self.ui_thread_id != 0) { - _ = PostThreadMessageW(self.ui_thread_id, WM_WINHOSTTY_WAKE, 0, 0); + _ = sys.PostThreadMessageW(self.ui_thread_id, c.WM_WINHOSTTY_WAKE, 0, 0); } } @@ -4164,13 +2981,13 @@ pub const App = struct { const stage_dir_w = try std.unicode.utf8ToUtf16LeAllocZ(self.core_app.alloc, stage_dir); defer self.core_app.alloc.free(stage_dir_w); - const result = ShellExecuteW( + const result = sys.ShellExecuteW( null, shell_runas, installer_w.ptr, params_w.ptr, stage_dir_w.ptr, - SW_SHOW, + c.SW_SHOW, ); if (@intFromPtr(result) <= 32) { self.showUpdateInfo(updateApplyFailureMessage(error.UpdateApplyLaunchFailed)) catch |banner_err| { @@ -4186,7 +3003,7 @@ pub const App = struct { self.stopQuitTimer(); self.running = false; self.destroyAllWindows(); - if (self.windows.items.len == 0) PostQuitMessage(0); + if (self.windows.items.len == 0) sys.PostQuitMessage(0); } fn canApplyStagedUpdate(self: *App) bool { @@ -4202,7 +3019,7 @@ pub const App = struct { return; } } - try self.showInfoMessage(.app, "noctty", message); + try showInfoMessage(.app, "noctty", message); } /// True when any user-facing top-level UI window is still alive — @@ -4231,13 +3048,13 @@ pub const App = struct { const delay = self.config.@"quit-after-last-window-closed-delay" orelse { self.stopQuitTimer(); self.running = false; - PostQuitMessage(0); + sys.PostQuitMessage(0); return; }; self.stopQuitTimer(); const delay_ms = quitTimerDelayMs(delay); - const timer_id = SetTimer(null, 0, delay_ms, null); + const timer_id = sys.SetTimer(null, 0, delay_ms, null); if (timer_id == 0) { log.err("failed to start win32 quit timer delay_ms={} err={}", .{ delay_ms, @@ -4250,14 +3067,14 @@ pub const App = struct { fn stopQuitTimer(self: *App) void { if (self.quit_timer_id) |timer_id| { - _ = KillTimer(null, timer_id); + _ = sys.KillTimer(null, timer_id); self.quit_timer_id = null; } } fn stopUndoPruneTimer(self: *App) void { if (self.undo_prune_timer_id) |timer_id| { - _ = KillTimer(null, timer_id); + _ = sys.KillTimer(null, timer_id); self.undo_prune_timer_id = null; } } @@ -4301,10 +3118,10 @@ pub const App = struct { self.stopUndoPruneTimer(); const oldest = self.oldestUndoTimestamp() orelse return; - const now = GetTickCount64(); + const now = sys.GetTickCount64(); const expires_at = oldest +| self.undoTimeoutMs() +| 1; const delay_ms = @max(minimum_delay_ms, if (expires_at <= now) 1 else expires_at - now); - const timer_id = SetTimer( + const timer_id = sys.SetTimer( null, 0, @intCast(@min(delay_ms, std.math.maxInt(UINT))), @@ -4331,7 +3148,7 @@ pub const App = struct { if (!host.pruneStructuralHistoryBefore(min_timestamp_ms)) complete = false; if (host.destroy_after_structural_dispose and host.tabs.items.len == 0 and !host.hasStructuralHistory()) { if (host.hwnd) |hwnd| { - _ = DestroyWindow(hwnd); + _ = sys.DestroyWindow(hwnd); self.removeHost(host); continue; } @@ -4353,15 +3170,9 @@ pub const App = struct { if (self.running and !self.hasLiveUiWindows()) self.startQuitTimer(); } - fn ensureMessageQueue(self: *App) void { - _ = self; + fn ensureMessageQueue() void { var msg: MSG = undefined; - _ = PeekMessageW(&msg, null, 0, 0, PM_NOREMOVE); - } - - pub fn keyboardLayout(self: *const App) input.KeyboardLayout { - _ = self; - return .unknown; + _ = sys.PeekMessageW(&msg, null, 0, 0, c.PM_NOREMOVE); } pub fn performAction( @@ -4375,7 +3186,7 @@ pub const App = struct { self.stopQuitTimer(); self.running = false; self.destroyAllWindows(); - if (self.windows.items.len == 0) PostQuitMessage(0); + if (self.windows.items.len == 0) sys.PostQuitMessage(0); return true; }, @@ -4442,7 +3253,7 @@ pub const App = struct { defer config.deinit(); const source = self.findSurfaceForTarget(target) orelse return false; const tab_info = self.findTabForSurface(source) orelse return false; - const inherited_profile_key = try self.applySplitProfileConfigFromSource(&config, source); + const inherited_profile_key = try applySplitProfileConfigFromSource(&config, source); _ = try applySplitWorkingDirectoryFromSource(&config, source, self.startup_cwd); const split_direction = splitDirectionFromAction(value); const surface = try self.createWindowSurface(&config, default_title, .{ @@ -4457,7 +3268,7 @@ pub const App = struct { tab_info.tab.clearRedoHistory(); if (tab_info.host.pushStructuralUndo(.{ .kind = .split_create, - .timestamp_ms = GetTickCount64(), + .timestamp_ms = sys.GetTickCount64(), .sequence_id = self.nextUndoSequence(), .payload = .{ .split_create = .{ .source_surface = source, @@ -4480,7 +3291,7 @@ pub const App = struct { self.stopQuitTimer(); self.running = false; self.destroyAllWindows(); - if (self.windows.items.len == 0) PostQuitMessage(0); + if (self.windows.items.len == 0) sys.PostQuitMessage(0); return true; }, @@ -5279,22 +4090,22 @@ pub const App = struct { var wc: WNDCLASSEXW = .{ .cbSize = @sizeOf(WNDCLASSEXW), - .style = CS_OWNDC, + .style = c.CS_OWNDC, .lpfnWndProc = &windowProc, .cbClsExtra = 0, .cbWndExtra = 0, .hInstance = self.hinstance, .hIcon = loadAppIcon(self.hinstance, false), - .hCursor = LoadCursorW(null, IDC_ARROW), + .hCursor = sys.LoadCursorW(null, c.IDC_ARROW), .hbrBackground = null, .lpszMenuName = null, .lpszClassName = class_name, .hIconSm = loadAppIcon(self.hinstance, true), }; - self.class_atom = RegisterClassExW(&wc); + self.class_atom = sys.RegisterClassExW(&wc); if (self.class_atom == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } @@ -5309,16 +4120,16 @@ pub const App = struct { .cbWndExtra = 0, .hInstance = self.hinstance, .hIcon = loadAppIcon(self.hinstance, false), - .hCursor = LoadCursorW(null, IDC_ARROW), + .hCursor = sys.LoadCursorW(null, c.IDC_ARROW), .hbrBackground = null, .lpszMenuName = null, .lpszClassName = host_class_name, .hIconSm = loadAppIcon(self.hinstance, true), }; - self.host_class_atom = RegisterClassExW(&wc); + self.host_class_atom = sys.RegisterClassExW(&wc); if (self.host_class_atom == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } @@ -5333,16 +4144,16 @@ pub const App = struct { .cbWndExtra = 0, .hInstance = self.hinstance, .hIcon = null, - .hCursor = LoadCursorW(null, IDC_ARROW), + .hCursor = sys.LoadCursorW(null, c.IDC_ARROW), .hbrBackground = null, .lpszMenuName = null, .lpszClassName = palette_list_class_name, .hIconSm = null, }; - self.palette_list_class_atom = RegisterClassExW(&wc); + self.palette_list_class_atom = sys.RegisterClassExW(&wc); if (self.palette_list_class_atom == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } @@ -5357,16 +4168,16 @@ pub const App = struct { .cbWndExtra = 0, .hInstance = self.hinstance, .hIcon = null, - .hCursor = LoadCursorW(null, IDC_ARROW), + .hCursor = sys.LoadCursorW(null, c.IDC_ARROW), .hbrBackground = null, .lpszMenuName = null, .lpszClassName = scrollbar_class_name, .hIconSm = null, }; - self.scrollbar_class_atom = RegisterClassExW(&wc); + self.scrollbar_class_atom = sys.RegisterClassExW(&wc); if (self.scrollbar_class_atom == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } @@ -5388,11 +4199,9 @@ pub const App = struct { } fn applySplitProfileConfigFromSource( - self: *App, config: *configpkg.Config, source: *Surface, ) !?[]const u8 { - _ = self; const key = source.launch_profile_key orelse return null; const host = source.host orelse return null; const profile = (try host.profileForKey(key)) orelse return null; @@ -5449,8 +4258,8 @@ pub const App = struct { fn refreshSystemWheelSettings(self: *App) void { self.wheel_settings = .{ - .lines = readSystemWheelSetting(SPI_GETWHEELSCROLLLINES, self.wheel_settings.lines), - .chars = readSystemWheelSetting(SPI_GETWHEELSCROLLCHARS, self.wheel_settings.chars), + .lines = readSystemWheelSetting(c.SPI_GETWHEELSCROLLLINES, self.wheel_settings.lines), + .chars = readSystemWheelSetting(c.SPI_GETWHEELSCROLLCHARS, self.wheel_settings.chars), }; } @@ -5485,27 +4294,27 @@ pub const App = struct { if (host.hwnd) |hwnd| { applyDwmThemeWithBuild(hwnd, &self.resolved_theme, &self.config, self.os_build); if (frame_mode_changed) { - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, null, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED, + c.SWP_NOMOVE | c.SWP_NOSIZE | c.SWP_NOZORDER | c.SWP_NOACTIVATE | c.SWP_FRAMECHANGED, ); - host.layout() catch {}; + runUiActionOrLog("theme update layout failed", host.layout()); } // Theme resource replacement always invalidates the full host // chrome plus native child controls. A System->Dark swap does // not necessarily change frame mode, but every retained pixel // still belongs to the previous palette. host.invalidateChrome(); - _ = RedrawWindow( + _ = sys.RedrawWindow( hwnd, null, null, - RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN, + c.RDW_INVALIDATE | c.RDW_ERASE | c.RDW_FRAME | c.RDW_ALLCHILDREN, ); } } @@ -5544,7 +4353,10 @@ pub const App = struct { if (self.launcher_quick_slot_keys[index]) |dupe| self.core_app.alloc.free(dupe); self.launcher_quick_slot_keys[index] = try self.core_app.alloc.dupeZ(u8, current); } else { - appendOwnedString(self.core_app.alloc, &self.launcher_quick_slot_keys[index], null) catch {}; + runUiActionOrLog( + "quick slot deduplication failed", + appendOwnedString(self.core_app.alloc, &self.launcher_quick_slot_keys[index], null), + ); } } } @@ -5552,7 +4364,7 @@ pub const App = struct { try appendOwnedString(self.core_app.alloc, &self.launcher_quick_slot_keys[slot_ordinal], key); for (self.hosts.items) |host| { - host.reapplyLauncherProfilePreferences() catch {}; + runUiActionOrLog("quick slot profile refresh failed", host.reapplyLauncherProfilePreferences()); host.refreshProfileChrome(); } } @@ -5560,7 +4372,7 @@ pub const App = struct { fn clearLauncherQuickSlotPreferences(self: *App) void { if (launcherQuickSlotPinsAlreadyClear(self.launcher_quick_slot_keys)) return; for (&self.launcher_quick_slot_keys) |*value| { - appendOwnedString(self.core_app.alloc, value, null) catch {}; + runUiActionOrLog("quick slot clear failed", appendOwnedString(self.core_app.alloc, value, null)); } for (self.hosts.items) |host| { @@ -5681,28 +4493,28 @@ pub const App = struct { host.cached_decorations_visible = startup_decorations_visible; const startup_fullscreen = if (clone_state_from) |source| source.fullscreen else false; - const hwnd = CreateWindowExW( + const hwnd = sys.CreateWindowExW( 0, host_class_name, title, startupHostWindowStyle(startup_decorations_visible, startup_fullscreen), - if (position) |v| v.x else CW_USEDEFAULT, - if (position) |v| v.y else CW_USEDEFAULT, + if (position) |v| v.x else c.CW_USEDEFAULT, + if (position) |v| v.y else c.CW_USEDEFAULT, 1280, 800, null, null, self.hinstance, host, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); host.hwnd = hwnd; setWindowIcon(hwnd, self.hinstance); applyDwmThemeWithBuild(hwnd, &self.resolved_theme, &self.config, self.os_build); - host.current_dpi = GetDpiForWindow(hwnd); + host.current_dpi = sys.GetDpiForWindow(hwnd); if (host.current_dpi == 0) host.current_dpi = 96; host.chrome_font = host.createChromeFont(); host.recreateTitlebarIconFonts(); - errdefer _ = DestroyWindow(hwnd); + errdefer _ = sys.DestroyWindow(hwnd); self.attachShellCompositorWindow(hwnd); @@ -5716,8 +4528,8 @@ pub const App = struct { // does NOT bring it to the foreground, so the user's current // typing context stays intact. `SW_SHOW` would activate the // new HWND and steal focus. - _ = ShowWindow(hwnd, if (passive_show) SW_SHOWNOACTIVATE else SW_SHOW); - _ = UpdateWindow(hwnd); + _ = sys.ShowWindow(hwnd, if (passive_show) c.SW_SHOWNOACTIVATE else c.SW_SHOW); + _ = sys.UpdateWindow(hwnd); self.maybeScheduleAutomaticUpdateCheck(); return host; } @@ -5869,15 +4681,14 @@ pub const App = struct { return null; } - fn activeTab(self: *App, host: *Host) ?*Tab { - _ = self; + fn activeTab(host: *Host) ?*Tab { if (host.tabs.items.len == 0 or host.active_tab >= host.tabs.items.len) return null; return &host.tabs.items[host.active_tab]; } fn activeSurfaceForHost(self: *App, host_id: u32) ?*Surface { const host = self.findHostById(host_id) orelse return null; - const tab = self.activeTab(host) orelse return null; + const tab = activeTab(host) orelse return null; return tab.focusedSurface(); } @@ -5899,19 +4710,19 @@ pub const App = struct { const dst_hwnd = destination.hwnd orelse return; const src_hwnd = source.hwnd orelse return; var rect: RECT = undefined; - if (GetWindowRect(src_hwnd, &rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetWindowRect(src_hwnd, &rect) == 0) { + return lastError(); } - if (SetWindowPos( + if (sys.SetWindowPos( dst_hwnd, null, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, - SWP_NOACTIVATE, + c.SWP_NOACTIVATE, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } @@ -6011,7 +4822,7 @@ pub const App = struct { } fn undoCutoffTimestampMs(self: *const App) u64 { - return GetTickCount64() -| self.undoTimeoutMs(); + return sys.GetTickCount64() -| self.undoTimeoutMs(); } fn findSurfaceByCore(self: *App, core_surface: *CoreSurface) ?*Surface { @@ -6102,8 +4913,8 @@ pub const App = struct { host.prepareActiveTabVisibility(tab_info.index); var active_it = tab_info.tab.tree.iterator(); while (active_it.next()) |entry| entry.view.setVisible(true); - host.layout() catch {}; - host.refreshChrome() catch {}; + runUiActionOrLog("tab activation layout failed", host.layout()); + runUiActionOrLog("tab activation chrome refresh failed", host.refreshChrome()); if (focus) { surface.presentWindow(); } else { @@ -6117,7 +4928,7 @@ pub const App = struct { if (self.findTabForSurface(surface)) |found| { const handle = found.tab.findHandle(surface) orelse found.tab.focused; const host_window_visible = if (found.host.hwnd) |hwnd| - IsWindowVisible(hwnd) != 0 + sys.IsWindowVisible(hwnd) != 0 else false; const needs_host_sync = surfaceFocusedActivationNeedsHostSync( @@ -6198,7 +5009,7 @@ pub const App = struct { return surface; } } - if (self.activeTab(host)) |tab| { + if (activeTab(host)) |tab| { if (tab.focusedSurface()) |surface| { return surface; } @@ -6232,7 +5043,7 @@ pub const App = struct { if (self.findTabForSurface(surface)) |found| { const handle = found.tab.findHandle(surface) orelse found.tab.focused; const host_window_visible = if (found.host.hwnd) |hwnd| - IsWindowVisible(hwnd) != 0 + sys.IsWindowVisible(hwnd) != 0 else false; const needs_host_sync = surfaceActivationNeedsHostSync( @@ -6373,16 +5184,16 @@ pub const App = struct { host.destroy_after_structural_dispose = true; shell_mapping_stable = false; } else { - if (host.hwnd) |hwnd| _ = DestroyWindow(hwnd); + if (host.hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); self.removeHost(host); } } else if (self.running) { - if (self.activeTab(host)) |tab| if (tab.focusedSurface()) |replacement| self.activateSurface(replacement); + if (activeTab(host)) |tab| if (tab.focusedSurface()) |replacement| self.activateSurface(replacement); // Relayout after tab removal because the chrome // reservation may have changed. In this fork, // `.auto` still shows the row since it carries the // essential host controls (+, overflow). - host.layout() catch {}; + runUiActionOrLog("surface close layout failed", host.layout()); } if (shell_mapping_changed and shell_mapping_stable) { self.auditShellNativeMapping("surface-close"); @@ -6506,7 +5317,7 @@ pub const App = struct { const stored_entry = host.pushStructuralUndo(entry) catch |err| { log.warn("close_tab undo snapshot failed err={}", .{err}); var failed = entry; - errdefer failed.dispose(host, .normal) catch {}; + errdefer runUiActionOrLog("close tab rollback dispose failed", failed.dispose(host, .normal)); _ = try host.restoreClosedTabEntryNative(&failed.payload.close_tab); if (host.activeSurface()) |restored| self.activateSurface(restored); return err; @@ -6539,9 +5350,12 @@ pub const App = struct { self.activateSurface(replacement); } else { host.refocusHostWindow(); - host.layout() catch {}; - host.refreshChrome() catch {}; - host.setBanner(.info, "Tab closed. Use undo to restore it.") catch {}; + runUiActionOrLog("tab close layout failed", host.layout()); + runUiActionOrLog("tab close chrome refresh failed", host.refreshChrome()); + runUiActionOrLog( + "tab close banner failed", + host.setBanner(.info, "Tab closed. Use undo to restore it."), + ); } return true; }, @@ -6644,12 +5458,11 @@ pub const App = struct { return null; } - fn hideQuickTerminalSurface(self: *App, surface: *Surface) void { - _ = self; + fn hideQuickTerminalSurface(surface: *Surface) void { const top_level = surface.windowHwnd() orelse return; if (surface.host) |host| host.cancelQuickTerminalAnimation(); surface.setVisible(false); - _ = ShowWindow(top_level, SW_HIDE); + _ = sys.ShowWindow(top_level, c.SW_HIDE); } fn quickTerminalFocusPolicy(self: *App) win32_quick_terminal.FocusPolicy { @@ -6689,17 +5502,17 @@ pub const App = struct { // when hidden, and would run `MonitorFromWindow` / // `SetWindowPos` against the wrong target when showing. const top_level = surface.windowHwnd() orelse return; - const visible = IsWindowVisible(top_level) != 0; + const visible = sys.IsWindowVisible(top_level) != 0; if (visible) { - self.hideQuickTerminalSurface(surface); + hideQuickTerminalSurface(surface); } else { self.windows_hidden = false; self.applyQuickTerminalGeometry(top_level, true) catch |err| { std.log.warn("QT geometry failed err={}", .{err}); }; - _ = ShowWindow( + _ = sys.ShowWindow( top_level, - if (want_focus) SW_SHOW else SW_SHOWNOACTIVATE, + if (want_focus) c.SW_SHOW else c.SW_SHOWNOACTIVATE, ); // Mirror the hide-side invariant: tell the surface // it's visible so the renderer wakes and the @@ -6782,12 +5595,12 @@ pub const App = struct { const mi = switch (qt_cfg.screen) { .focused => blk: { var pt: POINT = .{ .x = 0, .y = 0 }; - if (GetCursorPos(&pt) == 0) { - const monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) orelse + if (sys.GetCursorPos(&pt) == 0) { + const monitor = sys.MonitorFromWindow(hwnd, c.MONITOR_DEFAULTTONEAREST) orelse return error.MonitorLookupFailed; break :blk monitorInfo(monitor) orelse return error.MonitorInfoFailed; } - const monitor = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST) orelse + const monitor = sys.MonitorFromPoint(pt, c.MONITOR_DEFAULTTONEAREST) orelse return error.MonitorLookupFailed; break :blk monitorInfo(monitor) orelse return error.MonitorInfoFailed; }, @@ -6812,10 +5625,10 @@ pub const App = struct { const end = win32_quick_terminal.computeEndRect(qt_cfg, mi); const start = win32_quick_terminal.computeStartRect(qt_cfg, mi); const duration_ms = win32_quick_terminal.animationDurationMs(qt_cfg.animation_duration_s); - const flags = SWP_NOACTIVATE; + const flags = c.SWP_NOACTIVATE; if (!animate or duration_ms == 0 or rectEqual(start, end)) { if (getHost(hwnd)) |host| host.cancelQuickTerminalAnimation(); - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, HWND_TOPMOST, end.left, @@ -6828,7 +5641,7 @@ pub const App = struct { } const host = getHost(hwnd) orelse { - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, HWND_TOPMOST, end.left, @@ -6843,7 +5656,7 @@ pub const App = struct { const effective_duration_ms = host.effectiveTweenDuration(duration_ms); if (effective_duration_ms == 0) { host.cancelQuickTerminalAnimation(); - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, HWND_TOPMOST, end.left, @@ -6856,7 +5669,7 @@ pub const App = struct { } host.cancelQuickTerminalAnimation(); - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, HWND_TOPMOST, start.left, @@ -6878,7 +5691,7 @@ pub const App = struct { }; } else { host.cancelQuickTerminalAnimation(); - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, HWND_TOPMOST, end.left, @@ -6893,7 +5706,7 @@ pub const App = struct { fn showOnScreenKeyboard(self: *App) !void { const osk_w = try std.unicode.utf8ToUtf16LeAllocZ(self.core_app.alloc, "osk.exe"); defer self.core_app.alloc.free(osk_w); - const result = ShellExecuteW(null, shell_open, osk_w.ptr, null, null, SW_SHOW); + const result = sys.ShellExecuteW(null, shell_open, osk_w.ptr, null, null, c.SW_SHOW); if (@intFromPtr(result) <= 32) return error.OpenUrlFailed; } @@ -6907,7 +5720,7 @@ pub const App = struct { continue; }; host.clearStructuralHistory(.normal); - _ = DestroyWindow(hwnd); + _ = sys.DestroyWindow(hwnd); } } @@ -6919,9 +5732,9 @@ pub const App = struct { defer self.session_restore_rollback_host = null; host.clearStructuralHistory(.normal); if (host.hwnd) |hwnd| { - if (DestroyWindow(hwnd) == 0) { + if (sys.DestroyWindow(hwnd) == 0) { const destroy_err = windows.kernel32.GetLastError(); - if (IsWindow(hwnd) != 0) { + if (sys.IsWindow(hwnd) != 0) { log.err("failed to destroy partially restored host; retaining ownership err={}", .{ destroy_err, }); @@ -6954,8 +5767,8 @@ pub const App = struct { const profile_w = try std.unicode.utf8ToUtf16LeAllocZ(self.core_app.alloc, profile); defer self.core_app.alloc.free(profile_w); - if (SetCurrentDirectoryW(profile_w.ptr) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.SetCurrentDirectoryW(profile_w.ptr) == 0) { + return lastError(); } log.debug("win32 current directory reset cwd={s}", .{profile}); @@ -6964,8 +5777,8 @@ pub const App = struct { fn clientSize(self: *App, hwnd: HWND) !apprt.SurfaceSize { _ = self; var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetClientRect(hwnd, &rect) == 0) { + return lastError(); } return .{ @@ -6988,8 +5801,8 @@ pub const App = struct { return .{ .nSize = @sizeOf(PIXELFORMATDESCRIPTOR), .nVersion = 1, - .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, - .iPixelType = PFD_TYPE_RGBA, + .dwFlags = c.PFD_DRAW_TO_WINDOW | c.PFD_SUPPORT_OPENGL | c.PFD_DOUBLEBUFFER, + .iPixelType = c.PFD_TYPE_RGBA, .cColorBits = 32, .cRedBits = 0, .cRedShift = 0, @@ -7007,7 +5820,7 @@ pub const App = struct { .cDepthBits = 24, .cStencilBits = 8, .cAuxBuffers = 0, - .iLayerType = PFD_MAIN_PLANE, + .iLayerType = c.PFD_MAIN_PLANE, .bReserved = 0, .dwLayerMask = 0, .dwVisibleMask = 0, @@ -7018,34 +5831,34 @@ pub const App = struct { fn createGLContext(self: *App, hwnd: HWND) !struct { hdc: HDC, hglrc: HGLRC } { if (self.core_app.first) beginOpenGLStartupDiagnostics(); - const hdc = GetDC(hwnd) orelse { + const hdc = sys.GetDC(hwnd) orelse { recordOpenGLStartupWin32Failure(.get_dc, windows.kernel32.GetLastError()); return error.GetDCFailed; }; - errdefer _ = ReleaseDC(hwnd, hdc); + errdefer _ = sys.ReleaseDC(hwnd, hdc); const pfd = defaultPixelFormatDescriptor(); - const pixel_format = ChoosePixelFormat(hdc, &pfd); + const pixel_format = sys.ChoosePixelFormat(hdc, &pfd); if (pixel_format == 0) { const err = windows.kernel32.GetLastError(); recordOpenGLStartupWin32Failure(.choose_pixel_format, err); return windows.unexpectedError(err); } - if (SetPixelFormat(hdc, pixel_format, &pfd) == 0) { + if (sys.SetPixelFormat(hdc, pixel_format, &pfd) == 0) { const err = windows.kernel32.GetLastError(); recordOpenGLStartupWin32Failure(.set_pixel_format, err); return windows.unexpectedError(err); } - const hglrc = wglCreateContext(hdc) orelse { + const hglrc = sys.wglCreateContext(hdc) orelse { const err = windows.kernel32.GetLastError(); recordOpenGLStartupWin32Failure(.create_context, err); return windows.unexpectedError(err); }; - errdefer _ = wglDeleteContext(hglrc); + errdefer _ = sys.wglDeleteContext(hglrc); - if (wglMakeCurrent(hdc, hglrc) == 0) { + if (sys.wglMakeCurrent(hdc, hglrc) == 0) { const err = windows.kernel32.GetLastError(); recordOpenGLStartupWin32Failure(.initial_make_current, err); return windows.unexpectedError(err); @@ -7058,7 +5871,7 @@ pub const App = struct { const url_w = try std.unicode.utf8ToUtf16LeAllocZ(self.core_app.alloc, url); defer self.core_app.alloc.free(url_w); - const result = ShellExecuteW(null, shell_open, url_w.ptr, null, null, SW_SHOW); + const result = sys.ShellExecuteW(null, shell_open, url_w.ptr, null, null, c.SW_SHOW); if (@intFromPtr(result) <= 32) return error.OpenUrlFailed; } @@ -7113,7 +5926,7 @@ pub const App = struct { const tmp_path = try std.fmt.allocPrint( alloc, "{s}.tmp-{x}-{x}", - .{ target_path, GetCurrentProcessId(), unixMillis() }, + .{ target_path, sys.GetCurrentProcessId(), unixMillis() }, ); defer alloc.free(tmp_path); @@ -7198,7 +6011,7 @@ pub const App = struct { // block so the file is closed BEFORE ReplaceFileW — Windows // blocks the replace while the temp handle is open. var temp_pending = false; - defer if (temp_pending) std.fs.cwd().deleteFile(tmp_path) catch {}; + defer if (temp_pending) std.fs.cwd().deleteFile(tmp_path) catch {}; // Best-effort temp cleanup. { const tmp = std.fs.cwd().createFile(tmp_path, .{}) catch return error.TempCreateFailed; temp_pending = true; @@ -7215,11 +6028,11 @@ pub const App = struct { const tmp_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, tmp_path) catch return error.OutOfMemory; defer alloc.free(tmp_w); - if (ReplaceFileW(target_w, tmp_w, null, 0, null, null) == 0) { + if (sys.ReplaceFileW(target_w, tmp_w, null, 0, null, null) == 0) { // Likely ERROR_FILE_NOT_FOUND if the user didn't have a // config file yet. Fall back to MoveFileExW. - if (MoveFileExW(tmp_w, target_w, MOVEFILE_REPLACE_EXISTING) == 0) { - std.fs.cwd().deleteFile(tmp_path) catch {}; + if (sys.MoveFileExW(tmp_w, target_w, c.MOVEFILE_REPLACE_EXISTING) == 0) { + std.fs.cwd().deleteFile(tmp_path) catch {}; // Preserve the primary replace failure. return error.ReplaceFailed; } } @@ -7340,7 +6153,7 @@ pub const App = struct { fn ringBell(self: *App, target: apprt.Target) void { _ = self; _ = target; - _ = MessageBeep(MB_ICONINFORMATION); + _ = sys.MessageBeep(c.MB_ICONINFORMATION); } fn showDesktopNotificationWithLaunch( @@ -7380,7 +6193,7 @@ pub const App = struct { // notifications with an empty `body` (e.g. settings-save // success, where title alone carries the signal) still render // something readable. - try self.showInfoMessage(target, caption, message); + try showInfoMessage(target, caption, message); } fn showDesktopNotification( @@ -7405,7 +6218,7 @@ pub const App = struct { ); defer self.core_app.alloc.free(message); if (try self.showHostBanner(target, .info, message)) return; - try self.showInfoMessage(target, "noctty", message); + try showInfoMessage(target, "noctty", message); } const CommandFinishPlan = struct { @@ -7488,12 +6301,10 @@ pub const App = struct { } fn showInfoMessage( - self: *App, target: apprt.Target, title: []const u8, message: []const u8, ) !void { - _ = self; _ = target; // Final fallback for notification paths that cannot surface a // host banner. Keep failures observable without blocking the UI @@ -7708,17 +6519,17 @@ fn appendInnoQuotedArg( ) !void { try buf.appendSlice(alloc, prefix); try buf.append(alloc, '"'); - for (value) |c| { - if (c == '"') try buf.append(alloc, '"'); - try buf.append(alloc, c); + for (value) |char| { + if (char == '"') try buf.append(alloc, '"'); + try buf.append(alloc, char); } try buf.append(alloc, '"'); } fn postUpdateCheckCompletion(ui_thread_id: DWORD, completion: *UpdateCheckCompletion) void { - if (PostThreadMessageW( + if (sys.PostThreadMessageW( ui_thread_id, - WM_WINHOSTTY_UPDATE, + c.WM_WINHOSTTY_UPDATE, 0, @as(LPARAM, @bitCast(@as(usize, @intFromPtr(completion)))), ) == 0) { @@ -7752,9 +6563,9 @@ fn winrtToastActivationCallback(ctx: *anyopaque, launch: []const u8) void { }; activation.* = parsed; - if (PostThreadMessageW( + if (sys.PostThreadMessageW( ui_thread_id, - WM_WINHOSTTY_TOAST_ACTIVATION, + c.WM_WINHOSTTY_TOAST_ACTIVATION, 0, @as(LPARAM, @bitCast(@as(usize, @intFromPtr(activation)))), ) == 0) { @@ -7766,12 +6577,7 @@ fn winrtToastActivationCallback(ctx: *anyopaque, launch: []const u8) void { } } -const SearchStatus = struct { - active: bool = false, - needle: ?[]const u8 = null, - total: ?usize = null, - selected: ?usize = null, -}; +const SearchStatus = labels.SearchStatus; /// Which caption button (integrated titlebar) the cursor is hovering. const CaptionButton = enum { none, minimize, maximize, close }; @@ -7874,17 +6680,17 @@ fn titlebarRoleFromCaption(button: CaptionButton) TitlebarButtonRole { fn titlebarCaptionFromHitTest(ht: i32) CaptionButton { return switch (ht) { - HTMINBUTTON => .minimize, - HTMAXBUTTON => .maximize, - HTCLOSE => .close, + c.HTMINBUTTON => .minimize, + c.HTMAXBUTTON => .maximize, + c.HTCLOSE => .close, else => .none, }; } fn captionButtonSysCommand(ht: i32, is_zoomed: bool) ?WPARAM { return switch (ht) { - HTMINBUTTON => SC_MINIMIZE, - HTMAXBUTTON => if (is_zoomed) SC_RESTORE else SC_MAXIMIZE, + c.HTMINBUTTON => c.SC_MINIMIZE, + c.HTMAXBUTTON => if (is_zoomed) c.SC_RESTORE else c.SC_MAXIMIZE, else => null, }; } @@ -7965,9 +6771,9 @@ const PendingClipboardOp = union(enum) { switch (self.*) { .paste => |*p| alloc.free(p.data), .write => |*w| { - for (w.contents) |*c| { - alloc.free(c.mime); - alloc.free(c.data); + for (w.contents) |*content| { + alloc.free(content.mime); + alloc.free(content.data); } alloc.free(w.contents); }, @@ -7975,21 +6781,8 @@ const PendingClipboardOp = union(enum) { } }; -const SurfaceStatus = struct { - pwd: ?[]const u8 = null, - scrollbar: terminal.Scrollbar = .zero, - readonly: bool = false, - secure_input: bool = false, - key_sequence_active: bool = false, - key_table_name: ?[]const u8 = null, - search: SearchStatus = .{}, - progress: ?[]const u8 = null, -}; - -const HostTabStatus = struct { - index: usize = 0, - total: usize = 1, -}; +const SurfaceStatus = labels.SurfaceStatus; +const HostTabStatus = labels.HostTabStatus; /// Payload for a non-modal confirm overlay. The callbacks receive the /// `userdata` pointer passed at construction time so the issuer can @@ -8029,17 +6822,6 @@ const ScrollbarPolicy = enum { always, }; -const VisibleTabRange = struct { - start: usize, - count: usize, -}; - -const TabOverviewEntry = struct { - title: ?[]const u8 = null, - pane_count: usize = 1, - active: bool = false, -}; - const SplitTreeSurface = SplitTree(Surface); const Tab = struct { @@ -8745,7 +7527,7 @@ const Host = struct { return .{ .kind = .close_tab, - .timestamp_ms = GetTickCount64(), + .timestamp_ms = sys.GetTickCount64(), .sequence_id = self.app.nextUndoSequence(), .payload = .{ .close_tab = .{ .tab = removed, @@ -9059,8 +7841,8 @@ const Host = struct { self.app.activateSurface(surface); } else { self.refocusHostWindow(); - self.layout() catch {}; - self.refreshChrome() catch {}; + runUiActionOrLog("redo close layout failed", self.layout()); + runUiActionOrLog("redo close chrome refresh failed", self.refreshChrome()); } return "Tab closed again."; }, @@ -9090,8 +7872,8 @@ const Host = struct { fn clientAnimationsEnabled() bool { var enabled: BOOL = 1; - if (SystemParametersInfoW( - SPI_GETCLIENTAREAANIMATION, + if (sys.SystemParametersInfoW( + c.SPI_GETCLIENTAREAANIMATION, 0, @ptrCast(&enabled), 0, @@ -9128,7 +7910,7 @@ const Host = struct { duration_ms: u16, easing: [4]f32, ) ?win32_tween.TweenId { - const now = GetTickCount64(); + const now = sys.GetTickCount64(); const effective_duration = self.effectiveTweenDuration(duration_ms); const id = self.tween_sched.add(now, .{ @@ -9145,7 +7927,7 @@ const Host = struct { fn ensureTweenTimer(self: *Host) void { if (self.tween_timer_active) return; const hwnd = self.hwnd orelse return; - const prev = SetTimer(hwnd, TWEEN_TIMER_ID, TWEEN_TIMER_INTERVAL_MS, null); + const prev = sys.SetTimer(hwnd, c.TWEEN_TIMER_ID, c.TWEEN_TIMER_INTERVAL_MS, null); if (prev == 0) { std.log.warn("tween: SetTimer failed; animations will be static this tick", .{}); return; @@ -9159,25 +7941,25 @@ const Host = struct { self.tween_timer_active = false; return; }; - _ = KillTimer(hwnd, TWEEN_TIMER_ID); + _ = sys.KillTimer(hwnd, c.TWEEN_TIMER_ID); self.tween_timer_active = false; } fn tickTweens(self: *Host) void { - const now = GetTickCount64(); + const now = sys.GetTickCount64(); var quick_terminal_finished = false; if (self.quick_terminal_animation) |anim| { if (self.tween_sched.value(anim.id, now)) |value| { const rect = win32_quick_terminal.lerpRect(anim.start, anim.end, value); if (self.hwnd) |hwnd| { - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, HWND_TOPMOST, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, - SWP_NOACTIVATE, + c.SWP_NOACTIVATE, ); } quick_terminal_finished = value >= 1.0; @@ -9189,14 +7971,14 @@ const Host = struct { if (quick_terminal_finished) { if (self.quick_terminal_animation) |anim| { if (self.hwnd) |hwnd| { - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, HWND_TOPMOST, anim.end.left, anim.end.top, anim.end.right - anim.end.left, anim.end.bottom - anim.end.top, - SWP_NOACTIVATE, + c.SWP_NOACTIVATE, ); } self.quick_terminal_animation = null; @@ -9213,14 +7995,14 @@ const Host = struct { // frame. When both slots converge, we stop forcing repaints. if (self.tab_close_hover_hwnd) |tab_hwnd| { if (self.tab_close_hover.animating(now)) { - _ = InvalidateRect(tab_hwnd, null, 0); + _ = sys.InvalidateRect(tab_hwnd, null, 0); } else { self.tab_close_hover_hwnd = null; } } if (self.tab_close_prev_hwnd) |prev_hwnd| { if (self.tab_close_prev.animating(now)) { - _ = InvalidateRect(prev_hwnd, null, 0); + _ = sys.InvalidateRect(prev_hwnd, null, 0); } else { self.tab_close_prev_hwnd = null; } @@ -9240,7 +8022,7 @@ const Host = struct { fn ensureResizeSettleTimer(self: *Host) void { if (self.resize_settle_timer_active) return; const hwnd = self.hwnd orelse return; - const prev = SetTimer(hwnd, RESIZE_SETTLE_TIMER_ID, RESIZE_SETTLE_TIMER_INTERVAL_MS, null); + const prev = sys.SetTimer(hwnd, c.RESIZE_SETTLE_TIMER_ID, c.RESIZE_SETTLE_TIMER_INTERVAL_MS, null); if (prev == 0) { std.log.warn("resize-settle: SetTimer failed; final pane repaint may wait for input", .{}); return; @@ -9254,12 +8036,12 @@ const Host = struct { self.resize_settle_timer_active = false; return; }; - _ = KillTimer(hwnd, RESIZE_SETTLE_TIMER_ID); + _ = sys.KillTimer(hwnd, c.RESIZE_SETTLE_TIMER_ID); self.resize_settle_timer_active = false; } fn startResizeSettleRepaints(self: *Host) void { - self.resize_settle_repaint_ticks = RESIZE_SETTLE_REPAINT_TICKS; + self.resize_settle_repaint_ticks = c.RESIZE_SETTLE_REPAINT_TICKS; self.ensureResizeSettleTimer(); } @@ -9302,7 +8084,7 @@ const Host = struct { return; } - const now = GetTickCount64(); + const now = sys.GetTickCount64(); self.tab_underline.retargetTo(new_left, new_width, now); // Keep the 16 ms heartbeat alive for the slide duration so // `paintChrome` re-runs every frame and reads the interpolated @@ -9332,14 +8114,14 @@ const Host = struct { const params: *NCCALCSIZE_PARAMS = @ptrFromInt(@as(usize, @bitCast(lParam))); const original = params.rgrc[0]; // Let DWP compute the default side/bottom frame… - _ = DefWindowProcW(hwnd, WM_NCCALCSIZE, wParam, lParam); + _ = sys.DefWindowProcW(hwnd, c.WM_NCCALCSIZE, wParam, lParam); // …then compute the top edge through the shared NC-layout // math. Side / bottom stay DWP-owned so the stock frame // widths survive, while the integrated-caption policy // (caption row inside client + maximized invisible-margin // compensation) stays centralized in `win32_nc_layout`. const metrics = win32_nc_layout.metricsDefault(self.current_dpi); - const state: win32_nc_layout.WindowState = if (IsZoomed(hwnd) != 0) .maximized else .normal; + const state: win32_nc_layout.WindowState = if (sys.IsZoomed(hwnd) != 0) .maximized else .normal; const adjusted = win32_nc_layout.calcNcClientRect(.{ .left = params.rgrc[0].left, .top = original.top, @@ -9360,7 +8142,7 @@ const Host = struct { const cy: i32 = @as(i16, @bitCast(@as(u16, @truncate(bits >> 16)))); var win_rect: RECT = undefined; - if (GetWindowRect(hwnd, &win_rect) == 0) return null; + if (sys.GetWindowRect(hwnd, &win_rect) == 0) return null; const window_rect: win32_nc_layout.Rect = .{ .left = win_rect.left, @@ -9371,24 +8153,24 @@ const Host = struct { const cursor: win32_nc_layout.Point = .{ .x = cx, .y = cy }; const metrics = win32_nc_layout.metricsDefault(self.current_dpi); const state: win32_nc_layout.WindowState = - if (IsZoomed(hwnd) != 0) .maximized else .normal; + if (sys.IsZoomed(hwnd) != 0) .maximized else .normal; const ht = win32_nc_layout.hitTest(window_rect, cursor, metrics, state); return switch (ht) { - .nowhere => HTNOWHERE, - .client => HTCLIENT, - .caption => HTCAPTION, - .sysmenu => HTSYSMENU, - .minbutton => HTMINBUTTON, - .maxbutton => HTMAXBUTTON, - .left => HTLEFT, - .right => HTRIGHT, - .top => HTTOP, - .topleft => HTTOPLEFT, - .topright => HTTOPRIGHT, - .bottom => HTBOTTOM, - .bottomleft => HTBOTTOMLEFT, - .bottomright => HTBOTTOMRIGHT, - .close => HTCLOSE, + .nowhere => c.HTNOWHERE, + .client => c.HTCLIENT, + .caption => c.HTCAPTION, + .sysmenu => c.HTSYSMENU, + .minbutton => c.HTMINBUTTON, + .maxbutton => c.HTMAXBUTTON, + .left => c.HTLEFT, + .right => c.HTRIGHT, + .top => c.HTTOP, + .topleft => c.HTTOPLEFT, + .topright => c.HTTOPRIGHT, + .bottom => c.HTBOTTOM, + .bottomleft => c.HTBOTTOMLEFT, + .bottomright => c.HTBOTTOMRIGHT, + .close => c.HTCLOSE, }; } @@ -9405,11 +8187,11 @@ const Host = struct { if (!self.caption_track_armed) { var track: TRACKMOUSEEVENT = .{ .cbSize = @sizeOf(TRACKMOUSEEVENT), - .dwFlags = TME_LEAVE | TME_NONCLIENT, + .dwFlags = c.TME_LEAVE | c.TME_NONCLIENT, .hwndTrack = hwnd, .dwHoverTime = 0, }; - if (TrackMouseEvent(&track) != 0) { + if (sys.TrackMouseEvent(&track) != 0) { self.caption_track_armed = true; } } @@ -9500,7 +8282,7 @@ const Host = struct { fn paletteListRowCapacity(self: *Host) usize { const hwnd = self.hwnd orelse return 0; var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) return 0; + if (sys.GetClientRect(hwnd, &rect) == 0) return 0; const list_top = self.tabBarHeight() + self.scaled(host_overlay_height); return paletteVisibleRowCapacity( rect.bottom - list_top - self.scaled(8), @@ -9669,7 +8451,11 @@ const Host = struct { } } - if (self.ensureProfiles() catch false) { + const profiles_ready = self.ensureProfiles() catch |err| blk: { + logUiActionError("palette profile load failed", err); + break :blk false; + }; + if (profiles_ready) { for (self.profiles.?) |profile| { _ = self.appendPaletteDescriptor(.{ .item = .{ @@ -9736,7 +8522,7 @@ const Host = struct { win32_uia.events.raiseStructureChanged(&provider.base, .children_invalidated, null); structure_announced_before_layout = true; } - const list_visible = if (self.palette_list_hwnd) |hwnd| IsWindowVisible(hwnd) != 0 else false; + const list_visible = if (self.palette_list_hwnd) |hwnd| sys.IsWindowVisible(hwnd) != 0 else false; if (paletteNoMatchNotificationTarget( self.palette_list_uia_provider != null, list_visible, @@ -9767,7 +8553,7 @@ const Host = struct { .{self.palette_list_ranked_count}, ); - if (self.palette_list_hwnd) |h| _ = InvalidateRect(h, null, 0); + if (self.palette_list_hwnd) |h| _ = sys.InvalidateRect(h, null, 0); if (self.palette_list_uia_provider) |provider| { if (!structure_announced_before_layout) { win32_uia.events.raiseStructureChanged(&provider.base, .children_invalidated, null); @@ -9790,7 +8576,7 @@ const Host = struct { } else if (next >= self.palette_list_scroll + visible_rows) { self.palette_list_scroll = next - visible_rows + 1; } - if (self.palette_list_hwnd) |h| _ = InvalidateRect(h, null, 0); + if (self.palette_list_hwnd) |h| _ = sys.InvalidateRect(h, null, 0); if (changed) { self.announcePaletteSelection(); self.previewSelectedPaletteTheme(); @@ -9947,7 +8733,7 @@ const Host = struct { } fn abortPaletteAction(self: *Host, message: []const u8) void { - self.setBanner(.err, message) catch {}; + runUiActionOrLog("palette abort banner failed", self.setBanner(.err, message)); self.announcePaletteOutcome(.action_aborted, message); } @@ -10226,7 +9012,10 @@ const Host = struct { self.previewPaletteTheme(name) catch |err| { log.warn("palette theme preview failed theme={s} err={}", .{ name, err }); self.revertPaletteThemePreview(); - self.setBanner(.err, "Theme preview failed; selection was not applied.") catch {}; + runUiActionOrLog( + "palette theme preview banner failed", + self.setBanner(.err, "Theme preview failed; selection was not applied."), + ); }; } @@ -10339,7 +9128,10 @@ const Host = struct { self.hideOverlay(); if (saved_but_masked) { log.warn("palette theme saved but masked by a later config layer theme={s}", .{theme_name}); - self.setBanner(.info, "Theme saved, but a later config layer masks it.") catch {}; + runUiActionOrLog( + "palette masked theme banner failed", + self.setBanner(.info, "Theme saved, but a later config layer masks it."), + ); } self.layout() catch |err| { log.warn("palette theme post-save layout failed err={}", .{err}); @@ -10437,7 +9229,7 @@ const Host = struct { @intCast(surface.core().id) else 0; - if (PostMessageW(hwnd, WM_WINHOSTTY_HOST_NEW_TAB, source_surface_id, 0) == 0) { + if (sys.PostMessageW(hwnd, c.WM_WINHOSTTY_HOST_NEW_TAB, source_surface_id, 0) == 0) { if (source_surface) |surface| { runUiActionOrLog("deferred new tab fallback failed", self.app.performAction(.{ .surface = surface.core() }, .new_tab, {})); } @@ -10483,7 +9275,7 @@ const Host = struct { } else if (self.palette_list_selected >= visible_end) { self.palette_list_selected = visible_end - 1; } - if (self.palette_list_hwnd) |h| _ = InvalidateRect(h, null, 0); + if (self.palette_list_hwnd) |h| _ = sys.InvalidateRect(h, null, 0); if (self.palette_list_selected != previous_selection) { self.announcePaletteSelection(); self.previewSelectedPaletteTheme(); @@ -10497,11 +9289,11 @@ const Host = struct { fn paintPaletteList(self: *Host) void { const hwnd = self.palette_list_hwnd orelse return; var ps: PAINTSTRUCT = undefined; - const hdc = BeginPaint(hwnd, &ps); - defer _ = EndPaint(hwnd, &ps); + const hdc = sys.BeginPaint(hwnd, &ps); + defer _ = sys.EndPaint(hwnd, &ps); var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) return; + if (sys.GetClientRect(hwnd, &rect) == 0) return; const theme = &self.app.resolved_theme; fillSolidRect(hdc, rect, theme.overlay_bg); @@ -10513,10 +9305,10 @@ const Host = struct { const row_h = self.scaled(palette_row_height); const padding = self.scaled(12); - _ = SetBkMode(hdc, TRANSPARENT); - const old_font: ?HGDIOBJ = if (self.chrome_font) |f| SelectObject(hdc, f) else null; + _ = sys.SetBkMode(hdc, c.TRANSPARENT); + const old_font: ?HGDIOBJ = if (self.chrome_font) |f| sys.SelectObject(hdc, f) else null; defer { - if (old_font) |f| _ = SelectObject(hdc, f); + if (old_font) |f| _ = sys.SelectObject(hdc, f); } const visible_end = @min( @@ -10590,19 +9382,19 @@ const Host = struct { // scheduler — leaving a SetTimer bound to a destroyed HWND // would panic the next time it fired. if (self.tween_timer_active) { - if (self.hwnd) |h| _ = KillTimer(h, TWEEN_TIMER_ID); + if (self.hwnd) |h| _ = sys.KillTimer(h, c.TWEEN_TIMER_ID); self.tween_timer_active = false; } if (self.search_timer_active) { - if (self.hwnd) |h| _ = KillTimer(h, SEARCH_TIMER_ID); + if (self.hwnd) |h| _ = sys.KillTimer(h, c.SEARCH_TIMER_ID); self.search_timer_active = false; } if (self.scrollbar_timer_active) { - if (self.hwnd) |h| _ = KillTimer(h, SCROLLBAR_TIMER_ID); + if (self.hwnd) |h| _ = sys.KillTimer(h, c.SCROLLBAR_TIMER_ID); self.scrollbar_timer_active = false; } if (self.resize_settle_timer_active) { - if (self.hwnd) |h| _ = KillTimer(h, RESIZE_SETTLE_TIMER_ID); + if (self.hwnd) |h| _ = sys.KillTimer(h, c.RESIZE_SETTLE_TIMER_ID); self.resize_settle_timer_active = false; } self.tween_sched.deinit(); @@ -10625,12 +9417,12 @@ const Host = struct { if (self.selected_profile_key) |value| self.app.core_app.alloc.free(value); if (self.cached_window_title) |value| self.app.core_app.alloc.free(value); self.freeCachedChromeStrings(); - if (self.chrome_brush) |brush| _ = DeleteObject(brush); - if (self.overlay_brush) |brush| _ = DeleteObject(brush); - if (self.edit_brush) |brush| _ = DeleteObject(brush); - if (self.chrome_font) |font| _ = DeleteObject(font); - if (self.titlebar_caption_icon_font) |font| _ = DeleteObject(font); - if (self.titlebar_action_icon_font) |font| _ = DeleteObject(font); + if (self.chrome_brush) |brush| _ = sys.DeleteObject(brush); + if (self.overlay_brush) |brush| _ = sys.DeleteObject(brush); + if (self.edit_brush) |brush| _ = sys.DeleteObject(brush); + if (self.chrome_font) |font| _ = sys.DeleteObject(font); + if (self.titlebar_caption_icon_font) |font| _ = sys.DeleteObject(font); + if (self.titlebar_action_icon_font) |font| _ = sys.DeleteObject(font); self.clearStructuralHistory(.host_destroy); self.structural_undo_entries.deinit(self.app.core_app.alloc); self.structural_redo_entries.deinit(self.app.core_app.alloc); @@ -10641,8 +9433,8 @@ const Host = struct { fn destroyChildControls(self: *Host) void { if (self.hwnd) |hwnd| { - if (IsWindow(hwnd) != 0) { - _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + if (sys.IsWindow(hwnd) != 0) { + setWindowData(hwnd, null); } } @@ -10712,7 +9504,7 @@ const Host = struct { fn refocusHostWindow(self: *Host) void { if (self.hwnd) |hwnd| { - _ = SetFocus(hwnd); + _ = sys.SetFocus(hwnd); } } @@ -10757,8 +9549,8 @@ const Host = struct { const hwnd = self.hwnd orelse return null; var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) return null; - const sb_h = self.statusBarHeight(); + if (sys.GetClientRect(hwnd, &rect) == 0) return null; + const sb_h = statusBarHeight(); if (sb_h == 0) return null; const status_y = @max(self.scaled(host_tab_height) + self.scaled(2), rect.bottom - sb_h + self.scaled(4)); var status_x: i32 = self.scaled(16); @@ -10809,7 +9601,7 @@ const Host = struct { for (self.tabs.items, 0..) |tab, i| { const btn = tab.button_hwnd orelse continue; var rect: RECT = undefined; - if (GetWindowRect(btn, &rect) == 0) continue; + if (sys.GetWindowRect(btn, &rect) == 0) continue; // Check if screen_x falls within the button's horizontal midpoint region if (screen_x >= rect.left and screen_x < rect.right) return i; } @@ -10854,13 +9646,13 @@ const Host = struct { self.tabs.items[a].clearRedoHistory(); self.tabs.items[b].clearRedoHistory(); self.app.auditShellNativeMapping("tab-drag-reorder"); - self.layout() catch {}; + runUiActionOrLog("tab drag reorder layout failed", self.layout()); } fn hideTabDropPreview(self: *Host) void { self.tab_drop_operation = .none; self.tab_drop_target_surface = null; - if (self.tab_drop_preview_hwnd) |preview| _ = ShowWindow(preview, SW_HIDE); + if (self.tab_drop_preview_hwnd) |preview| _ = sys.ShowWindow(preview, c.SW_HIDE); } fn updateTabDropPreview(self: *Host, source_index: usize, screen_point: POINT) void { @@ -10872,7 +9664,7 @@ const Host = struct { } const hwnd = self.hwnd orelse return self.hideTabDropPreview(); var client_point = screen_point; - if (ScreenToClient(hwnd, &client_point) == 0) return self.hideTabDropPreview(); + if (sys.ScreenToClient(hwnd, &client_point) == 0) return self.hideTabDropPreview(); const target_surface = target: { const tab = self.activeTab() orelse return self.hideTabDropPreview(); var it = tab.tree.iterator(); @@ -10899,11 +9691,11 @@ const Host = struct { } const preview_rect = target.preview_rect orelse return self.hideTabDropPreview(); if (self.tab_drop_preview_hwnd == null) { - self.tab_drop_preview_hwnd = CreateWindowExW( - WS_EX_LAYERED | WS_EX_TRANSPARENT, + self.tab_drop_preview_hwnd = sys.CreateWindowExW( + c.WS_EX_LAYERED | c.WS_EX_TRANSPARENT, prompt_label_class, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_CHILD, + c.WS_CHILD, preview_rect.left, preview_rect.top, preview_rect.right - preview_rect.left, @@ -10914,7 +9706,7 @@ const Host = struct { null, ); if (self.tab_drop_preview_hwnd) |preview| { - _ = SetLayeredWindowAttributes(preview, 0, 72, LWA_ALPHA); + _ = sys.SetLayeredWindowAttributes(preview, 0, 72, c.LWA_ALPHA); } } const preview = self.tab_drop_preview_hwnd orelse return; @@ -10927,18 +9719,18 @@ const Host = struct { .split_down => std.unicode.utf8ToUtf16LeStringLiteral("Split below"), .none, .new_tab => unreachable, }; - _ = SetWindowTextW(preview, operation_label); - _ = SetWindowPos( + _ = sys.SetWindowTextW(preview, operation_label); + _ = sys.SetWindowPos( preview, null, preview_rect.left, preview_rect.top, preview_rect.right - preview_rect.left, preview_rect.bottom - preview_rect.top, - SWP_NOACTIVATE, + c.SWP_NOACTIVATE, ); - _ = ShowWindow(preview, SW_SHOWNOACTIVATE); - _ = InvalidateRect(preview, null, 1); + _ = sys.ShowWindow(preview, c.SW_SHOWNOACTIVATE); + _ = sys.InvalidateRect(preview, null, 1); } fn commitTabDropSplit(self: *Host, source_index: usize) bool { @@ -11009,7 +9801,7 @@ const Host = struct { self.active_tab = target_index; _ = self.appendStructuralUndoAssumeCapacity(.{ .kind = .tab_subtree_transfer, - .timestamp_ms = GetTickCount64(), + .timestamp_ms = sys.GetTickCount64(), .sequence_id = self.app.nextUndoSequence(), .payload = .{ .tab_subtree_transfer = .{ .source_tab = removed, @@ -11050,7 +9842,7 @@ const Host = struct { } source_surface.host_active = true; source_surface.setVisible(true); - self.layout() catch {}; + runUiActionOrLog("tab subtree transfer layout failed", self.layout()); self.app.activateSurface(source_surface); self.app.auditShellNativeMapping("tab-subtree-transfer"); return true; @@ -11070,7 +9862,7 @@ const Host = struct { // with precomputed coords. Invoking layout after the index // change ensures the slide kicks off immediately on activation // instead of waiting for the next incidental layout tick. - self.layout() catch {}; + runUiActionOrLog("tab activation layout failed", self.layout()); if (self.tabs.items[index].focusedSurface()) |surface| { self.app.activateSurface(surface); self.forceHostCompositionPaint(); @@ -11086,13 +9878,6 @@ const Host = struct { return self.activateTabIndex(index); } - fn navigateActiveSearch(self: *Host, dir: input.Binding.Action.NavigateSearch) bool { - const surface = self.activeSurface() orelse return false; - if (!surface.search_active and !surface.search_bar.visible) return false; - _ = surface.navigateDockedSearch(dir) catch return false; - return true; - } - fn dismissActiveSearch(self: *Host) bool { const surface = self.activeSurface() orelse return false; if (!surface.search_active and !surface.search_bar.visible and self.overlay_mode != .search) return false; @@ -11106,18 +9891,10 @@ const Host = struct { return true; } - fn completeCommandPaletteFromButton(self: *Host, reverse: bool) bool { - if (self.overlay_mode != .command_palette) { - return self.toggleCommandPaletteFromButton(); - } - _ = self.completeCommandPalette(reverse) catch return false; - return true; - } - fn dismissCommandPalette(self: *Host) bool { if (self.overlay_mode != .command_palette) return false; self.hideOverlay(); - self.layout() catch {}; + runUiActionOrLog("command palette dismiss layout failed", self.layout()); self.forceHostCompositionPaint(); refocusActiveSurface(self); return true; @@ -11239,7 +10016,7 @@ const Host = struct { } fn refreshProfileChrome(self: *Host) void { - const status_h = self.statusBarHeight(); + const status_h = statusBarHeight(); if (!profileChromeVisible(self.overlay_mode, status_h)) return; if (self.overlay_mode == .profile) { _ = self.syncOverlayLabel() catch false; @@ -11260,12 +10037,15 @@ const Host = struct { fn toggleProfileOverlay(self: *Host) bool { if (!(self.reloadProfiles() catch false)) { - self.setBanner(.err, "No supported Windows profiles detected.") catch {}; + runUiActionOrLog( + "profile availability banner failed", + self.setBanner(.err, "No supported Windows profiles detected."), + ); return false; } if (self.overlay_mode == .profile) { self.hideOverlay(); - self.layout() catch {}; + runUiActionOrLog("profile overlay dismiss layout failed", self.layout()); self.forceHostCompositionPaint(); return true; } @@ -11276,16 +10056,6 @@ const Host = struct { return true; } - fn cycleSelectedProfile(self: *Host, reverse: bool) bool { - if (!(self.ensureProfiles() catch false)) return false; - const profiles = self.profiles.?; - const next = nextTabOverviewSelection(self.selected_profile + 1, profiles.len, reverse) - 1; - if (self.setSelectedProfileIndex(next) catch return false) { - self.refreshProfileChrome(); - } - return true; - } - fn stepProfileSelection(self: *Host, reverse: bool) !bool { if (self.overlay_mode != .profile) return false; if (!(try self.ensureProfiles())) return false; @@ -11300,26 +10070,34 @@ const Host = struct { const next = nextTabOverviewSelection(current_index + 1, profiles.len, reverse) - 1; const selection_changed = try self.setSelectedProfileIndex(next); _ = try self.setOverlayEditText(profiles[next].key); - _ = SendMessageW(edit_hwnd, EM_SETSEL, 0, -1); + _ = sys.SendMessageW(edit_hwnd, c.EM_SETSEL, 0, -1); if (selection_changed) self.refreshProfileChrome(); return true; } fn openSelectedProfile(self: *Host, open_target: ProfileOpenTarget) bool { - if (!(self.ensureProfiles() catch false)) return false; + const profiles_ready = self.ensureProfiles() catch |err| { + logUiActionError("selected profile load failed", err); + return false; + }; + if (!profiles_ready) return false; const profile = self.selectedProfile() orelse return false; const source = self.activeSurface() orelse return false; const surface = self.app.createProfileSurface(.{ .surface = source.core() }, profile, open_target) catch return false; if (self.overlay_mode == .profile) { self.hideOverlay(); - self.layout() catch {}; + runUiActionOrLog("profile launch layout failed", self.layout()); } self.app.activateSurface(surface); return true; } fn quickOpenProfileIndex(self: *Host, index: usize, open_target: ProfileOpenTarget) bool { - if (!(self.ensureProfiles() catch false)) return false; + const profiles_ready = self.ensureProfiles() catch |err| { + logUiActionError("quick profile load failed", err); + return false; + }; + if (!profiles_ready) return false; const profiles = self.profiles.?; if (index >= profiles.len) return false; if (self.setSelectedProfileIndex(index) catch return false) { @@ -11420,10 +10198,10 @@ const Host = struct { switch (role) { .minimize, .maximize, .close => self.repaintTopChrome(), .new_tab => { - if (self.new_tab_hwnd) |hwnd| _ = InvalidateRect(hwnd, null, 0); + if (self.new_tab_hwnd) |hwnd| _ = sys.InvalidateRect(hwnd, null, 0); }, .dropdown => { - if (self.overflow_hwnd) |hwnd| _ = InvalidateRect(hwnd, null, 0); + if (self.overflow_hwnd) |hwnd| _ = sys.InvalidateRect(hwnd, null, 0); }, .none => {}, } @@ -11439,7 +10217,7 @@ const Host = struct { return; } - self.titlebar_hover_fade.start(GetTickCount64(), duration); + self.titlebar_hover_fade.start(sys.GetTickCount64(), duration); self.titlebar_hover_fade_role = role; self.invalidateTitlebarButtonRole(role); _ = self.addTween(0.0, 1.0, duration, .{ 0.0, 0.0, 1.0, 1.0 }); @@ -11471,8 +10249,8 @@ const Host = struct { if (self.hovered_button_hwnd == child) return; const previous = self.hovered_button_hwnd; self.hovered_button_hwnd = child; - if (previous) |hwnd| _ = InvalidateRect(hwnd, null, 0); - if (child) |hwnd| _ = InvalidateRect(hwnd, null, 0); + if (previous) |hwnd| _ = sys.InvalidateRect(hwnd, null, 0); + if (child) |hwnd| _ = sys.InvalidateRect(hwnd, null, 0); const previous_titlebar_role = if (previous) |h| self.titlebarActionButtonRole(h) else .none; if (previous_titlebar_role != .none) self.startTitlebarHoverFade(previous_titlebar_role); @@ -11485,7 +10263,7 @@ const Host = struct { const prev_was_tab = if (previous) |h| self.isTabButton(h) else false; const new_is_tab = if (child) |h| self.isTabButton(h) else false; if (prev_was_tab or new_is_tab) { - const now = GetTickCount64(); + const now = sys.GetTickCount64(); if (prev_was_tab and new_is_tab) { // Adjacent tab transition: move the previous target @@ -11530,39 +10308,12 @@ const Host = struct { self.repaintStatusBar(); } - fn focusQuickSlotEdge(self: *Host, toward_start: bool) bool { - if (!(self.ensureProfiles() catch false)) return false; - const next = nextQuickSlotFocus( - self.profiles.?.len, - self.selectedProfileIndex(), - null, - !toward_start, - 3, - ) orelse return false; - self.setFocusedQuickSlot(next); - return true; - } - - fn cycleFocusedQuickSlot(self: *Host, reverse: bool) bool { - if (!(self.ensureProfiles() catch false)) return false; - const next = nextQuickSlotFocus( - self.profiles.?.len, - self.selectedProfileIndex(), - self.focused_quick_slot, - reverse, - 3, - ) orelse return false; - self.setFocusedQuickSlot(next); - return true; - } - - fn openFocusedQuickSlot(self: *Host, open_target: ProfileOpenTarget) bool { - const profile_index = self.focused_quick_slot orelse return false; - return self.quickOpenProfileIndex(profile_index, open_target); - } - fn assignSelectedProfileToQuickSlot(self: *Host, slot_ordinal: usize) bool { - if (!(self.ensureProfiles() catch false)) return false; + const profiles_ready = self.ensureProfiles() catch |err| { + logUiActionError("quick slot profile load failed", err); + return false; + }; + if (!profiles_ready) return false; const profile = self.selectedProfile() orelse return false; self.app.setLauncherQuickSlotPreference(slot_ordinal, profile.key) catch return false; const message = std.fmt.allocPrint( @@ -11571,14 +10322,14 @@ const Host = struct { .{ profile.label, slot_ordinal + 1 }, ) catch return false; defer self.app.core_app.alloc.free(message); - self.setBanner(.info, message) catch {}; + runUiActionOrLog("quick slot pin banner failed", self.setBanner(.info, message)); return true; } fn clearQuickSlotPins(self: *Host) bool { self.app.clearLauncherQuickSlotPreferences(); self.setFocusedQuickSlot(null); - self.setBanner(.info, "Cleared quick slot pins.") catch {}; + runUiActionOrLog("quick slot clear banner failed", self.setBanner(.info, "Cleared quick slot pins.")); return true; } @@ -11588,14 +10339,10 @@ const Host = struct { proc: WNDPROC, prev_slot: *?*const anyopaque, ) void { - _ = SetWindowLongPtrW( - button_hwnd, - GWLP_USERDATA, - @as(LONG_PTR, @intCast(@intFromPtr(self))), - ); - const previous = SetWindowLongPtrW( + setWindowData(button_hwnd, self); + const previous = sys.SetWindowLongPtrW( button_hwnd, - GWLP_WNDPROC, + c.GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(proc))), ); prev_slot.* = if (previous == 0) @@ -11612,20 +10359,20 @@ const Host = struct { while (it.next()) |entry| entry.view.setVisible(false); } } - _ = ShowWindow(hwnd, if (visible) SW_SHOW else SW_HIDE); + _ = sys.ShowWindow(hwnd, if (visible) c.SW_SHOW else c.SW_HIDE); } fn present(self: *Host) void { const hwnd = self.hwnd orelse return; if (hostPresentShowCommand( - IsWindowVisible(hwnd) != 0, - IsIconic(hwnd) != 0, - IsZoomed(hwnd) != 0, + sys.IsWindowVisible(hwnd) != 0, + sys.IsIconic(hwnd) != 0, + sys.IsZoomed(hwnd) != 0, )) |show_cmd| { - _ = ShowWindow(hwnd, show_cmd); + _ = sys.ShowWindow(hwnd, show_cmd); } - _ = SetForegroundWindow(hwnd); - _ = SetFocus(hwnd); + _ = sys.SetForegroundWindow(hwnd); + _ = sys.SetFocus(hwnd); } fn setBanner(self: *Host, kind: HostBannerKind, text: ?[]const u8) !void { @@ -11675,11 +10422,11 @@ const Host = struct { const hwnd = self.hwnd orelse return; if (self.overlay_edit_hwnd != null) return; - self.overlay_label_hwnd = CreateWindowExW( + self.overlay_label_hwnd = sys.CreateWindowExW( 0, prompt_label_class, host_overlay_command_palette_label, - WS_CHILD, + c.WS_CHILD, 0, 0, 80, @@ -11688,13 +10435,13 @@ const Host = struct { @ptrFromInt(2001), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); - self.overlay_edit_hwnd = CreateWindowExW( + self.overlay_edit_hwnd = sys.CreateWindowExW( 0, prompt_edit_class, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_CHILD | WS_TABSTOP | ES_AUTOHSCROLL, + c.WS_CHILD | c.WS_TABSTOP | c.ES_AUTOHSCROLL, 0, 0, 100, @@ -11703,17 +10450,13 @@ const Host = struct { @ptrFromInt(2002), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); const edit_hwnd = self.overlay_edit_hwnd.?; - _ = SetWindowLongPtrW( - edit_hwnd, - GWLP_USERDATA, - @as(LONG_PTR, @intCast(@intFromPtr(self))), - ); - const previous = SetWindowLongPtrW( + setWindowData(edit_hwnd, self); + const previous = sys.SetWindowLongPtrW( edit_hwnd, - GWLP_WNDPROC, + c.GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(&overlayEditProc))), ); self.overlay_edit_prev_proc = if (previous == 0) @@ -11722,10 +10465,10 @@ const Host = struct { @ptrFromInt(@as(usize, @intCast(previous))); const edit_margin = @as(u16, @intCast(@max(self.scaled(10), 6))); const packed_margins: isize = @intCast(@as(usize, edit_margin) | (@as(usize, edit_margin) << 16)); - _ = SendMessageW( + _ = sys.SendMessageW( edit_hwnd, - EM_SETMARGINS, - EC_LEFTMARGIN | EC_RIGHTMARGIN, + c.EM_SETMARGINS, + c.EC_LEFTMARGIN | c.EC_RIGHTMARGIN, packed_margins, ); self.overlay_edit_uia_provider = if (self.app.com_initialized) @@ -11738,15 +10481,15 @@ const Host = struct { break :blk null; } else blk: { - log.warn("overlay edit UIA provider disabled: UI thread is not a confirmed STA", .{}); + log.debug("overlay edit UIA provider disabled: UI thread is not a confirmed STA", .{}); break :blk null; }; - self.overlay_hint_hwnd = CreateWindowExW( + self.overlay_hint_hwnd = sys.CreateWindowExW( 0, prompt_label_class, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_CHILD, + c.WS_CHILD, 0, 0, 100, @@ -11755,13 +10498,13 @@ const Host = struct { @ptrFromInt(2005), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); - self.overlay_accept_hwnd = CreateWindowExW( + self.overlay_accept_hwnd = sys.CreateWindowExW( 0, prompt_button_class, prompt_ok_label, - WS_CHILD | WS_TABSTOP | BS_OWNERDRAW, + c.WS_CHILD | c.WS_TABSTOP | c.BS_OWNERDRAW, 0, 0, 70, @@ -11770,14 +10513,14 @@ const Host = struct { @ptrFromInt(2003), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); self.subclassButton(self.overlay_accept_hwnd.?, &hostButtonProc, &self.overlay_button_prev_proc); - self.overlay_cancel_hwnd = CreateWindowExW( + self.overlay_cancel_hwnd = sys.CreateWindowExW( 0, prompt_button_class, prompt_cancel_label, - WS_CHILD | WS_TABSTOP | BS_OWNERDRAW, + c.WS_CHILD | c.WS_TABSTOP | c.BS_OWNERDRAW, 0, 0, 80, @@ -11786,17 +10529,17 @@ const Host = struct { @ptrFromInt(2004), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); self.subclassButton(self.overlay_cancel_hwnd.?, &hostButtonProc, &self.overlay_button_prev_proc); // Palette list — shown only when overlay_mode == .command_palette. // Stamped with a pointer to the Host via GWLP_USERDATA so the // wndproc can dispatch clicks back to `host.invokePaletteRow`. - self.palette_list_hwnd = CreateWindowExW( + self.palette_list_hwnd = sys.CreateWindowExW( 0, palette_list_class_name, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_CHILD, + c.WS_CHILD, 0, 0, 320, @@ -11805,12 +10548,8 @@ const Host = struct { @ptrFromInt(2006), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); - _ = SetWindowLongPtrW( - self.palette_list_hwnd.?, - GWLP_USERDATA, - @as(LONG_PTR, @intCast(@intFromPtr(self))), - ); + ) orelse return lastError(); + setWindowData(self.palette_list_hwnd.?, self); self.palette_list_uia_provider = if (self.app.com_initialized) win32_uia.PaletteListProvider.create( std.heap.page_allocator, @@ -11821,7 +10560,7 @@ const Host = struct { break :blk null; } else blk: { - log.warn("palette UIA provider disabled: UI thread is not a confirmed STA", .{}); + log.debug("palette UIA provider disabled: UI thread is not a confirmed STA", .{}); break :blk null; }; @@ -11832,11 +10571,11 @@ const Host = struct { const hwnd = self.hwnd orelse return; if (self.new_tab_hwnd == null) { - self.new_tab_hwnd = CreateWindowExW( + self.new_tab_hwnd = sys.CreateWindowExW( 0, prompt_button_class, host_tab_new_button_label, - WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_OWNERDRAW, + c.WS_CHILD | c.WS_VISIBLE | c.WS_TABSTOP | c.BS_OWNERDRAW, 0, 0, host_tab_small_button_width, @@ -11845,16 +10584,16 @@ const Host = struct { @ptrFromInt(1904), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); self.subclassButton(self.new_tab_hwnd.?, &hostButtonProc, &self.chrome_button_prev_proc); } if (self.overflow_hwnd == null) { - self.overflow_hwnd = CreateWindowExW( + self.overflow_hwnd = sys.CreateWindowExW( 0, prompt_button_class, host_tab_dropdown_button_label, - WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_OWNERDRAW, + c.WS_CHILD | c.WS_VISIBLE | c.WS_TABSTOP | c.BS_OWNERDRAW, 0, 0, host_tab_overflow_button_width, @@ -11863,7 +10602,7 @@ const Host = struct { @ptrFromInt(1911), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); self.subclassButton(self.overflow_hwnd.?, &hostButtonProc, &self.chrome_button_prev_proc); } } @@ -11928,16 +10667,16 @@ const Host = struct { // Show body as hint label. Prefer the visible Accept button, // then the visible Cancel button, as the keyboard-focus target. // The destructive action remains one explicit click/Enter away. - const initial_target = if (IsWindowVisible(accept_hwnd) != 0) + const initial_target = if (sys.IsWindowVisible(accept_hwnd) != 0) accept_hwnd - else if (IsWindowVisible(cancel_hwnd) != 0) + else if (sys.IsWindowVisible(cancel_hwnd) != 0) cancel_hwnd else return; - _ = SetFocus(initial_target); + _ = sys.SetFocus(initial_target); } else { - _ = SetFocus(edit_hwnd); - _ = SendMessageW(edit_hwnd, EM_SETSEL, 0, -1); + _ = sys.SetFocus(edit_hwnd); + _ = sys.SendMessageW(edit_hwnd, c.EM_SETSEL, 0, -1); } self.forceOverlayTransitionPaint(); } @@ -11946,7 +10685,7 @@ const Host = struct { const was_confirm = self.overlay_mode == .confirm; const was_palette = self.overlay_mode == .command_palette; const restore_terminal_focus = shouldRefocusAfterOverlayHide( - GetFocus(), + sys.GetFocus(), self.overlay_edit_hwnd, self.overlay_accept_hwnd, self.overlay_cancel_hwnd, @@ -11955,7 +10694,7 @@ const Host = struct { self.revertPaletteThemePreview(); self.overlay_mode = .none; self.clearOverlayCompletion(); - self.setBanner(.none, null) catch {}; + runUiActionOrLog("overlay banner clear failed", self.setBanner(.none, null)); // Drop any active confirm payload. Both accept and cancel // paths route through hideOverlay, so this is the one place // the owned byte slices get freed. Accept/cancel callbacks @@ -11980,7 +10719,7 @@ const Host = struct { self.invalidateOverlayTransitionPlacementCache(); self.forceHostCompositionPaint(); if (was_confirm or was_palette) { - self.layout() catch {}; + runUiActionOrLog("overlay hide layout failed", self.layout()); self.forceVisibleSurfaceRepaintsNow(); } if (restore_terminal_focus) { @@ -12408,49 +11147,49 @@ const Host = struct { fn ensureThemeBrushes(self: *Host) !void { const theme = &self.app.resolved_theme; if (self.chrome_brush == null) { - self.chrome_brush = CreateSolidBrush(theme.chrome_bg) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + self.chrome_brush = sys.CreateSolidBrush(theme.chrome_bg) orelse return lastError(); } if (self.overlay_brush == null) { - self.overlay_brush = CreateSolidBrush(theme.overlay_bg) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + self.overlay_brush = sys.CreateSolidBrush(theme.overlay_bg) orelse return lastError(); } if (self.edit_brush == null) { - self.edit_brush = CreateSolidBrush(theme.edit_bg) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + self.edit_brush = sys.CreateSolidBrush(theme.edit_bg) orelse return lastError(); } } fn rebuildThemeBrushes(self: *Host) void { - if (self.chrome_brush) |brush| _ = DeleteObject(brush); - if (self.overlay_brush) |brush| _ = DeleteObject(brush); - if (self.edit_brush) |brush| _ = DeleteObject(brush); + if (self.chrome_brush) |brush| _ = sys.DeleteObject(brush); + if (self.overlay_brush) |brush| _ = sys.DeleteObject(brush); + if (self.edit_brush) |brush| _ = sys.DeleteObject(brush); self.chrome_brush = null; self.overlay_brush = null; self.edit_brush = null; - self.ensureThemeBrushes() catch {}; + runUiActionOrLog("theme brush recreation failed", self.ensureThemeBrushes()); } fn createChromeFont(self: *Host) ?*anyopaque { var lf: LOGFONTW = .{}; lf.lfHeight = -self.scaled(14); - lf.lfWeight = FW_NORMAL; + lf.lfWeight = c.FW_NORMAL; // Try config font family first if (self.app.config.@"window-title-font-family") |family| { const name_w = std.unicode.utf8ToUtf16LeAllocZ(self.app.core_app.alloc, family) catch return null; defer self.app.core_app.alloc.free(name_w); - const copy_len = @min(name_w.len, LF_FACESIZE - 1); + const copy_len = @min(name_w.len, c.LF_FACESIZE - 1); @memcpy(lf.lfFaceName[0..copy_len], name_w[0..copy_len]); } else { // Fallback: system UI font via NONCLIENTMETRICS var ncm: NONCLIENTMETRICSW = undefined; ncm.cbSize = @sizeOf(NONCLIENTMETRICSW); - if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, @sizeOf(NONCLIENTMETRICSW), @ptrCast(&ncm), 0) != 0) { + if (sys.SystemParametersInfoW(c.SPI_GETNONCLIENTMETRICS, @sizeOf(NONCLIENTMETRICSW), @ptrCast(&ncm), 0) != 0) { lf = ncm.lfMessageFont; lf.lfHeight = -self.scaled(14); - lf.lfQuality = CLEARTYPE_QUALITY; + lf.lfQuality = c.CLEARTYPE_QUALITY; } } - return CreateFontIndirectW(&lf); + return sys.CreateFontIndirectW(&lf); } fn createTitlebarIconFont(self: *Host, logical_px: i32) ?*anyopaque { @@ -12465,30 +11204,30 @@ const Host = struct { ) ?*anyopaque { var lf: LOGFONTW = .{}; lf.lfHeight = -self.scaled(logical_px); - lf.lfWeight = FW_NORMAL; - lf.lfQuality = CLEARTYPE_QUALITY; + lf.lfWeight = c.FW_NORMAL; + lf.lfQuality = c.CLEARTYPE_QUALITY; const name = std.mem.span(face); - const copy_len = @min(name.len, LF_FACESIZE - 1); + const copy_len = @min(name.len, c.LF_FACESIZE - 1); @memcpy(lf.lfFaceName[0..copy_len], name[0..copy_len]); - return CreateFontIndirectW(&lf); + return sys.CreateFontIndirectW(&lf); } fn recreateTitlebarIconFonts(self: *Host) void { - if (self.titlebar_caption_icon_font) |old| _ = DeleteObject(old); - if (self.titlebar_action_icon_font) |old| _ = DeleteObject(old); + if (self.titlebar_caption_icon_font) |old| _ = sys.DeleteObject(old); + if (self.titlebar_action_icon_font) |old| _ = sys.DeleteObject(old); self.titlebar_caption_icon_font = self.createTitlebarIconFont(10); self.titlebar_action_icon_font = self.createTitlebarIconFont(12); } fn recreateChromeFont(self: *Host) void { - if (self.chrome_font) |old| _ = DeleteObject(old); + if (self.chrome_font) |old| _ = sys.DeleteObject(old); self.chrome_font = self.createChromeFont(); self.recreateTitlebarIconFonts(); // Send WM_SETFONT to child controls if (self.chrome_font) |font| { - if (self.overlay_edit_hwnd) |edit| _ = SendMessageW(edit, WM_SETFONT, @intFromPtr(font), 1); - if (self.overlay_label_hwnd) |label| _ = SendMessageW(label, WM_SETFONT, @intFromPtr(font), 1); - if (self.overlay_hint_hwnd) |hint| _ = SendMessageW(hint, WM_SETFONT, @intFromPtr(font), 1); + if (self.overlay_edit_hwnd) |edit| _ = sys.SendMessageW(edit, c.WM_SETFONT, @intFromPtr(font), 1); + if (self.overlay_label_hwnd) |label| _ = sys.SendMessageW(label, c.WM_SETFONT, @intFromPtr(font), 1); + if (self.overlay_hint_hwnd) |hint| _ = sys.SendMessageW(hint, c.WM_SETFONT, @intFromPtr(font), 1); for (self.app.windows.items) |surface| { if (surface.host != self) continue; surface.applySearchBarFont(font); @@ -12499,13 +11238,13 @@ const Host = struct { fn ensureSearchTimer(self: *Host) void { if (self.search_timer_active) return; const hwnd = self.hwnd orelse return; - _ = SetTimer(hwnd, SEARCH_TIMER_ID, SEARCH_TIMER_INTERVAL_MS, null); + _ = sys.SetTimer(hwnd, c.SEARCH_TIMER_ID, c.SEARCH_TIMER_INTERVAL_MS, null); self.search_timer_active = true; } fn killSearchTimer(self: *Host) void { if (!self.search_timer_active) return; - if (self.hwnd) |hwnd| _ = KillTimer(hwnd, SEARCH_TIMER_ID); + if (self.hwnd) |hwnd| _ = sys.KillTimer(hwnd, c.SEARCH_TIMER_ID); self.search_timer_active = false; } @@ -12530,7 +11269,7 @@ const Host = struct { } fn tickSearchBars(self: *Host) void { - const now_ms: i64 = @intCast(GetTickCount64()); + const now_ms: i64 = @intCast(sys.GetTickCount64()); if (self.activeTab()) |tab| { var it = tab.tree.iterator(); while (it.next()) |entry| { @@ -12548,13 +11287,13 @@ const Host = struct { fn ensureScrollbarTimer(self: *Host) void { if (self.scrollbar_timer_active) return; const hwnd = self.hwnd orelse return; - _ = SetTimer(hwnd, SCROLLBAR_TIMER_ID, SCROLLBAR_TIMER_INTERVAL_MS, null); + _ = sys.SetTimer(hwnd, c.SCROLLBAR_TIMER_ID, c.SCROLLBAR_TIMER_INTERVAL_MS, null); self.scrollbar_timer_active = true; } fn killScrollbarTimer(self: *Host) void { if (!self.scrollbar_timer_active) return; - if (self.hwnd) |hwnd| _ = KillTimer(hwnd, SCROLLBAR_TIMER_ID); + if (self.hwnd) |hwnd| _ = sys.KillTimer(hwnd, c.SCROLLBAR_TIMER_ID); self.scrollbar_timer_active = false; } @@ -12577,7 +11316,7 @@ const Host = struct { } fn tickScrollbars(self: *Host) void { - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); if (self.activeTab()) |tab| { var it = tab.tree.iterator(); while (it.next()) |entry| { @@ -12589,8 +11328,8 @@ const Host = struct { fn showContextMenu(self: *Host, screen_x: i32, screen_y: i32) void { const hwnd = self.hwnd orelse return; - const menu = CreatePopupMenu() orelse return; - defer _ = DestroyMenu(menu); + const menu = sys.CreatePopupMenu() orelse return; + defer _ = sys.DestroyMenu(menu); // Check if active surface has a selection const has_selection = if (self.activeSurface()) |s| blk: { @@ -12598,29 +11337,29 @@ const Host = struct { break :blk s.core_surface.hasSelection(); } else false; - _ = AppendMenuW(menu, if (has_selection) MF_STRING else MF_GRAYED, CTX_COPY, std.unicode.utf8ToUtf16LeStringLiteral("Copy\tCtrl+Shift+C")); - _ = AppendMenuW(menu, MF_STRING, CTX_PASTE, std.unicode.utf8ToUtf16LeStringLiteral("Paste\tCtrl+Shift+V")); - _ = AppendMenuW(menu, MF_STRING, CTX_SELECT_ALL, std.unicode.utf8ToUtf16LeStringLiteral("Select All")); - _ = AppendMenuW(menu, MF_SEPARATOR, 0, null); - _ = AppendMenuW(menu, MF_STRING, CTX_FIND, std.unicode.utf8ToUtf16LeStringLiteral("Find...\tCtrl+Shift+F")); - _ = AppendMenuW(menu, MF_STRING, CTX_COMMAND_PALETTE, std.unicode.utf8ToUtf16LeStringLiteral("Command Palette\tCtrl+Shift+P")); - _ = AppendMenuW(menu, MF_SEPARATOR, 0, null); - _ = AppendMenuW(menu, MF_STRING, CTX_NEW_TAB, std.unicode.utf8ToUtf16LeStringLiteral("New Tab")); + _ = sys.AppendMenuW(menu, if (has_selection) c.MF_STRING else c.MF_GRAYED, c.CTX_COPY, std.unicode.utf8ToUtf16LeStringLiteral("Copy\tCtrl+Shift+C")); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_PASTE, std.unicode.utf8ToUtf16LeStringLiteral("Paste\tCtrl+Shift+V")); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_SELECT_ALL, std.unicode.utf8ToUtf16LeStringLiteral("Select All")); + _ = sys.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_FIND, std.unicode.utf8ToUtf16LeStringLiteral("Find...\tCtrl+Shift+F")); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_COMMAND_PALETTE, std.unicode.utf8ToUtf16LeStringLiteral("Command Palette\tCtrl+Shift+P")); + _ = sys.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_NEW_TAB, std.unicode.utf8ToUtf16LeStringLiteral("New Tab")); // Split submenu with all four directions - const split_menu = CreatePopupMenu() orelse return; - _ = AppendMenuW(split_menu, MF_STRING, CTX_SPLIT_RIGHT, std.unicode.utf8ToUtf16LeStringLiteral("Split Right")); - _ = AppendMenuW(split_menu, MF_STRING, CTX_SPLIT_DOWN, std.unicode.utf8ToUtf16LeStringLiteral("Split Down")); - _ = AppendMenuW(split_menu, MF_STRING, CTX_SPLIT_LEFT, std.unicode.utf8ToUtf16LeStringLiteral("Split Left")); - _ = AppendMenuW(split_menu, MF_STRING, CTX_SPLIT_UP, std.unicode.utf8ToUtf16LeStringLiteral("Split Up")); - _ = AppendMenuW(menu, MF_POPUP, @intFromPtr(split_menu), std.unicode.utf8ToUtf16LeStringLiteral("Split")); + const split_menu = sys.CreatePopupMenu() orelse return; + _ = sys.AppendMenuW(split_menu, c.MF_STRING, c.CTX_SPLIT_RIGHT, std.unicode.utf8ToUtf16LeStringLiteral("Split Right")); + _ = sys.AppendMenuW(split_menu, c.MF_STRING, c.CTX_SPLIT_DOWN, std.unicode.utf8ToUtf16LeStringLiteral("Split Down")); + _ = sys.AppendMenuW(split_menu, c.MF_STRING, c.CTX_SPLIT_LEFT, std.unicode.utf8ToUtf16LeStringLiteral("Split Left")); + _ = sys.AppendMenuW(split_menu, c.MF_STRING, c.CTX_SPLIT_UP, std.unicode.utf8ToUtf16LeStringLiteral("Split Up")); + _ = sys.AppendMenuW(menu, c.MF_POPUP, @intFromPtr(split_menu), std.unicode.utf8ToUtf16LeStringLiteral("Split")); - _ = AppendMenuW(menu, MF_STRING, CTX_NEW_WINDOW, std.unicode.utf8ToUtf16LeStringLiteral("New Window")); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_NEW_WINDOW, std.unicode.utf8ToUtf16LeStringLiteral("New Window")); // Menu must be owned by top-level host HWND to avoid dismiss bugs - _ = SetForegroundWindow(hwnd); - const cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_LEFTALIGN | TPM_TOPALIGN, screen_x, screen_y, 0, hwnd, null); - _ = PostMessageW(hwnd, WM_NULL, 0, 0); + _ = sys.SetForegroundWindow(hwnd); + const cmd = sys.TrackPopupMenu(menu, c.TPM_RETURNCMD | c.TPM_RIGHTBUTTON | c.TPM_LEFTALIGN | c.TPM_TOPALIGN, screen_x, screen_y, 0, hwnd, null); + _ = sys.PostMessageW(hwnd, c.WM_NULL, 0, 0); // Dispatch command (cmd is 0 on cancel) self.handleContextMenuCommand(cmd); @@ -12632,37 +11371,37 @@ const Host = struct { if (!surface.core_initialized) return; switch (@as(usize, @intCast(cmd))) { - CTX_COPY => { + c.CTX_COPY => { runUiActionOrLog("context menu copy failed", surface.core_surface.performBindingAction(.{ .copy_to_clipboard = .mixed })); }, - CTX_PASTE => { + c.CTX_PASTE => { runUiActionOrLog("context menu paste failed", surface.core_surface.performBindingAction(.{ .paste_from_clipboard = {} })); }, - CTX_SELECT_ALL => { + c.CTX_SELECT_ALL => { runUiActionOrLog("context menu select all failed", surface.core_surface.performBindingAction(.{ .select_all = {} })); }, - CTX_FIND => { + c.CTX_FIND => { runUiActionOrLog("context menu find failed", surface.showSearchOverlay("")); }, - CTX_COMMAND_PALETTE => { + c.CTX_COMMAND_PALETTE => { runUiActionOrLog("context menu command palette failed", surface.toggleCommandPalette()); }, - CTX_NEW_TAB => { + c.CTX_NEW_TAB => { self.postDeferredNewTab(); }, - CTX_SPLIT_RIGHT => { + c.CTX_SPLIT_RIGHT => { runUiActionOrLog("context menu split right failed", self.app.performAction(.{ .surface = surface.core() }, .new_split, .right)); }, - CTX_SPLIT_DOWN => { + c.CTX_SPLIT_DOWN => { runUiActionOrLog("context menu split down failed", self.app.performAction(.{ .surface = surface.core() }, .new_split, .down)); }, - CTX_SPLIT_LEFT => { + c.CTX_SPLIT_LEFT => { runUiActionOrLog("context menu split left failed", self.app.performAction(.{ .surface = surface.core() }, .new_split, .left)); }, - CTX_SPLIT_UP => { + c.CTX_SPLIT_UP => { runUiActionOrLog("context menu split up failed", self.app.performAction(.{ .surface = surface.core() }, .new_split, .up)); }, - CTX_NEW_WINDOW => { + c.CTX_NEW_WINDOW => { runUiActionOrLog("context menu new window failed", self.app.performAction(.{ .surface = surface.core() }, .new_window, .{})); }, else => {}, // 0 = cancel, ignore @@ -12671,50 +11410,50 @@ const Host = struct { fn showTabContextMenu(self: *Host, button_hwnd: HWND, tab_index: usize) void { const hwnd = self.hwnd orelse return; - const menu = CreatePopupMenu() orelse return; - defer _ = DestroyMenu(menu); + const menu = sys.CreatePopupMenu() orelse return; + defer _ = sys.DestroyMenu(menu); - _ = AppendMenuW(menu, MF_STRING, CTX_TAB_RENAME, std.unicode.utf8ToUtf16LeStringLiteral("Rename Tab")); - _ = AppendMenuW(menu, MF_SEPARATOR, 0, null); - _ = AppendMenuW(menu, MF_STRING, CTX_TAB_MOVE_LEFT, std.unicode.utf8ToUtf16LeStringLiteral("Move Tab Left")); - _ = AppendMenuW(menu, MF_STRING, CTX_TAB_MOVE_RIGHT, std.unicode.utf8ToUtf16LeStringLiteral("Move Tab Right")); - _ = AppendMenuW(menu, MF_SEPARATOR, 0, null); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_TAB_RENAME, std.unicode.utf8ToUtf16LeStringLiteral("Rename Tab")); + _ = sys.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_TAB_MOVE_LEFT, std.unicode.utf8ToUtf16LeStringLiteral("Move Tab Left")); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_TAB_MOVE_RIGHT, std.unicode.utf8ToUtf16LeStringLiteral("Move Tab Right")); + _ = sys.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); // Only allow closing if there's more than one tab - const close_flag: UINT = if (self.tabs.items.len > 1) MF_STRING else MF_GRAYED; - _ = AppendMenuW(menu, close_flag, CTX_TAB_CLOSE, std.unicode.utf8ToUtf16LeStringLiteral("Close Tab")); - _ = AppendMenuW(menu, if (self.tabs.items.len > 1) MF_STRING else MF_GRAYED, CTX_TAB_CLOSE_OTHERS, std.unicode.utf8ToUtf16LeStringLiteral("Close Other Tabs")); + const close_flag: UINT = if (self.tabs.items.len > 1) c.MF_STRING else c.MF_GRAYED; + _ = sys.AppendMenuW(menu, close_flag, c.CTX_TAB_CLOSE, std.unicode.utf8ToUtf16LeStringLiteral("Close Tab")); + _ = sys.AppendMenuW(menu, if (self.tabs.items.len > 1) c.MF_STRING else c.MF_GRAYED, c.CTX_TAB_CLOSE_OTHERS, std.unicode.utf8ToUtf16LeStringLiteral("Close Other Tabs")); // Position below the tab button var rect: RECT = undefined; - if (GetWindowRect(button_hwnd, &rect) == 0) return; + if (sys.GetWindowRect(button_hwnd, &rect) == 0) return; - _ = SetForegroundWindow(hwnd); - const cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_LEFTALIGN | TPM_TOPALIGN, rect.left, rect.bottom, 0, hwnd, null); - _ = PostMessageW(hwnd, WM_NULL, 0, 0); + _ = sys.SetForegroundWindow(hwnd); + const cmd = sys.TrackPopupMenu(menu, c.TPM_RETURNCMD | c.TPM_RIGHTBUTTON | c.TPM_LEFTALIGN | c.TPM_TOPALIGN, rect.left, rect.bottom, 0, hwnd, null); + _ = sys.PostMessageW(hwnd, c.WM_NULL, 0, 0); if (cmd <= 0) return; const surface = self.activeSurface() orelse return; if (!surface.core_initialized) return; switch (@as(usize, @intCast(cmd))) { - CTX_TAB_RENAME => { + c.CTX_TAB_RENAME => { runUiActionOrLog("tab context rename failed", surface.promptTitle(.tab)); }, - CTX_TAB_CLOSE => { + c.CTX_TAB_CLOSE => { _ = self.app.closeTab(.{ .surface = surface.core() }, .this) catch |err| { log.warn("close_tab action failed err={}", .{err}); }; }, - CTX_TAB_CLOSE_OTHERS => { + c.CTX_TAB_CLOSE_OTHERS => { _ = self.app.closeTab(.{ .surface = surface.core() }, .other) catch |err| { log.warn("close_tab other action failed err={}", .{err}); }; }, - CTX_TAB_MOVE_LEFT => { + c.CTX_TAB_MOVE_LEFT => { runUiActionOrLog("tab context move left failed", self.app.moveTab(.{ .surface = surface.core() }, .{ .amount = -1 })); }, - CTX_TAB_MOVE_RIGHT => { + c.CTX_TAB_MOVE_RIGHT => { runUiActionOrLog("tab context move right failed", self.app.moveTab(.{ .surface = surface.core() }, .{ .amount = 1 })); }, else => {}, @@ -12728,14 +11467,14 @@ const Host = struct { const button = self.overflow_hwnd orelse return; // Ensure profiles are loaded before building the menu - _ = self.ensureProfiles() catch {}; + runUiActionOrLog("overflow profile load failed", self.ensureProfiles()); // Position menu below the dropdown chevron button var rect: RECT = undefined; - if (GetWindowRect(button, &rect) == 0) return; + if (sys.GetWindowRect(button, &rect) == 0) return; - const menu = CreatePopupMenu() orelse return; - defer _ = DestroyMenu(menu); + const menu = sys.CreatePopupMenu() orelse return; + defer _ = sys.DestroyMenu(menu); // Profile list — each profile gets a numbered shortcut hint const selected_idx = self.selectedProfileIndex(); @@ -12746,24 +11485,24 @@ const Host = struct { const label_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, label_utf8) catch continue; defer alloc.free(label_w); const is_selected = selected_idx != null and selected_idx.? == i; - const flags: UINT = if (is_selected) MF_STRING | MF_CHECKED else MF_STRING; - _ = AppendMenuW(menu, flags, CTX_PROFILE_BASE + i, label_w.ptr); + const flags: UINT = if (is_selected) c.MF_STRING | c.MF_CHECKED else c.MF_STRING; + _ = sys.AppendMenuW(menu, flags, c.CTX_PROFILE_BASE + i, label_w.ptr); } - if (profiles.len > 0) _ = AppendMenuW(menu, MF_SEPARATOR, 0, null); + if (profiles.len > 0) _ = sys.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); } // Utility items - _ = AppendMenuW(menu, MF_STRING, CTX_NEW_TAB, std.unicode.utf8ToUtf16LeStringLiteral("Open a new tab")); - _ = AppendMenuW(menu, MF_STRING, CTX_NEW_WINDOW, std.unicode.utf8ToUtf16LeStringLiteral("New Window")); - _ = AppendMenuW(menu, MF_SEPARATOR, 0, null); - _ = AppendMenuW(menu, MF_STRING, CTX_COMMAND_PALETTE, std.unicode.utf8ToUtf16LeStringLiteral("Command Palette\tCtrl+Shift+P")); - _ = AppendMenuW(menu, MF_STRING, CTX_FIND, std.unicode.utf8ToUtf16LeStringLiteral("Find...\tCtrl+Shift+F")); - _ = AppendMenuW(menu, MF_SEPARATOR, 0, null); - _ = AppendMenuW(menu, MF_STRING, CTX_INSPECTOR, std.unicode.utf8ToUtf16LeStringLiteral("Toggle Inspector")); - - _ = SetForegroundWindow(hwnd); - const cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_LEFTALIGN | TPM_TOPALIGN, rect.left, rect.bottom, 0, hwnd, null); - _ = PostMessageW(hwnd, WM_NULL, 0, 0); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_NEW_TAB, std.unicode.utf8ToUtf16LeStringLiteral("Open a new tab")); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_NEW_WINDOW, std.unicode.utf8ToUtf16LeStringLiteral("New Window")); + _ = sys.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_COMMAND_PALETTE, std.unicode.utf8ToUtf16LeStringLiteral("Command Palette\tCtrl+Shift+P")); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_FIND, std.unicode.utf8ToUtf16LeStringLiteral("Find...\tCtrl+Shift+F")); + _ = sys.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); + _ = sys.AppendMenuW(menu, c.MF_STRING, c.CTX_INSPECTOR, std.unicode.utf8ToUtf16LeStringLiteral("Toggle Inspector")); + + _ = sys.SetForegroundWindow(hwnd); + const cmd = sys.TrackPopupMenu(menu, c.TPM_RETURNCMD | c.TPM_RIGHTBUTTON | c.TPM_LEFTALIGN | c.TPM_TOPALIGN, rect.left, rect.bottom, 0, hwnd, null); + _ = sys.PostMessageW(hwnd, c.WM_NULL, 0, 0); // Guard: host may have been destroyed during the modal menu loop if (self.hwnd == null) return; @@ -12775,8 +11514,8 @@ const Host = struct { const cmd_id = @as(usize, @intCast(cmd)); // Profile item dispatch — open the selected profile in a new tab - if (cmd_id >= CTX_PROFILE_BASE and cmd_id < CTX_PROFILE_BASE + 100) { - const profile_index = cmd_id - CTX_PROFILE_BASE; + if (cmd_id >= c.CTX_PROFILE_BASE and cmd_id < c.CTX_PROFILE_BASE + 100) { + const profile_index = cmd_id - c.CTX_PROFILE_BASE; // Validate index against current profiles (may have changed during modal menu) if (self.profiles) |profiles| { if (profile_index < profiles.len) { @@ -12790,19 +11529,19 @@ const Host = struct { if (!surface.core_initialized) return; switch (cmd_id) { - CTX_COMMAND_PALETTE => { + c.CTX_COMMAND_PALETTE => { runUiActionOrLog("overflow command palette failed", surface.toggleCommandPalette()); }, - CTX_FIND => { + c.CTX_FIND => { runUiActionOrLog("overflow find failed", surface.showSearchOverlay("")); }, - CTX_NEW_TAB => { + c.CTX_NEW_TAB => { self.postDeferredNewTab(); }, - CTX_NEW_WINDOW => { + c.CTX_NEW_WINDOW => { runUiActionOrLog("overflow new window failed", self.app.performAction(.{ .surface = surface.core() }, .new_window, .{})); }, - CTX_INSPECTOR => { + c.CTX_INSPECTOR => { runUiActionOrLog("overflow inspector toggle failed", self.app.toggleInspectorForSurface(surface)); }, else => {}, @@ -12827,17 +11566,17 @@ const Host = struct { self.overlay_mode, current_slot, reverse, - self.overlay_edit_hwnd != null and IsWindowVisible(self.overlay_edit_hwnd.?) != 0, - self.overlay_accept_hwnd != null and IsWindowVisible(self.overlay_accept_hwnd.?) != 0, - self.overlay_cancel_hwnd != null and IsWindowVisible(self.overlay_cancel_hwnd.?) != 0, + self.overlay_edit_hwnd != null and sys.IsWindowVisible(self.overlay_edit_hwnd.?) != 0, + self.overlay_accept_hwnd != null and sys.IsWindowVisible(self.overlay_accept_hwnd.?) != 0, + self.overlay_cancel_hwnd != null and sys.IsWindowVisible(self.overlay_cancel_hwnd.?) != 0, ) orelse return false; const target = switch (target_slot) { .edit => self.overlay_edit_hwnd, .accept => self.overlay_accept_hwnd, .cancel => self.overlay_cancel_hwnd, } orelse return false; - _ = SetFocus(target); - return GetFocus() == target; + _ = sys.SetFocus(target); + return sys.GetFocus() == target; } fn searchControlSurface(self: *const Host, child: HWND) ?*Surface { @@ -12950,7 +11689,7 @@ const Host = struct { } fn drawSearchBarBackground(self: *Host, draw: *const DRAWITEMSTRUCT) bool { - if (draw.CtlType != ODT_STATIC) return false; + if (draw.CtlType != c.ODT_STATIC) return false; const surface = self.searchBarBackgroundSurface(draw.hwndItem) orelse return false; const theme = &self.app.resolved_theme; @@ -13035,7 +11774,7 @@ const Host = struct { if (surface.search_bar_edit_placement.rect_known) { const edit_rect = rectOffset(surface.search_bar_edit_placement.rect, -origin_x, -origin_y); const edit_focused = if (surface.search_bar_edit_hwnd) |edit_hwnd| - GetFocus() == edit_hwnd + sys.GetFocus() == edit_hwnd else false; const edit_frame_rect = RECT{ @@ -13095,12 +11834,12 @@ const Host = struct { } const content_rect = rectInset(bg_rect, self.scaled(7), self.scaled(5)); - _ = SetBkMode(draw.hDC, TRANSPARENT); - _ = SetTextColor(draw.hDC, colors.fg); + _ = sys.SetBkMode(draw.hDC, c.TRANSPARENT); + _ = sys.SetTextColor(draw.hDC, colors.fg); if (show_label) { var text_buf: [160]u16 = undefined; - const text_len = GetWindowTextW(draw.hwndItem, &text_buf, text_buf.len); + const text_len = sys.GetWindowTextW(draw.hwndItem, &text_buf, text_buf.len); const icon_box_w = @min(content_rect.bottom - content_rect.top, self.scaled(18)); const icon_rect = RECT{ .left = content_rect.left, @@ -13117,12 +11856,12 @@ const Host = struct { var label_rect = content_rect; label_rect.left = icon_rect.right + self.scaled(5); - _ = DrawTextW( + _ = sys.DrawTextW( draw.hDC, @ptrCast(&text_buf), text_len, &label_rect, - DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS, + c.DT_LEFT | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX | c.DT_END_ELLIPSIS, ); return; } @@ -13173,7 +11912,7 @@ const Host = struct { theme, role, parent_bg, - self.titlebarHoverAlpha(role, hovered, GetTickCount64()), + self.titlebarHoverAlpha(role, hovered, sys.GetTickCount64()), pressed, is_hc, ); @@ -13203,13 +11942,13 @@ const Host = struct { } fn drawButton(self: *Host, draw: *const DRAWITEMSTRUCT) void { - if (draw.CtlType != ODT_BUTTON) return; + if (draw.CtlType != c.ODT_BUTTON) return; self.ensureThemeBrushes() catch return; - if (self.chrome_font) |font| _ = SelectObject(draw.hDC, font); + if (self.chrome_font) |font| _ = sys.SelectObject(draw.hDC, font); - const disabled = (draw.itemState & ODS_DISABLED) != 0; - const pressed = (draw.itemState & ODS_SELECTED) != 0; - const focused = (draw.itemState & ODS_FOCUS) != 0 or GetFocus() == draw.hwndItem; + const disabled = (draw.itemState & c.ODS_DISABLED) != 0; + const pressed = (draw.itemState & c.ODS_SELECTED) != 0; + const focused = (draw.itemState & c.ODS_FOCUS) != 0 or sys.GetFocus() == draw.hwndItem; const active = self.isActiveChromeButton(draw.hwndItem); const overlay = self.isOverlayStyledButton(draw.hwndItem); const hovered = self.isHoveredButton(draw.hwndItem); @@ -13364,9 +12103,9 @@ const Host = struct { const close_zone_width = if (can_close_tab) self.scaled(host_tab_close_zone_width) else 0; var text_buf: [160]u16 = undefined; - const text_len = GetWindowTextW(draw.hwndItem, &text_buf, text_buf.len); - _ = SetBkMode(draw.hDC, TRANSPARENT); - _ = SetTextColor(draw.hDC, fg); + const text_len = sys.GetWindowTextW(draw.hwndItem, &text_buf, text_buf.len); + _ = sys.SetBkMode(draw.hDC, c.TRANSPARENT); + _ = sys.SetTextColor(draw.hDC, fg); var text_rect = draw.rcItem; if (!overlay) { text_rect.left += self.scaled(6); @@ -13391,12 +12130,12 @@ const Host = struct { isHighContrastActive(), ); } else { - _ = DrawTextW( + _ = sys.DrawTextW( draw.hDC, @ptrCast(&text_buf), text_len, &text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX | c.DT_END_ELLIPSIS, ); } @@ -13407,7 +12146,7 @@ const Host = struct { // hover treatment. On non-hovered + non-active tabs the alpha // drops to 0 so the X vanishes entirely. if (can_close_tab) { - const now = GetTickCount64(); + const now = sys.GetTickCount64(); // Two animation slots: the incoming hover target // (`tab_close_hover_*` fades 0 → 1) and the departing tab // on adjacent transitions (`tab_close_prev_*` fades @@ -13434,19 +12173,19 @@ const Host = struct { // GDI's DrawTextW has no alpha channel, so we // pre-composite. const blended = blendColorRGB(bg, close_fg_full, alpha); - _ = SetTextColor(draw.hDC, blended); + _ = sys.SetTextColor(draw.hDC, blended); var close_rect = RECT{ .left = draw.rcItem.right - close_zone_width, .top = draw.rcItem.top, .right = draw.rcItem.right - self.scaled(2), .bottom = draw.rcItem.bottom, }; - _ = DrawTextW( + _ = sys.DrawTextW( draw.hDC, close_glyph, 1, &close_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX, ); } } @@ -13530,7 +12269,7 @@ const Host = struct { // `candidate` borrows from the current rich catalog. Preserve the // completion state before `syncCommandPaletteBanner` rebuilds it. _ = try self.setOverlayEditText(candidate.text); - _ = SendMessageW(edit_hwnd, EM_SETSEL, 0, -1); + _ = sys.SendMessageW(edit_hwnd, c.EM_SETSEL, 0, -1); if (!ownedStringEquals(self.overlay_completion_seed, seed_owned)) { try appendOwnedString(alloc, &self.overlay_completion_seed, seed_owned); } @@ -13598,7 +12337,7 @@ const Host = struct { const next_text = try std.fmt.allocPrint(self.app.core_app.alloc, "{d}", .{next}); defer self.app.core_app.alloc.free(next_text); _ = try self.setOverlayEditText(next_text); - _ = SendMessageW(edit_hwnd, EM_SETSEL, 0, -1); + _ = sys.SendMessageW(edit_hwnd, c.EM_SETSEL, 0, -1); _ = try self.syncOverlayLabel(); _ = try self.syncOverlayHint(); _ = try self.syncOverlayButtons(); @@ -13672,7 +12411,7 @@ const Host = struct { } const action = input.Binding.Action.parse(text) catch |err| { log.warn("win32 command palette invalid action action={s} err={}", .{ text, err }); - try self.setBanner(.err, "Unknown Ghostty action. Example: new_tab or toggle_fullscreen"); + try self.setBanner(.err, labels.command_palette_unknown_action); if (self.palette_list_uia_provider) |provider| { win32_uia.events.raiseNotification(&provider.base, .action_aborted, "Unknown command"); } @@ -13682,7 +12421,7 @@ const Host = struct { std.log.warn("palette MRU push failed err={}", .{err}); }; self.hideOverlay(); - self.layout() catch {}; + runUiActionOrLog("palette action layout failed", self.layout()); if (action == .new_tab) { self.postDeferredNewTab(); return true; @@ -13808,20 +12547,9 @@ const Host = struct { return win32_chrome_state.scaled(base, dpi); } - fn hasVisibleStatus(self: *const Host) bool { - // Status bar shows: profile chips, status text, banner info - if (self.cached_status_w != null) return true; - if (self.cached_detail_w != null) return true; - if (self.banner_text != null) return true; - if (self.profiles != null) return true; - if (self.overlay_mode != .none) return true; - return false; - } - - fn statusBarHeight(self: *Host) i32 { + fn statusBarHeight() i32 { // Status bar is disabled; banners and overlays carry transient // status feedback without reserving terminal rows. - _ = self; return 0; } @@ -13863,8 +12591,8 @@ const Host = struct { fn contentRect(self: *Host) !RECT { const hwnd = self.hwnd orelse return error.InvalidHost; var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetClientRect(hwnd, &rect) == 0) { + return lastError(); } const tab_offset: i32 = self.tabBarHeight(); const overlay_offset: i32 = if (self.overlay_mode == .none) 0 else self.scaled(host_overlay_height); @@ -13873,7 +12601,7 @@ const Host = struct { .left = 0, .top = tab_offset + overlay_offset + inspector_offset, .right = rect.right, - .bottom = @max(tab_offset + 1, rect.bottom - self.statusBarHeight()), + .bottom = @max(tab_offset + 1, rect.bottom - statusBarHeight()), }; } @@ -13897,7 +12625,7 @@ const Host = struct { if (surfaces.items.len == 0) { self.clearStructuralHistory(.normal); - if (fallback_hwnd) |hwnd| _ = DestroyWindow(hwnd); + if (fallback_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); self.app.removeHost(self); return; } @@ -14054,7 +12782,7 @@ const Host = struct { _ = try self.syncOverlayButtons(); } if (invalidate) { - if (chromeTextNeedsFullInvalidation(self.statusBarHeight())) { + if (chromeTextNeedsFullInvalidation(statusBarHeight())) { self.invalidateChromeText(); } else { self.invalidateTopChromeText(); @@ -14079,7 +12807,7 @@ const Host = struct { fn invalidateChromeMask(self: *Host, hwnd: HWND, mask: ChromeRepaintMask) void { if (mask.isEmpty()) return; var client_rect: RECT = undefined; - if (GetClientRect(hwnd, &client_rect) == 0) return; + if (sys.GetClientRect(hwnd, &client_rect) == 0) return; const content_rect = self.contentRect() catch return; if (mask.top) { @@ -14090,20 +12818,20 @@ const Host = struct { .bottom = @max(client_rect.top, content_rect.top), }; if (top_rect.bottom > top_rect.top) { - _ = InvalidateRect(hwnd, &top_rect, 0); + _ = sys.InvalidateRect(hwnd, &top_rect, 0); } } if (mask.content) { if (content_rect.bottom > content_rect.top) { - _ = InvalidateRect(hwnd, &content_rect, 0); + _ = sys.InvalidateRect(hwnd, &content_rect, 0); } } else if (mask.search_frames) { if (self.activeTab()) |tab| { var it = tab.tree.iterator(); while (it.next()) |entry| { if (!entry.view.search_bar_frame_visible) continue; - _ = InvalidateRect(hwnd, &entry.view.search_bar_frame_rect, 0); + _ = sys.InvalidateRect(hwnd, &entry.view.search_bar_frame_rect, 0); } } } @@ -14116,7 +12844,7 @@ const Host = struct { .bottom = client_rect.bottom, }; if (bottom_rect.bottom > bottom_rect.top) { - _ = InvalidateRect(hwnd, &bottom_rect, 0); + _ = sys.InvalidateRect(hwnd, &bottom_rect, 0); } } } @@ -14139,13 +12867,13 @@ const Host = struct { } fn repaintStatusBar(self: *Host) void { - const status_h = self.statusBarHeight(); + const status_h = statusBarHeight(); if (status_h <= 0) return; self.queueChromeRepaint(.{ .status = true }); } fn invalidateStatusBarText(self: *Host) void { - if (self.statusBarHeight() <= 0) return; + if (statusBarHeight() <= 0) return; self.chrome_text_dirty.status = true; self.repaintStatusBar(); } @@ -14214,11 +12942,11 @@ const Host = struct { self.palette_list_hwnd, }; for (controls) |maybe_hwnd| { - if (maybe_hwnd) |h| _ = InvalidateRect(h, null, 0); + if (maybe_hwnd) |h| _ = sys.InvalidateRect(h, null, 0); } for (self.tabs.items) |*t| { if (t.button_hwnd) |h| { - _ = InvalidateRect(h, null, 0); + _ = sys.InvalidateRect(h, null, 0); } } } @@ -14236,12 +12964,12 @@ const Host = struct { }; for (controls) |maybe_hwnd| { if (maybe_hwnd) |h| { - if (IsWindowVisible(h) != 0) _ = UpdateWindow(h); + if (sys.IsWindowVisible(h) != 0) _ = sys.UpdateWindow(h); } } for (self.tabs.items) |*t| { if (t.button_hwnd) |h| { - if (IsWindowVisible(h) != 0) _ = UpdateWindow(h); + if (sys.IsWindowVisible(h) != 0) _ = sys.UpdateWindow(h); } } } @@ -14249,8 +12977,8 @@ const Host = struct { fn redrawHostWindowNow(self: *Host) void { const hwnd = self.hwnd orelse return; const flags = hostCompositionRedrawFlags(); - if (RedrawWindow(hwnd, null, null, flags) == 0) { - _ = UpdateWindow(hwnd); + if (sys.RedrawWindow(hwnd, null, null, flags) == 0) { + _ = sys.UpdateWindow(hwnd); } } @@ -14352,7 +13080,7 @@ const Host = struct { const surface = self.activeSurface() orelse { if (!windowTitleSyncChanged(self.cached_window_title, "noctty")) return false; try appendOwnedString(alloc, &self.cached_window_title, "noctty"); - _ = SetWindowTextW(hwnd, default_title); + _ = sys.SetWindowTextW(hwnd, default_title); return true; }; const host_base_title = try buildHostAwareBaseTitle( @@ -14365,7 +13093,7 @@ const Host = struct { try appendOwnedString(alloc, &self.cached_window_title, host_base_title); const title_w = try std.unicode.utf8ToUtf16LeAllocZ(alloc, host_base_title); defer alloc.free(title_w); - _ = SetWindowTextW(hwnd, title_w.ptr); + _ = sys.SetWindowTextW(hwnd, title_w.ptr); return true; } @@ -14373,8 +13101,8 @@ const Host = struct { const hwnd = self.hwnd orelse return false; try self.ensureChromeButtons(); var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetClientRect(hwnd, &rect) == 0) { + return lastError(); } const width = @max(0, rect.right - rect.left); const right_buttons_width = self.rightButtonsWidth(); @@ -14417,11 +13145,11 @@ const Host = struct { try appendOwnedString(self.app.core_app.alloc, &tab.cached_button_label, label); const label_w = try std.unicode.utf8ToUtf16LeAllocZ(self.app.core_app.alloc, label); defer self.app.core_app.alloc.free(label_w); - tab.button_hwnd = CreateWindowExW( + tab.button_hwnd = sys.CreateWindowExW( 0, prompt_button_class, label_w.ptr, - WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_OWNERDRAW, + c.WS_CHILD | c.WS_VISIBLE | c.WS_TABSTOP | c.BS_OWNERDRAW, 0, 0, 100, @@ -14430,13 +13158,13 @@ const Host = struct { @ptrFromInt(1000 + i), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); self.subclassButton(tab.button_hwnd.?, &tabButtonProc, &tab.button_prev_proc); } else if (!ownedStringEquals(tab.cached_button_label, label)) { try appendOwnedString(self.app.core_app.alloc, &tab.cached_button_label, label); const label_w = try std.unicode.utf8ToUtf16LeAllocZ(self.app.core_app.alloc, label); defer self.app.core_app.alloc.free(label_w); - _ = SetWindowTextW(tab.button_hwnd.?, label_w.ptr); + _ = sys.SetWindowTextW(tab.button_hwnd.?, label_w.ptr); } try appendOwnedString(self.app.core_app.alloc, &tab.cached_button_title, title); tab.cached_button_index = i; @@ -14701,8 +13429,8 @@ const Host = struct { fn layout(self: *Host) !void { const hwnd = self.hwnd orelse return; var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetClientRect(hwnd, &rect) == 0) { + return lastError(); } var chrome_layout_changed = false; if (!self.layoutChromeForRect(rect, &chrome_layout_changed)) return; @@ -14870,16 +13598,16 @@ const Host = struct { var glyph_rect = centeredRect(rect, glyph_size, glyph_size); if (font) |font_handle| { - if (SelectObject(hdc, font_handle)) |old_font| { - defer _ = SelectObject(hdc, old_font); - _ = SetBkMode(hdc, TRANSPARENT); - _ = SetTextColor(hdc, color); - if (DrawTextW( + if (sys.SelectObject(hdc, font_handle)) |old_font| { + defer _ = sys.SelectObject(hdc, old_font); + _ = sys.SetBkMode(hdc, c.TRANSPARENT); + _ = sys.SetTextColor(hdc, color); + if (sys.DrawTextW( hdc, titlebarGlyphText(kind), 1, &glyph_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX, ) != 0) { return; } @@ -14938,7 +13666,7 @@ const Host = struct { const cb_h = self.scaled(host_caption_button_h); if (cb_w <= 0 or cb_h <= 0) return; - const maximized = if (self.hwnd) |h| IsZoomed(h) != 0 else false; + const maximized = if (self.hwnd) |h| sys.IsZoomed(h) != 0 else false; const right = client_rect.right; const close_rect: RECT = .{ @@ -14961,7 +13689,7 @@ const Host = struct { }; const is_hc = isHighContrastActive(); - const now = GetTickCount64(); + const now = sys.GetTickCount64(); self.paintCaptionButton(hdc, min_rect, .minimize, .minimize, now, is_hc, theme); self.paintCaptionButton( hdc, @@ -14975,52 +13703,14 @@ const Host = struct { self.paintCaptionButton(hdc, close_rect, .close, .close, now, is_hc, theme); } - fn paintChrome(self: *Host) void { - const hwnd = self.hwnd orelse return; - var ps: PAINTSTRUCT = undefined; - const hdc = BeginPaint(hwnd, &ps) orelse return; - defer _ = EndPaint(hwnd, &ps); - - const alloc = self.app.core_app.alloc; - const theme = &self.app.resolved_theme; - - // Select chrome font, save previous for restore - const prev_font = if (self.chrome_font) |font| SelectObject(hdc, font) else null; - defer if (prev_font) |pf| { - _ = SelectObject(hdc, pf); - }; - - var client_rect: RECT = undefined; - if (GetClientRect(hwnd, &client_rect) == 0) return; - const tab_h = self.tabBarHeight(); - // Note: tab_h is already scaled via tabBarHeight() - const overlay_offset: i32 = if (self.overlay_mode != .none) self.scaled(host_overlay_height) else 0; - const inspector_panel_visible = self.inspectorPanelVisible(); - const inspector_offset: i32 = if (inspector_panel_visible) self.scaled(host_inspector_panel_height) else 0; - const status_h = self.statusBarHeight(); - const content_rect = RECT{ - .left = 0, - .top = tab_h + overlay_offset + inspector_offset, - .right = client_rect.right, - .bottom = @max(tab_h + 1, client_rect.bottom - status_h), - }; - const top_rect = RECT{ - .left = client_rect.left, - .top = client_rect.top, - .right = client_rect.right, - .bottom = @max(client_rect.top, content_rect.top), - }; - const status_rect = RECT{ - .left = client_rect.left, - .top = @min(client_rect.bottom, content_rect.bottom), - .right = client_rect.right, - .bottom = client_rect.bottom, - }; - const paint_top = paintRectVisible(hdc, ps.rcPaint, top_rect); - const paint_content = paintRectVisible(hdc, ps.rcPaint, content_rect); - const paint_status = paintRectVisible(hdc, ps.rcPaint, status_rect); - const banner_y: i32 = tab_h + overlay_offset + inspector_offset + self.scaled(2); - + fn paintChromeTabBar( + self: *Host, + hdc: HDC, + client_rect: RECT, + tab_h: i32, + paint_top: bool, + theme: *const ThemeColors, + ) void { // Tab bar (only when visible) if (paint_top and tab_h > 0) { const tab_rect = RECT{ @@ -15041,7 +13731,7 @@ const Host = struct { // match Windows Terminal's tab indicator treatment // (wt-chrome-reference.md §1). { - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); const cur = self.tab_underline.currentRect(now_ms); const underline_left = @as(i32, @intFromFloat(@round(cur.left))) + self.scaled(3); const underline_right = @as(i32, @intFromFloat(@round(cur.left + cur.width))) - self.scaled(3); @@ -15063,10 +13753,7 @@ const Host = struct { .right = client_rect.right, .bottom = @max(1, self.scaled(2)), }, - if (theme.is_dark) - adjustColor(theme.accent, -12, -12, -12) - else - adjustColor(theme.accent, 18, 18, 18), + themeSurface(theme, .tab_accent), ); if (!self.usingIntegratedTitlebar()) { const cluster_left = @max(self.scaled(8), client_rect.right - self.rightButtonsWidth() - self.scaled(4)); @@ -15080,14 +13767,8 @@ const Host = struct { drawRoundedRect( hdc, cluster_rect, - if (theme.is_dark) - adjustColor(theme.chrome_bg, 8, 8, 10) - else - adjustColor(theme.chrome_bg, 6, 6, 6), - if (theme.is_dark) - adjustColor(theme.chrome_border, 10, 10, 12) - else - adjustColor(theme.chrome_border, -16, -16, -16), + themeSurface(theme, .caption_cluster_bg), + themeSurface(theme, .caption_cluster_border), self.scaled(6), ); if (cluster_left > self.scaled(14)) { @@ -15120,7 +13801,17 @@ const Host = struct { theme.chrome_border, ); } // end tab bar painting + } + fn paintChromeOverlay( + self: *Host, + hdc: HDC, + alloc: Allocator, + client_rect: RECT, + tab_h: i32, + paint_top: bool, + theme: *const ThemeColors, + ) bool { if (paint_top and self.overlay_mode != .none) { const overlay_rect = RECT{ .left = 0, @@ -15139,14 +13830,8 @@ const Host = struct { drawRoundedRect( hdc, overlay_panel, - if (theme.is_dark) - adjustColor(theme.overlay_bg, 4, 4, 6) - else - adjustColor(theme.overlay_bg, -4, -4, -4), - if (theme.is_dark) - adjustColor(theme.overlay_border, 10, 10, 12) - else - adjustColor(theme.overlay_border, -18, -18, -18), + themeSurface(theme, .overlay_panel_bg), + themeSurface(theme, .overlay_panel_border), self.scaled(7), ); fillSolidRect(hdc, .{ @@ -15185,7 +13870,7 @@ const Host = struct { self.profiles orelse &.{}, overlay_text, self.selectedProfileIndex() orelse 0, - ) catch return + ) catch return false else buildOverlayPaintLabelText( alloc, @@ -15195,17 +13880,17 @@ const Host = struct { if (surface) |value| value.search_selected else null, overlay_status, self.palettePresentation(), - ) catch return; + ) catch return false; defer alloc.free(overlay_label); if (self.cached_overlay_paint_label_w) |old| alloc.free(old); - self.cached_overlay_paint_label_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, overlay_label) catch return; + self.cached_overlay_paint_label_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, overlay_label) catch return false; const overlay_feedback = if (self.overlay_mode == .profile) if (self.banner_text) |value| switch (self.banner_kind) { - .err => std.fmt.allocPrint(alloc, "Error: {s}", .{value}) catch return, - .info => std.fmt.allocPrint(alloc, "Info: {s}", .{value}) catch return, - .none => alloc.dupe(u8, value) catch return, + .err => std.fmt.allocPrint(alloc, "Error: {s}", .{value}) catch return false, + .info => std.fmt.allocPrint(alloc, "Info: {s}", .{value}) catch return false, + .none => alloc.dupe(u8, value) catch return false, } else buildProfileHintText( @@ -15215,7 +13900,7 @@ const Host = struct { self.selectedProfileIndex() orelse 0, self.app.launcher_profile_target, self.app.launcher_quick_slot_keys, - ) catch return + ) catch return false else feedback: { var mru_buf: [5][]const u8 = undefined; const mru = self.app.paletteMruSlice(&mru_buf); @@ -15233,18 +13918,18 @@ const Host = struct { self.paletteSnapshot(), mru, self.palettePresentation(), - ) catch return; + ) catch return false; }; defer alloc.free(overlay_feedback); if (self.cached_overlay_paint_feedback_w) |old| alloc.free(old); - self.cached_overlay_paint_feedback_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, overlay_feedback) catch return; + self.cached_overlay_paint_feedback_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, overlay_feedback) catch return false; if (self.overlay_mode == .profile) { if (self.selectedProfile()) |profile| { - const badge = buildProfileChromeBadgeText(alloc, profile.kind) catch return; + const badge = buildProfileChromeBadgeText(alloc, profile.kind) catch return false; defer alloc.free(badge); if (self.cached_overlay_paint_badge_w) |old| alloc.free(old); - self.cached_overlay_paint_badge_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, badge) catch return; + self.cached_overlay_paint_badge_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, badge) catch return false; } else { if (self.cached_overlay_paint_badge_w) |old| alloc.free(old); self.cached_overlay_paint_badge_w = null; @@ -15255,7 +13940,7 @@ const Host = struct { } self.chrome_text_dirty.overlay = false; } - _ = SetBkMode(hdc, TRANSPARENT); + _ = sys.SetBkMode(hdc, c.TRANSPARENT); const overlay_padding = self.scaled(host_overlay_padding); const overlay_accept_button_w = self.scaled(host_overlay_accept_width); const overlay_cancel_button_w = self.scaled(host_overlay_cancel_width); @@ -15279,7 +13964,7 @@ const Host = struct { var overlay_label_color: u32 = theme.overlay_label_fg; if (overlay_label_reservation > 0 and self.overlay_mode == .profile) { if (self.selectedProfile()) |profile| { - const badge_w = self.cached_overlay_paint_badge_w orelse return; + const badge_w = self.cached_overlay_paint_badge_w orelse return false; const badge_width = self.scaled(16) + @as(i32, @intCast(badge_w.len * @as(usize, @intCast(self.scaled(7))))); const badge_rect = RECT{ .left = self.scaled(host_overlay_padding) + self.scaled(10), @@ -15289,7 +13974,7 @@ const Host = struct { }; const accent = profileChromeAccent(profile.kind, theme.is_dark); drawRoundedRect(hdc, badge_rect, accent.idle_bg, accent.idle_border, self.scaled(3)); - _ = SetTextColor(hdc, profileKindLabelColor(profile.kind, theme.is_dark)); + _ = sys.SetTextColor(hdc, profileKindLabelColor(profile.kind, theme.is_dark)); var badge_text_rect = badge_rect; badge_text_rect.left += self.scaled(6); badge_text_rect.right -= self.scaled(6); @@ -15297,13 +13982,13 @@ const Host = struct { hdc, badge_w, &badge_text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX, ); overlay_label_x = badge_rect.right + self.scaled(10); overlay_label_color = profileKindLabelColor(profile.kind, theme.is_dark); } } - _ = SetTextColor(hdc, overlay_label_color); + _ = sys.SetTextColor(hdc, overlay_label_color); if (overlay_label_reservation > 0) if (self.cached_overlay_paint_label_w) |overlay_label_w| { textOutWz(hdc, overlay_label_x, overlay_rect.top + self.scaled(7), overlay_label_w); }; @@ -15320,13 +14005,13 @@ const Host = struct { self.current_dpi, ); const overlay_edit_focused = if (self.overlay_edit_hwnd) |edit_hwnd| - GetFocus() == edit_hwnd + sys.GetFocus() == edit_hwnd else false; drawRoundedRect(hdc, edit_frame, theme.edit_frame_bg, overlayEditBorderColor(self.overlay_mode, overlay_edit_focused, theme.is_dark), self.scaled(4)); } - _ = SetTextColor(hdc, if (self.overlay_mode == .profile and self.banner_text == null) + _ = sys.SetTextColor(hdc, if (self.overlay_mode == .profile and self.banner_text == null) if (self.selectedProfile()) |profile| profileKindHintColor(profile.kind, theme.is_dark) else @@ -15350,11 +14035,21 @@ const Host = struct { hdc, overlay_feedback_w, &feedback_rect, - DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS, + c.DT_LEFT | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX | c.DT_END_ELLIPSIS, ); } } + return true; + } + + fn paintChromeContentLane( + self: *Host, + hdc: HDC, + content_rect: RECT, + paint_content: bool, + theme: *const ThemeColors, + ) void { if (paint_content) { // Content-lane paints can arrive after search-frame or pane // geometry shrinks/moves. Clear the exposed parent-painted @@ -15375,14 +14070,8 @@ const Host = struct { drawRoundedRect( hdc, frame_rect, - if (theme.is_dark) - adjustColor(theme.overlay_bg, 3, 3, 5) - else - adjustColor(theme.overlay_bg, -2, -2, -1), - if (theme.is_dark) - adjustColor(theme.overlay_border, 10, 10, 12) - else - adjustColor(theme.overlay_border, -14, -14, -14), + themeSurface(theme, .search_frame_bg), + themeSurface(theme, .search_frame_border), self.scaled(8), ); fillSolidRect(hdc, .{ @@ -15390,10 +14079,7 @@ const Host = struct { .top = frame_rect.bottom, .right = frame_rect.right - self.scaled(8), .bottom = frame_rect.bottom + 1, - }, if (theme.is_dark) - adjustColor(theme.overlay_border, -10, -10, -8) - else - adjustColor(theme.overlay_border, 10, 10, 10)); + }, themeSurface(theme, .search_frame_underline)); if (searchBarGroupRect(&[_]ChildPlacement{ entry.view.search_bar_results_placement, @@ -15453,7 +14139,7 @@ const Host = struct { if (entry.view.search_bar_edit_placement.rect_known) { const edit_rect = entry.view.search_bar_edit_placement.rect; const edit_focused = if (entry.view.search_bar_edit_hwnd) |edit_hwnd| - GetFocus() == edit_hwnd + sys.GetFocus() == edit_hwnd else false; const edit_frame_rect = RECT{ @@ -15481,7 +14167,19 @@ const Host = struct { } } } + } + fn paintChromeInspectorPanel( + self: *Host, + hdc: HDC, + alloc: Allocator, + client_rect: RECT, + tab_h: i32, + overlay_offset: i32, + inspector_panel_visible: bool, + paint_top: bool, + theme: *const ThemeColors, + ) void { if (paint_top and inspector_panel_visible) { const panel_rect = RECT{ .left = 0, @@ -15523,18 +14221,27 @@ const Host = struct { self.chrome_text_dirty.inspector = false; } - _ = SetBkMode(hdc, TRANSPARENT); + _ = sys.SetBkMode(hdc, c.TRANSPARENT); if (self.cached_inspector_title_w) |title_w| { - _ = SetTextColor(hdc, theme.overlay_label_fg); + _ = sys.SetTextColor(hdc, theme.overlay_label_fg); textOutWz(hdc, self.scaled(16), panel_rect.top + self.scaled(6), title_w); } if (self.cached_inspector_hint_w) |hint_w| { - _ = SetTextColor(hdc, theme.text_secondary); + _ = sys.SetTextColor(hdc, theme.text_secondary); textOutWz(hdc, self.scaled(16), panel_rect.top + self.scaled(22), hint_w); } } } + } + fn paintChromePaneDividers( + self: *Host, + hwnd: HWND, + hdc: HDC, + content_rect: RECT, + paint_content: bool, + theme: *const ThemeColors, + ) void { // Paint pane divider gaps between split panes if (paint_content) { if (self.activeTab()) |active_tab| { @@ -15549,11 +14256,11 @@ const Host = struct { if (focused_surface) |surface| { if (surface.hwnd) |surface_hwnd| { var sr: RECT = undefined; - if (GetWindowRect(surface_hwnd, &sr) != 0) { + if (sys.GetWindowRect(surface_hwnd, &sr) != 0) { var tl = POINT{ .x = sr.left, .y = sr.top }; var br = POINT{ .x = sr.right, .y = sr.bottom }; - _ = ScreenToClient(hwnd, &tl); - _ = ScreenToClient(hwnd, &br); + _ = sys.ScreenToClient(hwnd, &tl); + _ = sys.ScreenToClient(hwnd, &br); // Only draw focus border if pane is within content area if (tl.x >= c_rect.left and br.x <= c_rect.right and tl.y >= c_rect.top and br.y <= c_rect.bottom) @@ -15576,7 +14283,17 @@ const Host = struct { } } } + } + fn paintChromeStatusBackground( + hdc: HDC, + client_rect: RECT, + tab_h: i32, + overlay_offset: i32, + status_h: i32, + paint_status: bool, + theme: *const ThemeColors, + ) void { const status_top = @max(tab_h + overlay_offset, client_rect.bottom - @max(1, status_h)); if (paint_status and status_h > 0) { const status_bg_rect = RECT{ @@ -15597,9 +14314,21 @@ const Host = struct { theme.chrome_border, ); } - _ = SetBkMode(hdc, TRANSPARENT); - _ = SetTextColor(hdc, theme.text_primary); + _ = sys.SetBkMode(hdc, c.TRANSPARENT); + _ = sys.SetTextColor(hdc, theme.text_primary); + } + fn paintChromeBanner( + self: *Host, + hwnd: HWND, + hdc: HDC, + alloc: Allocator, + client_rect: RECT, + banner_y: i32, + inspector_panel_visible: bool, + paint_top: bool, + theme: *const ThemeColors, + ) bool { const explicit_banner_text: ?[]const u8 = if (self.overlay_mode == .none) self.banner_text else @@ -15629,13 +14358,13 @@ const Host = struct { if (paint_update_notice) { if (self.app.update_notice) |notice| { if (notice.message_text) |message| { - self.cached_banner_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, message) catch return; + self.cached_banner_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, message) catch return false; } } } else if (explicit_banner_text) |value| { - const full = buildHostBannerText(alloc, banner_kind, value) catch return; + const full = buildHostBannerText(alloc, banner_kind, value) catch return false; defer alloc.free(full); - self.cached_banner_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, full) catch return; + self.cached_banner_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, full) catch return false; } else if (paint_inspector_banner) { if (self.activeSurface()) |surface| { const host_status = self.app.hostTabStatus(surface); @@ -15645,11 +14374,11 @@ const Host = struct { host_status, pane_count, if (self.activeTab()) |tab| tab.tree.zoomed != null else false, - ) catch return; + ) catch return false; defer alloc.free(value); - const full = buildHostBannerText(alloc, banner_kind, value) catch return; + const full = buildHostBannerText(alloc, banner_kind, value) catch return false; defer alloc.free(full); - self.cached_banner_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, full) catch return; + self.cached_banner_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, full) catch return false; } } self.chrome_text_dirty.banner = false; @@ -15709,60 +14438,54 @@ const Host = struct { .bottom = banner_y + button_height, }; - _ = SetTextColor(hdc, theme.info_fg); + _ = sys.SetTextColor(hdc, theme.info_fg); if (self.cached_banner_w) |banner_w| { drawTextWz( hdc, banner_w, &text_rect, - DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS, + c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX | c.DT_END_ELLIPSIS, ); } drawRoundedRect( hdc, open_rect, - if (theme.is_dark) - adjustColor(theme.overlay_bg, 10, 10, 12) - else - adjustColor(theme.overlay_bg, -4, -4, -4), + themeSurface(theme, .update_open_bg), theme.info_fg, self.scaled(5), ); drawRoundedRect( hdc, dismiss_rect, - if (theme.is_dark) - adjustColor(theme.status_bg, 10, 10, 12) - else - adjustColor(theme.status_bg, -4, -4, -4), + themeSurface(theme, .update_dismiss_bg), theme.chrome_border, self.scaled(5), ); - _ = SetTextColor(hdc, theme.text_primary); + _ = sys.SetTextColor(hdc, theme.text_primary); var open_text_rect = open_rect; open_text_rect.left += self.scaled(6); open_text_rect.right -= self.scaled(6); var dismiss_text_rect = dismiss_rect; dismiss_text_rect.left += self.scaled(6); dismiss_text_rect.right -= self.scaled(6); - _ = DrawTextW( + _ = sys.DrawTextW( hdc, open_label, @intCast(std.mem.sliceTo(open_label, 0).len), &open_text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX, ); - _ = DrawTextW( + _ = sys.DrawTextW( hdc, dismiss_label, @intCast(std.mem.sliceTo(dismiss_label, 0).len), &dismiss_text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX, ); } else if (paint_inspector_banner) { - _ = SetTextColor(hdc, switch (banner_kind) { + _ = sys.SetTextColor(hdc, switch (banner_kind) { .none => theme.text_primary, .info => theme.info_fg, .err => theme.error_fg, @@ -15772,7 +14495,7 @@ const Host = struct { } } } else if (explicit_banner_text != null and !dwrite_banner_painted) { - _ = SetTextColor(hdc, switch (banner_kind) { + _ = sys.SetTextColor(hdc, switch (banner_kind) { .none => theme.text_primary, .info => theme.info_fg, .err => theme.error_fg, @@ -15781,13 +14504,21 @@ const Host = struct { textOutWz(hdc, self.scaled(16), banner_y, banner_w); } } - _ = SetTextColor(hdc, theme.text_primary); - if (status_h <= 0) { - self.chrome_repaint_dirty = false; - return; - } + _ = sys.SetTextColor(hdc, theme.text_primary); + return true; + } - const status_y = @max(self.scaled(host_tab_height) + self.scaled(2), ps.rcPaint.bottom - @max(1, status_h) + self.scaled(4)); + fn paintChromeStatusBar( + self: *Host, + hdc: HDC, + alloc: Allocator, + client_rect: RECT, + paint_rect: RECT, + status_h: i32, + paint_status: bool, + theme: *const ThemeColors, + ) bool { + const status_y = @max(self.scaled(host_tab_height) + self.scaled(2), paint_rect.bottom - @max(1, status_h) + self.scaled(4)); var status_x: i32 = self.scaled(16); const launcher_lane_right: ?i32 = blk: { if (!(status_h > 0 and self.overlay_mode == .none and self.selectedProfile() != null)) break :blk null; @@ -15807,14 +14538,8 @@ const Host = struct { drawRoundedRect( hdc, lane_rect, - if (theme.is_dark) - adjustColor(theme.status_bg, 8, 8, 10) - else - adjustColor(theme.status_bg, -6, -6, -6), - if (theme.is_dark) - adjustColor(theme.chrome_border, 12, 12, 14) - else - adjustColor(theme.chrome_border, -18, -18, -18), + themeSurface(theme, .launcher_lane_bg), + themeSurface(theme, .launcher_lane_border), self.scaled(6), ); if (self.selectedProfile()) |profile| { @@ -15843,9 +14568,9 @@ const Host = struct { profile, selected_profile_index, self.app.launcherQuickSlotOrdinal(profile.key), - ) catch return; + ) catch return false; defer alloc.free(chip); - self.cached_launcher_selected_chip_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, chip) catch return; + self.cached_launcher_selected_chip_w = std.unicode.utf8ToUtf16LeAllocZ(alloc, chip) catch return false; } if (self.profiles) |profiles| { @@ -15858,9 +14583,9 @@ const Host = struct { profile, index, self.app.launcherQuickSlotOrdinal(profile.key), - ) catch return; + ) catch return false; defer alloc.free(chip); - self.cached_launcher_quick_slot_chip_w[cached_slot] = std.unicode.utf8ToUtf16LeAllocZ(alloc, chip) catch return; + self.cached_launcher_quick_slot_chip_w[cached_slot] = std.unicode.utf8ToUtf16LeAllocZ(alloc, chip) catch return false; cached_slot += 1; } } @@ -15869,7 +14594,7 @@ const Host = struct { if (self.selectedProfile()) |profile| { const pinned_slot_ordinal = self.app.launcherQuickSlotOrdinal(profile.key); const pinned_slot_digit = pinnedSlotBadgeDigit(pinned_slot_ordinal); - const chip_w = self.cached_launcher_selected_chip_w orelse return; + const chip_w = self.cached_launcher_selected_chip_w orelse return false; const accent = profileChromeAccent(profile.kind, theme.is_dark); const chip_width = self.scaled(16) + @as(i32, @intCast(profileStatusBadgeTextLen( profile, @@ -15903,7 +14628,7 @@ const Host = struct { profileOpenTargetMarkerColor(self.app.launcher_profile_target), self.current_dpi, ); - _ = SetTextColor(hdc, profileKindLabelColor(profile.kind, theme.is_dark)); + _ = sys.SetTextColor(hdc, profileKindLabelColor(profile.kind, theme.is_dark)); var chip_text_rect = chip_rect; chip_text_rect.left += self.scaled(6); chip_text_rect.right -= launcherChipRightInset(pinned_slot_digit != null, true); @@ -15911,7 +14636,7 @@ const Host = struct { hdc, chip_w, &chip_text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX | c.DT_END_ELLIPSIS, ); status_x = chip_rect.right + self.scaled(10); } @@ -15921,7 +14646,7 @@ const Host = struct { if (drawn >= 3) break; if (selected_profile_index != null and index == selected_profile_index.?) continue; const pinned_slot_ordinal = self.app.launcherQuickSlotOrdinal(profile.key); - const chip_w = self.cached_launcher_quick_slot_chip_w[drawn] orelse return; + const chip_w = self.cached_launcher_quick_slot_chip_w[drawn] orelse return false; const focused = self.focused_quick_slot != null and self.focused_quick_slot.? == index; const hovered = self.hovered_quick_slot != null and self.hovered_quick_slot.? == index; const target_marker = shouldPaintQuickSlotTargetMarker(hovered, focused); @@ -15984,7 +14709,7 @@ const Host = struct { .bottom = chip_rect.bottom - self.scaled(2), }, focus_ring); } - _ = SetTextColor(hdc, colors.fg); + _ = sys.SetTextColor(hdc, colors.fg); var chip_text_rect = chip_rect; chip_text_rect.left += self.scaled(5); chip_text_rect.right -= launcherChipRightInset(false, target_marker); @@ -15992,7 +14717,7 @@ const Host = struct { hdc, chip_w, &chip_text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX | c.DT_END_ELLIPSIS, ); status_x = chip_rect.right + self.scaled(6); drawn += 1; @@ -16035,22 +14760,116 @@ const Host = struct { } if (paint_status) { if (self.cached_detail_w) |detail_w| { - _ = SetTextColor(hdc, theme.text_secondary); + _ = sys.SetTextColor(hdc, theme.text_secondary); textOutWz(hdc, status_x, status_y + self.scaled(18), detail_w); } } - self.chrome_repaint_dirty = false; + return true; } -}; -fn destroyChildWindow(hwnd_slot: *?HWND) void { - const hwnd = hwnd_slot.* orelse return; + fn paintChrome(self: *Host) void { + const hwnd = self.hwnd orelse return; + var ps: PAINTSTRUCT = undefined; + const hdc = sys.BeginPaint(hwnd, &ps) orelse return; + defer _ = sys.EndPaint(hwnd, &ps); - hwnd_slot.* = null; - if (IsWindow(hwnd) == 0) return; + const alloc = self.app.core_app.alloc; + const theme = &self.app.resolved_theme; - _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); - _ = DestroyWindow(hwnd); + const prev_font = if (self.chrome_font) |font| sys.SelectObject(hdc, font) else null; + defer if (prev_font) |pf| { + _ = sys.SelectObject(hdc, pf); + }; + + var client_rect: RECT = undefined; + if (sys.GetClientRect(hwnd, &client_rect) == 0) return; + const tab_h = self.tabBarHeight(); + const overlay_offset: i32 = if (self.overlay_mode != .none) self.scaled(host_overlay_height) else 0; + const inspector_panel_visible = self.inspectorPanelVisible(); + const inspector_offset: i32 = if (inspector_panel_visible) self.scaled(host_inspector_panel_height) else 0; + const status_h = statusBarHeight(); + const content_rect = RECT{ + .left = 0, + .top = tab_h + overlay_offset + inspector_offset, + .right = client_rect.right, + .bottom = @max(tab_h + 1, client_rect.bottom - status_h), + }; + const top_rect = RECT{ + .left = client_rect.left, + .top = client_rect.top, + .right = client_rect.right, + .bottom = @max(client_rect.top, content_rect.top), + }; + const status_rect = RECT{ + .left = client_rect.left, + .top = @min(client_rect.bottom, content_rect.bottom), + .right = client_rect.right, + .bottom = client_rect.bottom, + }; + const paint_top = paintRectVisible(hdc, ps.rcPaint, top_rect); + const paint_content = paintRectVisible(hdc, ps.rcPaint, content_rect); + const paint_status = paintRectVisible(hdc, ps.rcPaint, status_rect); + const banner_y: i32 = tab_h + overlay_offset + inspector_offset + self.scaled(2); + + self.paintChromeTabBar(hdc, client_rect, tab_h, paint_top, theme); + if (!self.paintChromeOverlay(hdc, alloc, client_rect, tab_h, paint_top, theme)) return; + self.paintChromeContentLane(hdc, content_rect, paint_content, theme); + self.paintChromeInspectorPanel( + hdc, + alloc, + client_rect, + tab_h, + overlay_offset, + inspector_panel_visible, + paint_top, + theme, + ); + self.paintChromePaneDividers(hwnd, hdc, content_rect, paint_content, theme); + paintChromeStatusBackground( + hdc, + client_rect, + tab_h, + overlay_offset, + status_h, + paint_status, + theme, + ); + if (!self.paintChromeBanner( + hwnd, + hdc, + alloc, + client_rect, + banner_y, + inspector_panel_visible, + paint_top, + theme, + )) return; + if (status_h <= 0) { + self.chrome_repaint_dirty = false; + return; + } + + if (!self.paintChromeStatusBar( + hdc, + alloc, + client_rect, + ps.rcPaint, + status_h, + paint_status, + theme, + )) return; + self.chrome_repaint_dirty = false; + } +}; + +fn destroyChildWindow(hwnd_slot: *?HWND) void { + const hwnd = hwnd_slot.* orelse return; + + hwnd_slot.* = null; + if (sys.IsWindow(hwnd) == 0) return; + + setWindowData(hwnd, null); + _ = sys.DestroyWindow(hwnd); } fn destroySubclassedWindowWithPrev( @@ -16064,17 +14883,17 @@ fn destroySubclassedWindowWithPrev( // WM_NCDESTROY, so leaving the HWND discoverable via host tabs/buttons // risks callbacks touching half-torn state. hwnd_slot.* = null; - if (IsWindow(hwnd) == 0) return; + if (sys.IsWindow(hwnd) == 0) return; - _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + setWindowData(hwnd, null); if (prev_proc) |proc| { - _ = SetWindowLongPtrW( + _ = sys.SetWindowLongPtrW( hwnd, - GWLP_WNDPROC, + c.GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(proc))), ); } - _ = DestroyWindow(hwnd); + _ = sys.DestroyWindow(hwnd); } fn destroySubclassedWindow( @@ -16239,75 +15058,9 @@ fn restoreTerminalUndoState( }; } -const HostBannerKind = enum { - none, - info, - err, -}; - -const TabButtonKeyAction = enum { - previous, - next, - first, - last, - move_previous, - move_next, - move_first, - move_last, - rename, - close, - overview, -}; - -const SearchButtonKeyAction = enum { - next, - previous, - dismiss, -}; - -const TabsButtonKeyAction = enum { - previous, - next, - rename, - overview, -}; - -const CommandButtonKeyAction = enum { - toggle, - previous, - next, - dismiss, -}; - -const ProfilesButtonKeyAction = enum { - open, - toggle, - previous, - next, - first, - last, -}; - -const QuickSlotFocusKeyAction = enum { - previous, - next, - first, - last, - open, -}; - -const LaunchTargetButtonKeyAction = enum { - previous, - next, - first, - last, -}; - -const ProfileOpenTarget = enum { - tab, - window, - split, -}; +const HostBannerKind = labels.HostBannerKind; +const LaunchTargetButtonKeyAction = labels.LaunchTargetButtonKeyAction; +const ProfileOpenTarget = labels.ProfileOpenTarget; fn parseProfileOpenTarget(raw: []const u8) ?ProfileOpenTarget { if (std.ascii.eqlIgnoreCase(raw, "tab")) return .tab; @@ -16332,10 +15085,10 @@ fn cycleProfileOpenTarget(current: ProfileOpenTarget, reverse: bool) ProfileOpen fn launchTargetButtonKeyAction(vk: WPARAM) ?LaunchTargetButtonKeyAction { return switch (vk) { - VK_LEFT, VK_UP => .previous, - VK_RIGHT, VK_DOWN, VK_RETURN, VK_SPACE => .next, - VK_HOME => .first, - VK_END => .last, + c.VK_LEFT, c.VK_UP => .previous, + c.VK_RIGHT, c.VK_DOWN, c.VK_RETURN, c.VK_SPACE => .next, + c.VK_HOME => .first, + c.VK_END => .last, else => null, }; } @@ -16363,12 +15116,7 @@ fn appendOwnedString( target.* = next; } -fn ownedStringEquals(current: ?[:0]const u8, value: []const u8) bool { - return if (current) |existing| - std.mem.eql(u8, existing, value) - else - false; -} +const ownedStringEquals = labels.ownedStringEquals; fn ownedBytesEquals(current: ?[]const u8, value: []const u8) bool { return if (current) |existing| @@ -16394,7 +15142,7 @@ fn syncWindowTextUtf8Cached( try appendOwnedString(alloc, cached, value); const value_w = try std.unicode.utf8ToUtf16LeAllocZ(alloc, value); defer alloc.free(value_w); - _ = SetWindowTextW(hwnd, value_w.ptr); + _ = sys.SetWindowTextW(hwnd, value_w.ptr); return true; } @@ -16407,7 +15155,7 @@ fn syncWindowTextUtf8CachedAfterSet( if (ownedStringEquals(cached.*, value)) return false; const value_w = try std.unicode.utf8ToUtf16LeAllocZ(alloc, value); defer alloc.free(value_w); - _ = SetWindowTextW(hwnd, value_w.ptr); + _ = sys.SetWindowTextW(hwnd, value_w.ptr); try appendOwnedString(alloc, cached, value); return true; } @@ -16422,186 +15170,6 @@ fn overlayEditText(self: *Host) ![]const u8 { return self.cached_overlay_edit orelse ""; } -fn rectEquals(a: RECT, b: RECT) bool { - return a.left == b.left and - a.top == b.top and - a.right == b.right and - a.bottom == b.bottom; -} - -fn childRect(x: i32, y: i32, width: i32, height: i32) RECT { - return .{ - .left = x, - .top = y, - .right = x + width, - .bottom = y + height, - }; -} - -fn layoutRectFromWin32(rect: RECT) win32_layout.Rect { - return .{ - .left = rect.left, - .top = rect.top, - .right = rect.right, - .bottom = rect.bottom, - }; -} - -fn layoutRectToWin32(rect: win32_layout.Rect) RECT { - return .{ - .left = rect.left, - .top = rect.top, - .right = rect.right, - .bottom = rect.bottom, - }; -} - -fn centeredRect(rect: RECT, width: i32, height: i32) RECT { - const outer_w = rect.right - rect.left; - const outer_h = rect.bottom - rect.top; - const left = rect.left + @divTrunc(outer_w - width, 2); - const top = rect.top + @divTrunc(outer_h - height, 2); - return childRect(left, top, width, height); -} - -fn overlayEditFrameRect( - width: i32, - overlay_y: i32, - padding: i32, - label_w: i32, - cancel_w: i32, - accept_reservation_w: i32, - row_h: i32, - dpi: u32, -) RECT { - const top_offset = Host.scaledBy(4, dpi); - const right_gap = Host.scaledBy(6, dpi); - const min_edit_width = Host.scaledBy(24, dpi); - const effective_label_w = overlayLabelReservation( - width, - padding, - label_w, - cancel_w, - accept_reservation_w, - dpi, - ); - const bounded_width = @max(0, width); - const raw_right = width - cancel_w - accept_reservation_w - (padding * 2) - right_gap; - const right = @min(bounded_width, @max(@min(bounded_width, min_edit_width), raw_right)); - const left = @min( - @max(0, padding + effective_label_w), - @max(0, right - min_edit_width), - ); - return .{ - .left = left, - .top = overlay_y + top_offset, - .right = right, - .bottom = overlay_y + top_offset + row_h, - }; -} - -fn overlayLabelReservation( - width: i32, - padding: i32, - desired_label_w: i32, - cancel_w: i32, - accept_reservation_w: i32, - dpi: u32, -) i32 { - const right_gap = Host.scaledBy(6, dpi); - const min_edit_width = Host.scaledBy(24, dpi); - const desired = @max(0, desired_label_w); - const available_before_actions = width - @max(0, cancel_w) - @max(0, accept_reservation_w) - - 2 * @max(0, padding) - right_gap; - return if (available_before_actions - @max(0, padding) >= desired + min_edit_width) desired else 0; -} - -const OverlayActionVisibility = struct { - accept: bool, - cancel: bool, -}; - -fn overlayActionVisibilityForWidth( - width: i32, - padding: i32, - cancel_w: i32, - accept_w: i32, - accept_requested: bool, - dpi: u32, -) OverlayActionVisibility { - const bounded_padding = @max(0, padding); - const right_gap = Host.scaledBy(6, dpi); - const min_edit_width = Host.scaledBy(24, dpi); - const cancel = width >= @max(0, cancel_w) + 3 * bounded_padding + right_gap + min_edit_width; - const accept = cancel and accept_requested and - width >= @max(0, cancel_w) + @max(0, accept_w) + 4 * bounded_padding + right_gap + min_edit_width; - return .{ .accept = accept, .cancel = cancel }; -} - -const OverlayActionLayout = struct { - accept_visible: bool, - cancel_visible: bool, - compact_cancel: bool, - accept_x: i32, - cancel_x: i32, - accept_width: i32, - cancel_width: i32, - accept_reservation_width: i32, -}; - -fn overlayActionLayoutForWidth( - mode: HostOverlayMode, - width: i32, - padding: i32, - cancel_button_w: i32, - accept_button_w: i32, - dpi: u32, -) OverlayActionLayout { - const bounded_padding = @max(0, padding); - const visibility = overlayActionVisibilityForWidth( - width, - bounded_padding, - cancel_button_w, - accept_button_w, - overlayAcceptButtonVisible(mode), - dpi, - ); - const compact_cancel = mode == .confirm and !visibility.cancel and width > 0; - const compact_inset = if (compact_cancel) - @min(bounded_padding, @divTrunc(width - 1, 2)) - else - bounded_padding; - const cancel_visible = visibility.cancel or compact_cancel; - const accept_width = if (visibility.accept) @max(0, accept_button_w) else 0; - const cancel_width = if (compact_cancel) - width - 2 * compact_inset - else if (cancel_visible) - @max(0, cancel_button_w) - else - 0; - return .{ - .accept_visible = visibility.accept, - .cancel_visible = cancel_visible, - .compact_cancel = compact_cancel, - .accept_x = @max(0, width - cancel_width - accept_width - 2 * bounded_padding), - .cancel_x = if (compact_cancel) compact_inset else width - cancel_width - bounded_padding, - .accept_width = accept_width, - .cancel_width = cancel_width, - .accept_reservation_width = if (visibility.accept) accept_width + bounded_padding else 0, - }; -} - -fn overlayEditChildRectFromFrame(frame: RECT, inset_x: i32, inset_y: i32) RECT { - const left = @min(frame.right, frame.left + @max(0, inset_x)); - const top = @min(frame.bottom, frame.top + @max(0, inset_y)); - return .{ - .left = left, - .top = top, - .right = @max(left, frame.right - @max(0, inset_x)), - .bottom = @max(top, frame.bottom - @max(0, inset_y)), - }; -} - fn syncChildPlacementTarget(placement: *ChildPlacement, hwnd: HWND) void { if (placement.hwnd == null or placement.hwnd.? != hwnd) { placement.* = .{ .hwnd = hwnd }; @@ -16611,7 +15179,7 @@ fn syncChildPlacementTarget(placement: *ChildPlacement, hwnd: HWND) void { fn applyChildRect(hwnd: HWND, placement: *ChildPlacement, rect: RECT) bool { syncChildPlacementTarget(placement, hwnd); if (placement.rect_known and rectEquals(placement.rect, rect)) return false; - _ = MoveWindow( + _ = sys.MoveWindow( hwnd, rect.left, rect.top, @@ -16627,7 +15195,7 @@ fn applyChildRect(hwnd: HWND, placement: *ChildPlacement, rect: RECT) bool { fn applyChildVisibility(hwnd: HWND, placement: *ChildPlacement, visible: bool) bool { syncChildPlacementTarget(placement, hwnd); if (placement.visible_known and placement.visible == visible) return false; - _ = ShowWindow(hwnd, if (visible) SW_SHOW else SW_HIDE); + _ = sys.ShowWindow(hwnd, if (visible) c.SW_SHOW else c.SW_HIDE); placement.visible = visible; placement.visible_known = true; return true; @@ -16922,14 +15490,14 @@ fn scrollbarPaintKeyChanged(previous: ?ScrollbarPaintKey, next: ScrollbarPaintKe } fn highContrastThemeFromSysColors() ThemeColors { - const win_bg = GetSysColor(COLOR_WINDOW); - const win_fg = GetSysColor(COLOR_WINDOWTEXT); - const win_frame = GetSysColor(COLOR_WINDOWFRAME); - const btn_bg = GetSysColor(COLOR_BTNFACE); - const btn_fg = GetSysColor(COLOR_BTNTEXT); - const hi_bg = GetSysColor(COLOR_HIGHLIGHT); - const hi_fg = GetSysColor(COLOR_HIGHLIGHTTEXT); - const gray = GetSysColor(COLOR_GRAYTEXT); + const win_bg = sys.GetSysColor(c.COLOR_WINDOW); + const win_fg = sys.GetSysColor(c.COLOR_WINDOWTEXT); + const win_frame = sys.GetSysColor(c.COLOR_WINDOWFRAME); + const btn_bg = sys.GetSysColor(c.COLOR_BTNFACE); + const btn_fg = sys.GetSysColor(c.COLOR_BTNTEXT); + const hi_bg = sys.GetSysColor(c.COLOR_HIGHLIGHT); + const hi_fg = sys.GetSysColor(c.COLOR_HIGHLIGHTTEXT); + const gray = sys.GetSysColor(c.COLOR_GRAYTEXT); return .{ .chrome_bg = win_bg, @@ -17001,8 +15569,8 @@ fn isHighContrastActive() bool { .dwFlags = 0, .lpszDefaultScheme = null, }; - if (SystemParametersInfoW(SPI_GETHIGHCONTRAST, @sizeOf(HIGHCONTRASTW), @ptrCast(&hc), 0) != 0) { - return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0; + if (sys.SystemParametersInfoW(c.SPI_GETHIGHCONTRAST, @sizeOf(HIGHCONTRASTW), @ptrCast(&hc), 0) != 0) { + return (hc.dwFlags & c.HCF_HIGHCONTRASTON) != 0; } return false; } @@ -17010,15 +15578,15 @@ fn isHighContrastActive() bool { fn isSystemDarkMode() bool { const subkey = std.unicode.utf8ToUtf16LeStringLiteral("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"); const value_name = std.unicode.utf8ToUtf16LeStringLiteral("AppsUseLightTheme"); - var hkey: usize = 0; - if (RegOpenKeyExW(HKEY_CURRENT_USER, subkey, 0, KEY_READ, &hkey) != ERROR_SUCCESS) return true; - defer _ = RegCloseKey(hkey); + var hkey: sys.HKEY = undefined; + if (sys.RegOpenKeyExW(@ptrFromInt(c.HKEY_CURRENT_USER), subkey, 0, c.KEY_READ, &hkey) != c.ERROR_SUCCESS) return true; + defer _ = sys.RegCloseKey(hkey); var data: u32 = 1; var data_size: DWORD = @sizeOf(u32); var reg_type: DWORD = 0; - if (RegQueryValueExW(hkey, value_name, null, ®_type, @ptrCast(&data), &data_size) != ERROR_SUCCESS) return true; - if (reg_type != REG_DWORD or data_size != @sizeOf(u32)) return true; + if (sys.RegQueryValueExW(hkey, value_name, null, ®_type, @ptrCast(&data), &data_size) != c.ERROR_SUCCESS) return true; + if (reg_type != c.REG_DWORD or data_size != @sizeOf(u32)) return true; return data == 0; // 0 = dark mode, 1 = light mode } @@ -17026,19 +15594,19 @@ fn isSystemDarkMode() bool { fn readDynamicScrollbars(default_value: bool) bool { const subkey = std.unicode.utf8ToUtf16LeStringLiteral("Control Panel\\Accessibility"); const value_name = std.unicode.utf8ToUtf16LeStringLiteral("DynamicScrollbars"); - var hkey: usize = 0; - if (RegOpenKeyExW(HKEY_CURRENT_USER, subkey, 0, KEY_READ, &hkey) != ERROR_SUCCESS) { + var hkey: sys.HKEY = undefined; + if (sys.RegOpenKeyExW(@ptrFromInt(c.HKEY_CURRENT_USER), subkey, 0, c.KEY_READ, &hkey) != c.ERROR_SUCCESS) { return default_value; } - defer _ = RegCloseKey(hkey); + defer _ = sys.RegCloseKey(hkey); var data: u32 = if (default_value) 1 else 0; var data_size: DWORD = @sizeOf(u32); var reg_type: DWORD = 0; - if (RegQueryValueExW(hkey, value_name, null, ®_type, @ptrCast(&data), &data_size) != ERROR_SUCCESS) { + if (sys.RegQueryValueExW(hkey, value_name, null, ®_type, @ptrCast(&data), &data_size) != c.ERROR_SUCCESS) { return default_value; } - if (reg_type != REG_DWORD or data_size != @sizeOf(u32)) return default_value; + if (reg_type != c.REG_DWORD or data_size != @sizeOf(u32)) return default_value; return data != 0; } @@ -17053,15 +15621,15 @@ fn readDynamicScrollbars(default_value: bool) bool { fn resolveSystemAccentColor() ?u32 { const subkey = std.unicode.utf8ToUtf16LeStringLiteral("Software\\Microsoft\\Windows\\DWM"); const value_name = std.unicode.utf8ToUtf16LeStringLiteral("AccentColor"); - var hkey: usize = 0; - if (RegOpenKeyExW(HKEY_CURRENT_USER, subkey, 0, KEY_READ, &hkey) != ERROR_SUCCESS) return null; - defer _ = RegCloseKey(hkey); + var hkey: sys.HKEY = undefined; + if (sys.RegOpenKeyExW(@ptrFromInt(c.HKEY_CURRENT_USER), subkey, 0, c.KEY_READ, &hkey) != c.ERROR_SUCCESS) return null; + defer _ = sys.RegCloseKey(hkey); var data: u32 = 0; var data_size: DWORD = @sizeOf(u32); var reg_type: DWORD = 0; - if (RegQueryValueExW(hkey, value_name, null, ®_type, @ptrCast(&data), &data_size) != ERROR_SUCCESS) return null; - if (reg_type != REG_DWORD or data_size != @sizeOf(u32)) return null; + if (sys.RegQueryValueExW(hkey, value_name, null, ®_type, @ptrCast(&data), &data_size) != c.ERROR_SUCCESS) return null; + if (reg_type != c.REG_DWORD or data_size != @sizeOf(u32)) return null; // Drop alpha; keep 0x00BBGGRR. return data & 0x00FFFFFF; @@ -17084,13 +15652,13 @@ fn shouldUseSystemBackdrop(config: *const configpkg.Config) bool { } fn supportsDwmSystemBackdropAttribute(os_build: u32) bool { - return os_build >= OS_BUILD_WIN11_22H2; + return os_build >= c.OS_BUILD_WIN11_22H2; } fn systemBackdropTypeForBuild(config: *const configpkg.Config, os_build: u32) u32 { - if (!shouldUseSystemBackdrop(config)) return DWMSBT_NONE; - if (!supportsDwmSystemBackdropAttribute(os_build)) return DWMSBT_NONE; - return DWMSBT_TABBEDWINDOW; + if (!shouldUseSystemBackdrop(config)) return c.DWMSBT_NONE; + if (!supportsDwmSystemBackdropAttribute(os_build)) return c.DWMSBT_NONE; + return c.DWMSBT_TABBEDWINDOW; } fn configuredHostWindowPosition(config: *const configpkg.Config) ?struct { x: i32, y: i32 } { @@ -17136,59 +15704,6 @@ fn applyDwmThemeWithBuild(hwnd: HWND, theme: *const ThemeColors, config: *const ); } -/// Linear-interpolate two `COLORREF`-shaped values (`0x00BBGGRR` on -/// Windows GDI) by `alpha` ∈ [0, 1]. `alpha = 0` returns `a`, -/// `alpha = 1` returns `b`. Used for pre-composited alpha on paths -/// where GDI can't render RGBA directly — specifically, `DrawTextW` -/// has no alpha channel, so the text colour must be pre-blended -/// against the surface background for fade effects to look right. -fn blendColorRGB(a: u32, b: u32, alpha: f32) u32 { - const t: f32 = std.math.clamp(alpha, 0.0, 1.0); - const inv: f32 = 1.0 - t; - const ar: u32 = a & 0xFF; - const ag: u32 = (a >> 8) & 0xFF; - const ab: u32 = (a >> 16) & 0xFF; - const br: u32 = b & 0xFF; - const bg: u32 = (b >> 8) & 0xFF; - const bb: u32 = (b >> 16) & 0xFF; - const rr: u32 = @intFromFloat(@as(f32, @floatFromInt(ar)) * inv + @as(f32, @floatFromInt(br)) * t); - const gg: u32 = @intFromFloat(@as(f32, @floatFromInt(ag)) * inv + @as(f32, @floatFromInt(bg)) * t); - const bb2: u32 = @intFromFloat(@as(f32, @floatFromInt(ab)) * inv + @as(f32, @floatFromInt(bb)) * t); - return (bb2 << 16) | (gg << 8) | rr; -} - -fn fillSolidRect(hdc: HDC, rect: RECT, color: u32) void { - const brush = GetStockObject(DC_BRUSH) orelse return; - _ = SetDCBrushColor(hdc, color); - _ = FillRect(hdc, &rect, brush); -} - -fn drawRectBorder(hdc: HDC, rect: RECT, color: u32, thickness: i32) void { - if (rect.right <= rect.left or rect.bottom <= rect.top or thickness <= 0) return; - const stroke = @min(thickness, @min(rect.right - rect.left, rect.bottom - rect.top)); - fillSolidRect(hdc, .{ .left = rect.left, .top = rect.top, .right = rect.right, .bottom = rect.top + stroke }, color); - fillSolidRect(hdc, .{ .left = rect.left, .top = rect.bottom - stroke, .right = rect.right, .bottom = rect.bottom }, color); - fillSolidRect(hdc, .{ .left = rect.left, .top = rect.top + stroke, .right = rect.left + stroke, .bottom = rect.bottom - stroke }, color); - fillSolidRect(hdc, .{ .left = rect.right - stroke, .top = rect.top + stroke, .right = rect.right, .bottom = rect.bottom - stroke }, color); -} - -fn utf16GdiTextLen(text: [:0]const u16) i32 { - const max_len: usize = @intCast(std.math.maxInt(i32)); - return @intCast(@min(text.len, max_len)); -} - -fn textOutWz(hdc: HDC, x: i32, y: i32, text: [:0]const u16) void { - const len = utf16GdiTextLen(text); - if (len == 0) return; - _ = TextOutW(hdc, x, y, text.ptr, len); -} - -fn drawTextWz(hdc: HDC, text: [:0]const u16, rect: *RECT, format: UINT) void { - const len = utf16GdiTextLen(text); - if (len == 0) return; - _ = DrawTextW(hdc, text.ptr, len, rect, format); -} - /// Draw a single-line, left-aligned, vertically-centred, ellipsized /// string into the rect. Used by the palette list row painter. fn drawPaletteRowText(hdc: HDC, text: []const u8, rect: RECT, color: u32) void { @@ -17201,21 +15716,19 @@ fn drawPaletteRowText(hdc: HDC, text: []const u8, rect: RECT, color: u32) void { const n: usize = @min(copied, buf.len - 1); buf[n] = 0; - _ = SetTextColor(hdc, color); + _ = sys.SetTextColor(hdc, color); var r = rect; - _ = DrawTextW( + _ = sys.DrawTextW( hdc, @ptrCast(&buf), @intCast(n), &r, - DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX, + c.DT_LEFT | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_END_ELLIPSIS | c.DT_NOPREFIX, ); } fn getPaletteListHost(hwnd: HWND) ?*Host { - const raw = GetWindowLongPtrW(hwnd, GWLP_USERDATA); - if (raw == 0) return null; - return @ptrFromInt(@as(usize, @intCast(raw))); + return windowData(Host, hwnd); } fn utf16CodeUnitOffsetToUtf8ByteOffset(text: []const u8, target_units: usize) usize { @@ -17279,9 +15792,9 @@ test "Win32 edit caret chooses the active selection endpoint" { fn nativeEditSelection(hwnd: HWND) [2]u32 { var start: u32 = 0; var end: u32 = 0; - _ = SendMessageW( + _ = sys.SendMessageW( hwnd, - EM_GETSEL, + c.EM_GETSEL, @intFromPtr(&start), @bitCast(@intFromPtr(&end)), ); @@ -17303,13 +15816,13 @@ fn editSelectionCaretEndpoint(selection: [2]u32, native_caret: u32) u32 { fn nativeEditCaret(hwnd: HWND, selection: [2]u32) u32 { if (selection[0] == selection[1]) return selection[0]; - if (GetFocus() != hwnd) return selection[1]; + if (sys.GetFocus() != hwnd) return selection[1]; var point: POINT = undefined; - if (GetCaretPos(&point) == 0) return selection[1]; + if (sys.GetCaretPos(&point) == 0) return selection[1]; const x: u16 = @bitCast(@as(i16, @truncate(point.x))); const y: u16 = @bitCast(@as(i16, @truncate(point.y))); const packed_point: LPARAM = @bitCast(@as(usize, x) | (@as(usize, y) << 16)); - const result = SendMessageW(hwnd, EM_CHARFROMPOS, 0, packed_point); + const result = sys.SendMessageW(hwnd, c.EM_CHARFROMPOS, 0, packed_point); const native_caret: u32 = @intCast(@as(usize, @bitCast(result)) & 0xFFFF); return editSelectionCaretEndpoint(selection, native_caret); } @@ -17342,8 +15855,8 @@ fn selectNativeEditRange( ) void { const start = utf8ByteOffsetToUtf16CodeUnitOffset(snapshot_text, range.start); const end = utf8ByteOffsetToUtf16CodeUnitOffset(snapshot_text, range.end); - _ = SendMessageW(hwnd, EM_SETSEL, start, @intCast(end)); - _ = SetFocus(hwnd); + _ = sys.SendMessageW(hwnd, c.EM_SETSEL, start, @intCast(end)); + _ = sys.SetFocus(hwnd); } fn overlayEditNameThunk(ctx: *anyopaque, buf: []u8) []const u8 { @@ -17376,7 +15889,7 @@ fn overlayEditSnapshotThunk(ctx: *anyopaque, alloc: Allocator) !win32_uia.Termin fn overlayEditFocusedThunk(ctx: *anyopaque) bool { const host: *const Host = @ptrCast(@alignCast(ctx)); const hwnd = host.overlay_edit_hwnd orelse return false; - return GetFocus() == hwnd; + return sys.GetFocus() == hwnd; } fn overlayEditSetValueThunk(ctx: *anyopaque, value: []const u8) !void { @@ -17384,7 +15897,7 @@ fn overlayEditSetValueThunk(ctx: *anyopaque, value: []const u8) !void { const hwnd = host.overlay_edit_hwnd orelse return error.ElementNotAvailable; const value_w = try std.unicode.utf8ToUtf16LeAllocZ(host.app.core_app.alloc, value); defer host.app.core_app.alloc.free(value_w); - if (SetWindowTextW(hwnd, value_w.ptr) == 0) return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.SetWindowTextW(hwnd, value_w.ptr) == 0) return lastError(); } fn overlayEditSelectRangeThunk( @@ -17419,7 +15932,7 @@ fn searchEditSnapshotThunk(ctx: *anyopaque, alloc: Allocator) !win32_uia.Termina fn searchEditFocusedThunk(ctx: *anyopaque) bool { const surface: *const Surface = @ptrCast(@alignCast(ctx)); const hwnd = surface.search_bar_edit_hwnd orelse return false; - return GetFocus() == hwnd; + return sys.GetFocus() == hwnd; } fn searchEditSetValueThunk(ctx: *anyopaque, value: []const u8) !void { @@ -17427,7 +15940,7 @@ fn searchEditSetValueThunk(ctx: *anyopaque, value: []const u8) !void { const hwnd = surface.search_bar_edit_hwnd orelse return error.ElementNotAvailable; const value_w = try std.unicode.utf8ToUtf16LeAllocZ(surface.app.core_app.alloc, value); defer surface.app.core_app.alloc.free(value_w); - if (SetWindowTextW(hwnd, value_w.ptr) == 0) return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.SetWindowTextW(hwnd, value_w.ptr) == 0) return lastError(); } fn searchEditSelectRangeThunk( @@ -17488,7 +16001,7 @@ fn paletteListGeometryThunk(ctx: *anyopaque) ?win32_uia.PaletteListGeometry { const host: *const Host = @ptrCast(@alignCast(ctx)); const hwnd = host.palette_list_hwnd orelse return null; var rect: RECT = undefined; - if (GetWindowRect(hwnd, &rect) == 0) return null; + if (sys.GetWindowRect(hwnd, &rect) == 0) return null; const row_height = host.scaled(palette_row_height); if (row_height <= 0 or rect.right <= rect.left or rect.bottom <= rect.top) return null; const remaining = host.palette_list_ranked_count -| host.palette_list_scroll; @@ -17771,20 +16284,20 @@ fn surfaceConfirmWriteAccept(userdata: ?*anyopaque) void { const alloc = surface.app.core_app.alloc; surface.pending_clipboard_op = null; defer { - for (pending.contents) |*c| { - alloc.free(c.mime); - alloc.free(c.data); + for (pending.contents) |*content| { + alloc.free(content.mime); + alloc.free(content.data); } alloc.free(pending.contents); } if (pending.clipboard != .standard) return; var html_content: ?[]const u8 = null; var plain_content: ?[]const u8 = null; - for (pending.contents) |c| { - if (std.mem.eql(u8, c.mime, "text/html")) { - html_content = c.data; - } else if (std.mem.eql(u8, c.mime, "text/plain")) { - plain_content = c.data; + for (pending.contents) |content| { + if (std.mem.eql(u8, content.mime, "text/html")) { + html_content = content.data; + } else if (std.mem.eql(u8, content.mime, "text/plain")) { + plain_content = content.data; } } if (html_content) |html| { @@ -17915,14 +16428,14 @@ fn rectEqual(a: RECT, b: RECT) bool { a.bottom == b.bottom; } -fn monitorInfo(monitor: ?*anyopaque) ?win32_quick_terminal.MonitorInfo { +fn monitorInfo(monitor: sys.HMONITOR) ?win32_quick_terminal.MonitorInfo { var info: MONITORINFO = .{ .cbSize = @sizeOf(MONITORINFO), .rcMonitor = .{ .left = 0, .top = 0, .right = 0, .bottom = 0 }, .rcWork = .{ .left = 0, .top = 0, .right = 0, .bottom = 0 }, .dwFlags = 0, }; - if (GetMonitorInfoW(monitor, &info) == 0) return null; + if (sys.GetMonitorInfoW(monitor, &info) == 0) return null; return .{ .work_area = .{ @@ -17941,11 +16454,11 @@ fn monitorInfo(monitor: ?*anyopaque) ?win32_quick_terminal.MonitorInfo { } const PrimaryMonitorSearch = struct { - found: ?*anyopaque = null, + found: ?sys.HMONITOR = null, }; fn primaryMonitorEnumProc( - monitor: ?*anyopaque, + monitor: sys.HMONITOR, _: HDC, _: *RECT, data: LPARAM, @@ -17957,21 +16470,21 @@ fn primaryMonitorEnumProc( .rcWork = .{ .left = 0, .top = 0, .right = 0, .bottom = 0 }, .dwFlags = 0, }; - if (GetMonitorInfoW(monitor, &info) == 0) return 1; - if ((info.dwFlags & MONITORINFOF_PRIMARY) == 0) return 1; + if (sys.GetMonitorInfoW(monitor, &info) == 0) return 1; + if ((info.dwFlags & c.MONITORINFOF_PRIMARY) == 0) return 1; search.found = monitor; return 0; } -fn primaryMonitor() ?*anyopaque { +fn primaryMonitor() ?sys.HMONITOR { var search: PrimaryMonitorSearch = .{}; - _ = EnumDisplayMonitors( + _ = sys.EnumDisplayMonitors( null, null, primaryMonitorEnumProc, @as(LPARAM, @bitCast(@intFromPtr(&search))), ); - return search.found orelse MonitorFromPoint(.{ .x = 0, .y = 0 }, MONITOR_DEFAULTTOPRIMARY); + return search.found orelse sys.MonitorFromPoint(.{ .x = 0, .y = 0 }, c.MONITOR_DEFAULTTOPRIMARY); } const AllMonitorsSearch = struct { @@ -17986,7 +16499,7 @@ fn isVisibleMonitorInfo(info: win32_quick_terminal.MonitorInfo) bool { } fn allMonitorsEnumProc( - monitor: ?*anyopaque, + monitor: sys.HMONITOR, _: HDC, _: *RECT, data: LPARAM, @@ -18010,7 +16523,7 @@ fn allMonitorsEnumProc( fn allMonitorsInfo() ?win32_quick_terminal.MonitorInfo { var search: AllMonitorsSearch = .{}; - if (EnumDisplayMonitors( + if (sys.EnumDisplayMonitors( null, null, allMonitorsEnumProc, @@ -18457,8 +16970,8 @@ fn stripHtmlTags(alloc: std.mem.Allocator, html: []const u8) ![]u8 { try buf.ensureTotalCapacity(alloc, html.len); var i: usize = 0; while (i < html.len) { - const c = html[i]; - if (c == '<') { + const char = html[i]; + if (char == '<') { if (std.mem.indexOfScalarPos(u8, html, i, '>')) |end| { i = end + 1; continue; @@ -18466,7 +16979,7 @@ fn stripHtmlTags(alloc: std.mem.Allocator, html: []const u8) ![]u8 { try buf.appendSlice(alloc, html[i..]); break; } - try buf.append(alloc, c); + try buf.append(alloc, char); i += 1; } return buf.toOwnedSlice(alloc); @@ -18555,30 +17068,6 @@ test "win32 appendOwnedString leaves target untouched on OOM" { try testing.expectEqualStrings("warm-up", target.?); } -fn rectIntersects(a: RECT, b: RECT) bool { - return a.left < b.right and - a.right > b.left and - a.top < b.bottom and - a.bottom > b.top; -} - -test "win32 rectIntersects only trips on positive overlap" { - const testing = std.testing; - - try testing.expect(rectIntersects( - .{ .left = 0, .top = 0, .right = 10, .bottom = 10 }, - .{ .left = 5, .top = 5, .right = 15, .bottom = 15 }, - )); - try testing.expect(!rectIntersects( - .{ .left = 0, .top = 0, .right = 10, .bottom = 10 }, - .{ .left = 10, .top = 0, .right = 20, .bottom = 10 }, - )); - try testing.expect(!rectIntersects( - .{ .left = 0, .top = 0, .right = 10, .bottom = 10 }, - .{ .left = 0, .top = 10, .right = 10, .bottom = 20 }, - )); -} - test "win32 palette EN_CHANGE re-entry guard: suppress_edit_events gates sync cascade" { // Sentinel test — the Host struct MUST carry the // `suppress_edit_events: bool = false` flag. The palette edit @@ -18598,13 +17087,13 @@ fn paletteListProc( lParam: LPARAM, ) callconv(.winapi) LRESULT { switch (msg) { - WM_ERASEBKGND => return 1, - WM_PAINT => { + c.WM_ERASEBKGND => return 1, + c.WM_PAINT => { if (getPaletteListHost(hwnd)) |host| host.paintPaletteList(); return 0; }, - WM_LBUTTONDOWN => { - const host = getPaletteListHost(hwnd) orelse return DefWindowProcW(hwnd, msg, wParam, lParam); + c.WM_LBUTTONDOWN => { + const host = getPaletteListHost(hwnd) orelse return sys.DefWindowProcW(hwnd, msg, wParam, lParam); const y: i32 = signedHighWord(lParamBits(lParam)); const row = host.paletteRowAtY(y); if (row) |r| host.invokePaletteRow(r) catch |err| { @@ -18612,14 +17101,14 @@ fn paletteListProc( }; return 0; }, - WM_MOUSEWHEEL => { - const host = getPaletteListHost(hwnd) orelse return DefWindowProcW(hwnd, msg, wParam, lParam); + c.WM_MOUSEWHEEL => { + const host = getPaletteListHost(hwnd) orelse return sys.DefWindowProcW(hwnd, msg, wParam, lParam); host.scrollPaletteList(wheelDeltaFromWParam(wParam)); return 0; }, - WM_GETOBJECT => { - const host = getPaletteListHost(hwnd) orelse return DefWindowProcW(hwnd, msg, wParam, lParam); - if (!host.app.com_initialized) return DefWindowProcW(hwnd, msg, wParam, lParam); + c.WM_GETOBJECT => { + const host = getPaletteListHost(hwnd) orelse return sys.DefWindowProcW(hwnd, msg, wParam, lParam); + if (!host.app.com_initialized) return sys.DefWindowProcW(hwnd, msg, wParam, lParam); if (host.palette_list_uia_provider) |provider| { if (win32_uia.returnPaletteListProvider( hwnd, @@ -18627,7 +17116,7 @@ fn paletteListProc( lParam, provider, )) |lr| return lr; - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); } const state = host.paletteListUiaState(); if (win32_uia.handlePaletteListGetObject( @@ -18637,9 +17126,9 @@ fn paletteListProc( lParam, state, )) |lr| return lr; - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - else => return DefWindowProcW(hwnd, msg, wParam, lParam), + else => return sys.DefWindowProcW(hwnd, msg, wParam, lParam), } } @@ -18651,599 +17140,133 @@ fn scrollbarProc( ) callconv(.winapi) LRESULT { const surface = getSurface(hwnd); switch (msg) { - WM_ERASEBKGND => return 1, - WM_PAINT => { + c.WM_ERASEBKGND => return 1, + c.WM_PAINT => { if (surface) |value| value.paintScrollbar(); return 0; }, - WM_MOUSEMOVE => { + c.WM_MOUSEMOVE => { if (surface) |value| value.handleScrollbarMouseMove(hwnd, lParam); return 0; }, - WM_MOUSELEAVE => { + c.WM_MOUSELEAVE => { if (surface) |value| value.handleScrollbarMouseLeave(); return 0; }, - WM_LBUTTONDOWN => { + c.WM_LBUTTONDOWN => { if (surface) |value| value.handleScrollbarLeftButtonDown(hwnd, lParam); return 0; }, - WM_LBUTTONUP => { + c.WM_LBUTTONUP => { if (surface) |value| value.handleScrollbarLeftButtonUp(lParam); return 0; }, - WM_CAPTURECHANGED => { + c.WM_CAPTURECHANGED => { if (surface) |value| value.handleScrollbarCaptureChanged(); return 0; }, - WM_SETCURSOR => { - _ = SetCursor(LoadCursorW(null, IDC_ARROW)); + c.WM_SETCURSOR => { + _ = sys.SetCursor(sys.LoadCursorW(null, c.IDC_ARROW)); return 1; }, - else => return DefWindowProcW(hwnd, msg, wParam, lParam), + else => return sys.DefWindowProcW(hwnd, msg, wParam, lParam), } } -fn drawRoundedRect(hdc: HDC, rect: RECT, bg: u32, border: u32, radius: i32) void { - const stock_brush = GetStockObject(DC_BRUSH) orelse return; - const stock_pen = GetStockObject(DC_PEN) orelse return; - _ = SetDCBrushColor(hdc, bg); - _ = SetDCPenColor(hdc, border); - const old_brush = SelectObject(hdc, stock_brush); - const old_pen = SelectObject(hdc, stock_pen); - // Inset by 1px — GDI strokes are centered, prevents clipping at edges - _ = RoundRect(hdc, rect.left + 1, rect.top + 1, rect.right - 1, rect.bottom - 1, radius, radius); - _ = SelectObject(hdc, old_pen); - _ = SelectObject(hdc, old_brush); - // Stock objects — never delete -} +const tabButtonKeyAction = labels.tabButtonKeyAction; -fn paintRectVisible(hdc: HDC, paint_rect: RECT, rect: RECT) bool { - if (rect.right <= rect.left or rect.bottom <= rect.top) return false; - if (!rectIntersects(paint_rect, rect)) return false; - return RectVisible(hdc, &rect) != 0; -} +const moveTabAmountToEdge = labels.moveTabAmountToEdge; -fn tabButtonKeyAction(vk: WPARAM, ctrl_pressed: bool) ?TabButtonKeyAction { - return switch (vk) { - VK_LEFT => if (ctrl_pressed) .move_previous else .previous, - VK_RIGHT => if (ctrl_pressed) .move_next else .next, - VK_HOME => if (ctrl_pressed) .move_first else .first, - VK_END => if (ctrl_pressed) .move_last else .last, - VK_F2 => .rename, - VK_DELETE => .close, - VK_APPS => .overview, - else => null, - }; -} +const searchButtonKeyAction = labels.searchButtonKeyAction; -fn moveTabAmountToEdge(total: usize, current: usize, toward_start: bool) isize { - if (total <= 1 or current >= total) return 0; - if (toward_start) return -@as(isize, @intCast(current)); - return @as(isize, @intCast((total - 1) - current)); -} +const dockedSearchCoreDirectionFromKeyAction = labels.dockedSearchCoreDirectionFromKeyAction; -fn searchButtonKeyAction(vk: WPARAM, shift_pressed: bool) ?SearchButtonKeyAction { - return switch (vk) { - VK_F3 => if (shift_pressed) .previous else .next, - VK_ESCAPE => .dismiss, - else => null, - }; -} +const dockedSearchButtonDirection = labels.dockedSearchButtonDirection; -fn dockedSearchCoreDirectionFromKeyAction(action: SearchButtonKeyAction) ?input.Binding.Action.NavigateSearch { - return switch (action) { - .previous => .previous, - .next => .next, - .dismiss => null, - }; -} +const dockedSearchEnterDirection = labels.dockedSearchEnterDirection; -fn dockedSearchButtonDirection(command_id: usize) ?input.Binding.Action.NavigateSearch { - // Core scrollback search indexes matches newest-first - // (`.previous` => newer/down, `.next` => older/up). The docked - // search bar is laid out as visible up/down navigation, so the - // button IDs map to the opposite core enum. - return switch (command_id) { - SEARCH_PREV_ID => .next, - SEARCH_NEXT_ID => .previous, - else => null, - }; -} +const dockedSearchArrowDirection = labels.dockedSearchArrowDirection; -fn dockedSearchEnterDirection(shift_pressed: bool) input.Binding.Action.NavigateSearch { - return dockedSearchArrowDirection(if (shift_pressed) VK_UP else VK_DOWN).?; -} +const searchSelectedRawFromCoreDisplay = labels.searchSelectedRawFromCoreDisplay; -fn dockedSearchArrowDirection(vk: WPARAM) ?input.Binding.Action.NavigateSearch { - return switch (vk) { - VK_UP => dockedSearchButtonDirection(SEARCH_PREV_ID), - VK_DOWN => dockedSearchButtonDirection(SEARCH_NEXT_ID), - else => null, - }; -} +const searchSelectedDisplayFromRaw = labels.searchSelectedDisplayFromRaw; -fn searchDirectionFromWheelDelta(delta: i16) input.Binding.Action.NavigateSearch { - return if (delta > 0) - .next - else - .previous; -} +const advanceSearchSelectedRaw = labels.advanceSearchSelectedRaw; -fn searchSelectedRawFromCoreDisplay(selected: ?usize) ?usize { - const value = selected orelse return null; - if (value == 0) return null; - return value - 1; -} +const searchBarSearchedStateForTotal = labels.searchBarSearchedStateForTotal; -fn searchSelectedDisplayFromRaw(raw: ?usize, total: ?usize) ?usize { - const idx = raw orelse return null; - const count = total orelse return null; - if (count == 0 or idx >= count) return null; - return idx + 1; -} +const searchBarSearchedStateForSelected = labels.searchBarSearchedStateForSelected; -fn advanceSearchSelectedRaw( - raw: ?usize, - total: ?usize, - dir: input.Binding.Action.NavigateSearch, - wrap: bool, -) ?usize { - const idx = raw orelse return null; - const count = total orelse return null; - if (count == 0 or idx >= count) return null; - const last = count - 1; - return switch (dir) { - .next => if (idx >= last) if (wrap) 0 else idx else idx + 1, - .previous => if (idx == 0) if (wrap) last else 0 else idx - 1, - }; -} +const searchBarDisplayStateChanged = labels.searchBarDisplayStateChanged; -fn searchBarSearchedStateForTotal(total: ?usize) bool { - return total != null; -} +const profileChromeVisible = labels.profileChromeVisible; -fn searchBarSearchedStateForSelected(selected: ?usize, total: ?usize) bool { - return selected != null or total != null; -} +const profileChromeNeedsFullTextInvalidation = labels.profileChromeNeedsFullTextInvalidation; -fn searchBarDisplayStateChanged( - search_bar: *const win32_search_bar.SearchBar, - searched: bool, - total: ?usize, - selected: ?usize, -) bool { - return search_bar.searched != searched or - search_bar.total != total or - search_bar.selected != selected; -} +const chromeTextNeedsFullInvalidation = labels.chromeTextNeedsFullInvalidation; -fn profileChromeVisible(overlay_mode: HostOverlayMode, status_bar_height: i32) bool { - return win32_chrome_state.profileVisible(overlay_mode, status_bar_height); -} +const inspectorVisibilityChangeNeedsHostRelayout = labels.inspectorVisibilityChangeNeedsHostRelayout; -fn profileChromeNeedsFullTextInvalidation(overlay_mode: HostOverlayMode, status_bar_height: i32) bool { - return win32_chrome_state.profileNeedsFullTextInvalidation(overlay_mode, status_bar_height); -} +const inspectorPanelVisibleForState = labels.inspectorPanelVisibleForState; -fn chromeTextNeedsFullInvalidation(status_bar_height: i32) bool { - return win32_chrome_state.textNeedsFullInvalidation(status_bar_height); -} +const layoutChildPaintPlan = labels.layoutChildPaintPlan; -fn inspectorVisibilityChangeNeedsHostRelayout(changed: bool) bool { - return changed; -} +const overlayAcceptButtonVisible = labels.overlayAcceptButtonVisible; -fn inspectorPanelVisibleForState(overlay_mode: HostOverlayMode, active_tab_has_inspector: bool) bool { - return overlay_mode == .none and active_tab_has_inspector; -} +const overlayEditFrameVisible = labels.overlayEditFrameVisible; -fn layoutChildPaintPlan(chrome_changed: bool, content_changed: bool) LayoutChildPaintPlan { - return win32_chrome_state.layoutChildPaintPlan(chrome_changed, content_changed); -} +const OverlayFocusSlot = labels.OverlayFocusSlot; -fn overlayAcceptButtonVisible(mode: HostOverlayMode) bool { - return mode != .command_palette; -} +const nextVisibleOverlayFocusSlot = labels.nextVisibleOverlayFocusSlot; -fn overlayEditFrameVisible(mode: HostOverlayMode) bool { - return mode != .confirm; -} +const inspectorChromeVisible = labels.inspectorChromeVisible; -const OverlayFocusSlot = enum { edit, accept, cancel }; +const inspectorBannerStateChanged = labels.inspectorBannerStateChanged; -fn nextOverlayFocusSlot(mode: HostOverlayMode, current: OverlayFocusSlot, reverse: bool) OverlayFocusSlot { - if (mode == .confirm) return if (current == .accept) .cancel else .accept; - if (!overlayAcceptButtonVisible(mode)) return if (current == .edit) .cancel else .edit; - return if (reverse) - switch (current) { - .edit => .cancel, - .accept => .edit, - .cancel => .accept, - } - else switch (current) { - .edit => .accept, - .accept => .cancel, - .cancel => .edit, - }; -} +const windowTitleSyncChanged = labels.windowTitleSyncChanged; -fn overlayFocusSlotVisible( - slot: OverlayFocusSlot, - edit_visible: bool, - accept_visible: bool, - cancel_visible: bool, -) bool { - return switch (slot) { - .edit => edit_visible, - .accept => accept_visible, - .cancel => cancel_visible, - }; -} +const scrollStatusTextChanged = labels.scrollStatusTextChanged; -fn nextVisibleOverlayFocusSlot( - mode: HostOverlayMode, - current: OverlayFocusSlot, - reverse: bool, - edit_visible: bool, - accept_visible: bool, - cancel_visible: bool, -) ?OverlayFocusSlot { - var candidate = current; - for (0..3) |_| { - candidate = nextOverlayFocusSlot(mode, candidate, reverse); - if (candidate != current and overlayFocusSlotVisible( - candidate, - edit_visible, - accept_visible, - cancel_visible, - )) return candidate; - } - return null; -} +const bindingActionsToggleCommandPalette = labels.bindingActionsToggleCommandPalette; -fn inspectorChromeVisible(overlay_mode: HostOverlayMode, status_bar_height: i32) bool { - return win32_chrome_state.inspectorVisible(overlay_mode, status_bar_height); -} +const profileShortcutIndexFromKey = labels.profileShortcutIndexFromKey; -fn inspectorBannerStateChanged( - overlay_mode: HostOverlayMode, - banner_kind: HostBannerKind, - banner_text: ?[:0]const u8, - visible: bool, -) bool { - if (overlay_mode != .none) return false; - if (visible) return banner_kind != .none or banner_text != null; - return banner_kind != .info or !ownedStringEquals(banner_text, host_banner_inspector_inactive); -} +const quickSlotShortcutProfileIndex = labels.quickSlotShortcutProfileIndex; -fn windowTitleSyncChanged(current: ?[:0]const u8, next: []const u8) bool { - return !ownedStringEquals(current, next); -} +const quickSlotPinOrdinalFromKey = labels.quickSlotPinOrdinalFromKey; -const ScrollStatusKey = struct { - visible: bool, - percent: usize, -}; +const clearQuickSlotPinsRequested = labels.clearQuickSlotPinsRequested; -fn scrollStatusKey(scrollbar: terminal.Scrollbar) ScrollStatusKey { - const visible = scrollbar.total > scrollbar.len and - scrollbar.offset + scrollbar.len < scrollbar.total; - const percent = if (visible and scrollbar.total > 0) - (scrollbar.offset * 100) / scrollbar.total - else - 0; - return .{ - .visible = visible, - .percent = percent, - }; -} +const ProfileSelection = labels.ProfileSelection; -fn scrollStatusTextChanged(previous: terminal.Scrollbar, next: terminal.Scrollbar) bool { - return !std.meta.eql(scrollStatusKey(previous), scrollStatusKey(next)); -} +const startsWithIgnoreCase = labels.startsWithIgnoreCase; -fn tabsButtonKeyAction(vk: WPARAM) ?TabsButtonKeyAction { - return switch (vk) { - VK_LEFT, VK_UP => .previous, - VK_RIGHT, VK_DOWN => .next, - VK_F2 => .rename, - VK_APPS => .overview, - else => null, - }; -} +const resolveProfileSelection = labels.resolveProfileSelection; -fn commandButtonKeyAction(vk: WPARAM) ?CommandButtonKeyAction { - return switch (vk) { - VK_RETURN, VK_SPACE => .toggle, - VK_UP => .previous, - VK_DOWN => .next, - VK_ESCAPE => .dismiss, - else => null, - }; -} +const profileIndexByKey = labels.profileIndexByKey; -fn bindingActionsToggleCommandPalette(actions: []const input.Binding.Action) bool { - for (actions) |action| switch (action) { - .toggle_command_palette => return true, - else => {}, - }; - return false; +fn applyProfileConfigByKey( + config: *configpkg.Config, + profiles: []const windows_shell.Profile, + key: []const u8, +) !void { + const index = profileIndexByKey(profiles, key) orelse return; + try applyProfileSurfaceConfig(config, &profiles[index]); } -fn commandPaletteDirectionFromWheelDelta(delta: i16) bool { - return delta > 0; -} +const preferredProfileIndex = labels.preferredProfileIndex; -fn profileDirectionFromWheelDelta(delta: i16) bool { - return delta > 0; -} +const formatProgressStatus = labels.formatProgressStatus; -fn profileShortcutIndexFromKey(vk: WPARAM) ?usize { - if (vk >= @as(WPARAM, '1') and vk <= @as(WPARAM, '9')) { - return @as(usize, @intCast(vk - @as(WPARAM, '1'))); - } - if (vk >= 0x61 and vk <= 0x69) { - return @as(usize, @intCast(vk - 0x61)); - } - return null; -} +const resolveWindowBaseTitle = labels.resolveWindowBaseTitle; -fn quickSlotShortcutProfileIndex( - profiles_len: usize, - selected_index: ?usize, - vk: WPARAM, - alt_pressed: bool, -) ?usize { - if (!alt_pressed) return null; - const slot_ordinal = profileShortcutIndexFromKey(vk) orelse return null; - if (slot_ordinal >= 3) return null; - return quickSlotProfileIndex(profiles_len, selected_index, slot_ordinal, 3); -} +const normalizedBackgroundOpacity = labels.normalizedBackgroundOpacity; -fn quickSlotPinOrdinalFromKey(vk: WPARAM, alt_pressed: bool, shift_pressed: bool) ?usize { - if (!alt_pressed or !shift_pressed) return null; - const slot_ordinal = profileShortcutIndexFromKey(vk) orelse return null; - if (slot_ordinal >= 3) return null; - return slot_ordinal; -} +const effectiveBackgroundOpacity = labels.effectiveBackgroundOpacity; -fn clearQuickSlotPinsRequested(vk: WPARAM, alt_pressed: bool, shift_pressed: bool) bool { - if (!alt_pressed or !shift_pressed) return false; - return vk == VK_0 or vk == VK_NUMPAD0; -} +const alphaByteForOpacity = labels.alphaByteForOpacity; -fn quickSlotFocusKeyAction(vk: WPARAM) ?QuickSlotFocusKeyAction { - return switch (vk) { - VK_LEFT, VK_UP => .previous, - VK_RIGHT, VK_DOWN => .next, - VK_HOME => .first, - VK_END => .last, - VK_RETURN => .open, - else => null, - }; -} - -fn profilesButtonKeyAction(vk: WPARAM) ?ProfilesButtonKeyAction { - return switch (vk) { - VK_RETURN => .open, - VK_SPACE, VK_APPS => .toggle, - VK_LEFT, VK_UP => .previous, - VK_RIGHT, VK_DOWN => .next, - VK_HOME => .first, - VK_END => .last, - else => null, - }; -} - -const ProfileSelection = union(enum) { - exact: usize, - ambiguous: usize, - invalid, -}; - -fn startsWithIgnoreCase(haystack: []const u8, prefix: []const u8) bool { - if (prefix.len > haystack.len) return false; - for (haystack[0..prefix.len], prefix) |a, b| { - if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false; - } - return true; -} - -fn resolveProfileSelection( - profiles: []const windows_shell.Profile, - input_text: []const u8, - fallback_index: usize, -) ProfileSelection { - if (profiles.len == 0) return .invalid; - const fallback = @min(fallback_index, profiles.len - 1); - if (input_text.len == 0) return .{ .exact = fallback }; - - const requested = std.fmt.parseUnsigned(usize, input_text, 10) catch null; - if (requested) |value| { - if (value == 0 or value > profiles.len) return .invalid; - return .{ .exact = value - 1 }; - } - - var exact_match: ?usize = null; - var unique_match: ?usize = null; - var match_count: usize = 0; - for (profiles, 0..) |profile, index| { - if (std.ascii.eqlIgnoreCase(profile.key, input_text) or - std.ascii.eqlIgnoreCase(profile.label, input_text)) - { - exact_match = index; - break; - } - if (startsWithIgnoreCase(profile.key, input_text) or - startsWithIgnoreCase(profile.label, input_text)) - { - unique_match = index; - match_count += 1; - } - } - if (exact_match) |index| return .{ .exact = index }; - if (match_count == 1) return .{ .exact = unique_match.? }; - if (match_count > 1) return .{ .ambiguous = match_count }; - return .invalid; -} - -fn profileIndexByKey(profiles: []const windows_shell.Profile, key: []const u8) ?usize { - for (profiles, 0..) |profile, index| { - if (std.ascii.eqlIgnoreCase(profile.key, key)) return index; - } - return null; -} - -fn applyProfileConfigByKey( - config: *configpkg.Config, - profiles: []const windows_shell.Profile, - key: []const u8, -) !void { - const index = profileIndexByKey(profiles, key) orelse return; - try applyProfileSurfaceConfig(config, &profiles[index]); -} - -fn preferredProfileIndex( - profiles: []const windows_shell.Profile, - selected_key: ?[]const u8, - app_key: ?[]const u8, - hint: ?[]const u8, - fallback_index: usize, -) ?usize { - if (profiles.len == 0) return null; - - const preferred_key = if (selected_key) |key| - key - else if (app_key) |key| - key - else - null; - if (preferred_key) |key| { - for (profiles, 0..) |profile, index| { - if (std.ascii.eqlIgnoreCase(profile.key, key)) return index; - } - } - - if (hint) |value| { - return switch (resolveProfileSelection(profiles, value, fallback_index)) { - .exact => |index| index, - .ambiguous, .invalid => null, - }; - } - - return null; -} - -fn formatProgressStatus( - alloc: Allocator, - value: win32_taskbar_progress.ProgressReport, -) !?[]u8 { - const progress = win32_taskbar_progress.clampPercent(value.progress); - return switch (value.state) { - .remove => null, - .set => if (progress) |value_| - try std.fmt.allocPrint(alloc, "progress:{d}%", .{value_}) - else - try alloc.dupe(u8, "progress"), - .@"error" => if (progress) |value_| - try std.fmt.allocPrint(alloc, "progress error:{d}%", .{value_}) - else - try alloc.dupe(u8, "progress error"), - .indeterminate => try alloc.dupe(u8, "progress:busy"), - .pause => if (progress) |value_| - try std.fmt.allocPrint(alloc, "progress paused:{d}%", .{value_}) - else - try alloc.dupe(u8, "progress paused"), - }; -} - -fn buildWindowTitle( - alloc: Allocator, - base_title: ?[]const u8, - status: SurfaceStatus, -) ![]u8 { - var buf: std.ArrayListUnmanaged(u8) = .empty; - errdefer buf.deinit(alloc); - - try buf.appendSlice(alloc, base_title orelse "noctty"); - - const appendStatus = struct { - fn call( - list: *std.ArrayListUnmanaged(u8), - alloc_: Allocator, - comptime fmt: []const u8, - args: anytype, - ) !void { - try list.appendSlice(alloc_, " | "); - try list.writer(alloc_).print(fmt, args); - } - }.call; - - if (status.readonly) try appendStatus(&buf, alloc, "readonly", .{}); - if (status.secure_input) try appendStatus(&buf, alloc, "sensitive", .{}); - if (status.key_sequence_active) try appendStatus(&buf, alloc, "keys", .{}); - if (status.key_table_name) |name| try appendStatus(&buf, alloc, "table:{s}", .{name}); - if (status.pwd) |pwd| try appendStatus(&buf, alloc, "cwd:{s}", .{pwd}); - if (status.search.active) { - if (status.search.needle) |needle| { - if (status.search.selected) |selected| { - if (status.search.total) |total| { - try appendStatus(&buf, alloc, "find:{s} ({d}/{d})", .{ needle, selected, total }); - } else { - try appendStatus(&buf, alloc, "find:{s} ({d})", .{ needle, selected }); - } - } else if (status.search.total) |total| { - try appendStatus(&buf, alloc, "find:{s} ({d})", .{ needle, total }); - } else { - try appendStatus(&buf, alloc, "find:{s}", .{needle}); - } - } else { - try appendStatus(&buf, alloc, "find", .{}); - } - } - if (status.progress) |progress| try appendStatus(&buf, alloc, "{s}", .{progress}); - - return buf.toOwnedSlice(alloc); -} - -fn resolveWindowBaseTitle( - terminal_title: ?[:0]const u8, - surface_override: ?[:0]const u8, - tab_override: ?[:0]const u8, -) ?[:0]const u8 { - return tab_override orelse surface_override orelse terminal_title; -} - -fn normalizedBackgroundOpacity(value: f64) f64 { - return std.math.clamp(value, 0.0, 1.0); -} - -fn effectiveBackgroundOpacity(configured: f64, force_opaque: bool) f64 { - return if (force_opaque) 1.0 else configured; -} - -fn alphaByteForOpacity(value: f64) u8 { - return @intFromFloat(@round(normalizedBackgroundOpacity(value) * 255.0)); -} - -fn hiddenScrollbarAlphaByte() u8 { - // Keep the layered child hit-testable while visually hidden. - // Windows won't deliver real hover input to an alpha-0 layered - // child, so a true zero here strands the dynamic scrollbar in the - // hidden state until some non-mouse activity wakes it up. - return 1; -} - -const ResizeSplitFallbackDelta = struct { - width: i32 = 0, - height: i32 = 0, -}; - -const SurfaceOrderEntry = struct { - host_id: u32, - host_active: bool, -}; +const hiddenScrollbarAlphaByte = labels.hiddenScrollbarAlphaByte; fn splitTreeEquivalent(a: *const SplitTreeSurface, b: *const SplitTreeSurface) bool { if (a.zoomed != b.zoomed) return false; @@ -19270,15 +17293,7 @@ fn splitTreeEquivalent(a: *const SplitTreeSurface, b: *const SplitTreeSurface) b return true; } -fn resizeSplitFallbackDelta(value: apprt.action.ResizeSplit) ResizeSplitFallbackDelta { - const amount: i32 = @intCast(value.amount); - return switch (value.direction) { - .left => .{ .width = -amount }, - .right => .{ .width = amount }, - .up => .{ .height = -amount }, - .down => .{ .height = amount }, - }; -} +const resizeSplitFallbackDelta = labels.resizeSplitFallbackDelta; fn splitDirectionFromAction(value: apprt.action.SplitDirection) SplitTreeSurface.Split.Direction { return switch (value) { @@ -19289,176 +17304,24 @@ fn splitDirectionFromAction(value: apprt.action.SplitDirection) SplitTreeSurface }; } -fn nextInspectorVisible(current: bool, mode: apprt.action.Inspector) bool { - return switch (mode) { - .toggle => !current, - .show => true, - .hide => false, - }; -} - -fn nextTabInspectorVisible(active_tab_has_inspector: bool, mode: apprt.action.Inspector) bool { - return nextInspectorVisible(active_tab_has_inspector, mode); -} - -fn primarySurfaceIndex(entries: []const SurfaceOrderEntry) ?usize { - if (entries.len == 0) return null; - const first_host_id = entries[0].host_id; - for (entries, 0..) |entry, i| { - if (entry.host_id == first_host_id and entry.host_active) return i; - } - return 0; -} - -fn buildHostAwareBaseTitle( - alloc: Allocator, - base_title: ?[]const u8, - host: HostTabStatus, -) ![]u8 { - if (host.total <= 1) return try alloc.dupe(u8, base_title orelse "noctty"); - return try std.fmt.allocPrint( - alloc, - "[{d}/{d}] {s}", - .{ host.index + 1, host.total, base_title orelse "noctty" }, - ); -} - -fn compactHostLabel( - alloc: Allocator, - value: []const u8, - max_len: usize, -) ![]u8 { - if (value.len <= max_len) return try alloc.dupe(u8, value); - if (max_len <= 3) return try alloc.dupe(u8, "..."); - return try std.fmt.allocPrint(alloc, "{s}...", .{value[0 .. max_len - 3]}); -} - -fn compactHostLabelLen(value: []const u8, max_len: usize) usize { - if (value.len <= max_len) return value.len; - return if (max_len <= 3) 3 else max_len; -} +const nextTabInspectorVisible = labels.nextTabInspectorVisible; -fn hostTabLabelMaxLen(button_width: i32) usize { - const estimated = @as(usize, @intCast(@max(6, @divTrunc(button_width - 26, 8)))); - return std.math.clamp(estimated, @as(usize, 6), @as(usize, host_tab_label_max_len)); -} +const buildHostAwareBaseTitle = labels.buildHostAwareBaseTitle; -fn shouldShowPaneCount(button_width: i32, pane_count: usize) bool { - return pane_count > 1 and button_width >= 140; -} +const hostTabLabelMaxLen = labels.hostTabLabelMaxLen; -fn visibleTabRange(tab_count: usize, active_index: usize, tab_area_width: i32) VisibleTabRange { - if (tab_count == 0) return .{ .start = 0, .count = 0 }; - const max_visible = std.math.clamp( - @as(usize, @intCast(@max(1, @divTrunc(tab_area_width, host_tab_min_button_width)))), - @as(usize, 1), - tab_count, - ); - if (tab_count <= max_visible) return .{ .start = 0, .count = tab_count }; +const shouldShowPaneCount = labels.shouldShowPaneCount; - const clamped_active = @min(active_index, tab_count - 1); - var start = clamped_active; - if (max_visible > 1) start = clamped_active -| @divTrunc(max_visible - 1, 2); - if (start + max_visible > tab_count) start = tab_count - max_visible; - return .{ .start = start, .count = max_visible }; -} +const visibleTabRange = labels.visibleTabRange; -fn buildTabButtonLabel( - alloc: Allocator, - base_title: ?[]const u8, - index: usize, - active: bool, - pane_count: usize, - max_len: usize, - show_pane_count: bool, -) ![]u8 { - const compact = try compactHostLabel(alloc, base_title orelse "noctty", max_len); - defer alloc.free(compact); - if (show_pane_count and pane_count > 1) { - return try std.fmt.allocPrint( - alloc, - "{s}{d}: {s} ({d})", - .{ - if (active) "* " else "", - index + 1, - compact, - pane_count, - }, - ); - } - - return try std.fmt.allocPrint( - alloc, - "{s}{d}: {s}", - .{ - if (active) "* " else "", - index + 1, - compact, - }, - ); -} - -fn buildTabOverviewBannerText( - alloc: Allocator, - entries: []const TabOverviewEntry, -) ![]u8 { - if (entries.len == 0) return try alloc.dupe(u8, "Tabs: none"); - - var buf: std.ArrayListUnmanaged(u8) = .empty; - errdefer buf.deinit(alloc); - try buf.appendSlice(alloc, "Tabs: "); - for (entries, 0..) |entry, i| { - if (i > 0) try buf.appendSlice(alloc, " | "); - const compact = try compactHostLabel(alloc, entry.title orelse "noctty", 18); - defer alloc.free(compact); - try buf.writer(alloc).print("{s}{d}:{s}", .{ - if (entry.active) "*" else "", - i + 1, - compact, - }); - if (entry.pane_count > 1) { - try buf.writer(alloc).print(" ({d})", .{entry.pane_count}); - } - } - return try buf.toOwnedSlice(alloc); -} - -fn buildSearchOverlayLabel( - alloc: Allocator, - total: ?usize, - selected: ?usize, -) ![]u8 { - if (selected) |current| { - if (total) |count| return try std.fmt.allocPrint(alloc, "Find {d}/{d}", .{ current, count }); - } - if (total) |count| return try std.fmt.allocPrint(alloc, "Find {d}", .{count}); - return try alloc.dupe(u8, "Find"); -} - -fn buildSearchBarResultsText( - alloc: Allocator, - bar: *const win32_search_bar.SearchBar, -) ![]u8 { - if (bar.query.len == 0) return try alloc.dupe(u8, search_results_idle); - if (!bar.searched or bar.total == null) return try alloc.dupe(u8, search_results_pending); +const buildTabButtonLabel = labels.buildTabButtonLabel; - const total = bar.total.?; - if (total == 0) return try alloc.dupe(u8, search_results_none); - if (bar.selected) |selected| return try std.fmt.allocPrint(alloc, "{d}/{d}", .{ selected, total }); - return try std.fmt.allocPrint(alloc, "{d}", .{total}); -} +const buildSearchOverlayLabel = labels.buildSearchOverlayLabel; -const SearchBarResultsVisual = struct { - bg: u32, - border: u32, - fg: u32, -}; +const buildSearchBarResultsText = labels.buildSearchBarResultsText; -const SearchBarToolbarVisual = struct { - bg: u32, - border: u32, - separator: u32, -}; +const SearchBarResultsVisual = labels.SearchBarResultsVisual; +const SearchBarToolbarVisual = labels.SearchBarToolbarVisual; const SearchBarChromeLayout = struct { pad_x: i32, @@ -19474,14 +17337,7 @@ const SearchBarChromeLayout = struct { control_h: i32, }; -const SearchBarButtonRole = enum { - prev, - next, - regex, - case_sensitive, - whole_word, - close, -}; +const SearchBarButtonRole = labels.SearchBarButtonRole; const search_bar_button_roles = [_]SearchBarButtonRole{ .prev, @@ -19492,49 +17348,15 @@ const search_bar_button_roles = [_]SearchBarButtonRole{ .close, }; -fn searchBarButtonCommandId(role: SearchBarButtonRole) usize { - return switch (role) { - .prev => SEARCH_PREV_ID, - .next => SEARCH_NEXT_ID, - .regex => SEARCH_REGEX_ID, - .case_sensitive => SEARCH_CASE_ID, - .whole_word => SEARCH_WORD_ID, - .close => SEARCH_CLOSE_ID, - }; -} +const searchBarButtonCommandId = labels.searchBarButtonCommandId; -fn searchBarButtonLabel(role: SearchBarButtonRole) LPCWSTR { - return switch (role) { - .prev => search_prev_label, - .next => search_next_label, - .regex => search_regex_label, - .case_sensitive => search_case_label, - .whole_word => search_word_label, - .close => search_close_label, - }; -} - -fn showSearchBarResults(bar: *const win32_search_bar.SearchBar) bool { - return bar.query.len > 0; -} - -/// Docked-search geometry only changes when the fixed-width results slot -/// appears or disappears. Query text updates within the non-empty state -/// repaint child HWNDs but do not move controls. -fn searchBarNeedsRelayoutForQueryChange(previous_query_len: usize, next_query_len: usize) bool { - return (previous_query_len == 0) != (next_query_len == 0); -} +const searchBarButtonLabel = labels.searchBarButtonLabel; -/// Once a query has reached core search, any edit must invalidate it until the -/// debounced replacement query runs. -fn searchBarShouldInvalidateCoreSearchOnEdit(previous_query_len: usize, next_query_len: usize) bool { - _ = next_query_len; - return previous_query_len > 0; -} +const showSearchBarResults = labels.showSearchBarResults; +const searchBarNeedsRelayoutForQueryChange = labels.searchBarNeedsRelayoutForQueryChange; +const searchBarShouldInvalidateCoreSearchOnEdit = labels.searchBarShouldInvalidateCoreSearchOnEdit; -fn shouldAcceptCoreSearchUpdates(bar: *const win32_search_bar.SearchBar) bool { - return bar.query.len == 0 or bar.searched; -} +const shouldAcceptCoreSearchUpdates = labels.shouldAcceptCoreSearchUpdates; fn searchBarChromeLayout(host: *const Host, frame_rect: RECT) SearchBarChromeLayout { _ = frame_rect; @@ -19553,38 +17375,13 @@ fn searchBarChromeLayout(host: *const Host, frame_rect: RECT) SearchBarChromeLay }; } -fn searchBarButtonShowsLabel(role: SearchBarButtonRole, button_width: i32) bool { - _ = role; - _ = button_width; - return false; -} +const searchBarButtonShowsLabel = labels.searchBarButtonShowsLabel; -fn rectInset(rect: RECT, inset_x: i32, inset_y: i32) RECT { - return .{ - .left = rect.left + inset_x, - .top = rect.top + inset_y, - .right = rect.right - inset_x, - .bottom = rect.bottom - inset_y, - }; -} +const rectInset = labels.rectInset; -fn rectUnion(a: RECT, b: RECT) RECT { - return .{ - .left = @min(a.left, b.left), - .top = @min(a.top, b.top), - .right = @max(a.right, b.right), - .bottom = @max(a.bottom, b.bottom), - }; -} +const rectUnion = labels.rectUnion; -fn rectOffset(rect: RECT, dx: i32, dy: i32) RECT { - return .{ - .left = rect.left + dx, - .top = rect.top + dy, - .right = rect.right + dx, - .bottom = rect.bottom + dy, - }; -} +const rectOffset = labels.rectOffset; fn searchBarGroupRect(placements: []const ChildPlacement, expand_x: i32, expand_y: i32) ?RECT { var out: ?RECT = null; @@ -19601,87 +17398,17 @@ fn searchBarGroupRect(placements: []const ChildPlacement, expand_x: i32, expand_ return out; } -fn searchBarResultsVisual( - theme: *const ThemeColors, - bar: *const win32_search_bar.SearchBar, -) SearchBarResultsVisual { - const accent = overlayAccentColor(.search, theme.is_dark); - if (!bar.searched or bar.total == null) { - return .{ - .bg = if (theme.is_dark) - adjustColor(theme.overlay_bg, 8, 10, 10) - else - adjustColor(theme.overlay_bg, -6, -4, -2), - .border = accent, - .fg = theme.overlay_label_fg, - }; - } +const searchBarResultsVisual = labels.searchBarResultsVisual; - if (bar.total.? == 0) { - return .{ - .bg = if (theme.is_dark) - rgb(48, 28, 30) - else - rgb(252, 236, 236), - .border = theme.error_fg, - .fg = theme.error_fg, - }; - } +const searchBarToolbarVisual = labels.searchBarToolbarVisual; - return .{ - .bg = if (theme.is_dark) - adjustColor(theme.edit_frame_bg, 10, 14, 10) - else - adjustColor(theme.edit_frame_bg, -6, -2, -4), - .border = accent, - .fg = theme.overlay_label_fg, - }; -} +const searchBarFrameBg = labels.searchBarFrameBg; -fn searchBarToolbarVisual(theme: *const ThemeColors) SearchBarToolbarVisual { - return .{ - .bg = if (theme.is_dark) - adjustColor(theme.edit_frame_bg, 6, 8, 10) - else - adjustColor(theme.edit_frame_bg, -4, -2, -2), - .border = if (theme.is_dark) - adjustColor(theme.overlay_border, 18, 18, 20) - else - adjustColor(theme.overlay_border, -18, -18, -18), - .separator = if (theme.is_dark) - adjustColor(theme.overlay_border, 30, 30, 34) - else - adjustColor(theme.overlay_border, -26, -26, -26), - }; -} +const searchBarFrameBorder = labels.searchBarFrameBorder; -fn searchBarFrameBg(theme: *const ThemeColors) u32 { - return if (theme.is_dark) - adjustColor(theme.overlay_bg, 3, 3, 5) - else - adjustColor(theme.overlay_bg, -2, -2, -1); -} +const searchBarBottomLine = labels.searchBarBottomLine; -fn searchBarFrameBorder(theme: *const ThemeColors) u32 { - return if (theme.is_dark) - adjustColor(theme.overlay_border, 10, 10, 12) - else - adjustColor(theme.overlay_border, -14, -14, -14); -} - -fn searchBarBottomLine(theme: *const ThemeColors) u32 { - return if (theme.is_dark) - adjustColor(theme.overlay_border, -10, -10, -8) - else - adjustColor(theme.overlay_border, 10, 10, 10); -} - -fn searchBarButtonParentBg(theme: *const ThemeColors, role: SearchBarButtonRole) u32 { - return switch (role) { - .close => theme.overlay_bg, - else => searchBarToolbarVisual(theme).bg, - }; -} +const searchBarButtonParentBg = labels.searchBarButtonParentBg; fn searchBarSeparatorX(left: ChildPlacement, right: ChildPlacement) ?i32 { if (!left.rect_known or !left.visible or !right.rect_known or !right.visible) return null; @@ -19690,597 +17417,49 @@ fn searchBarSeparatorX(left: ChildPlacement, right: ChildPlacement) ?i32 { return left.rect.right + @divTrunc(gap, 2); } -fn buildTabOverviewOverlayLabel( - alloc: Allocator, - current_index: usize, - total: usize, -) ![]u8 { - if (total <= 1) return try alloc.dupe(u8, "Tab"); - return try std.fmt.allocPrint(alloc, "Tab {d}/{d}", .{ current_index + 1, total }); -} +const buildTabOverviewOverlayLabel = labels.buildTabOverviewOverlayLabel; -fn buildOverlayPaintLabelText( - alloc: Allocator, - mode: HostOverlayMode, - input_text: []const u8, - search_total: ?usize, - search_selected: ?usize, - host_status: HostTabStatus, - palette_presentation: PalettePresentation, -) ![]u8 { - return switch (mode) { - .none => try alloc.dupe(u8, ""), - .surface_title => try alloc.dupe(u8, "Window title"), - .tab_title => try alloc.dupe(u8, "Tab title"), - .command_palette => try buildCommandPaletteOverlayLabel( - alloc, - input_text, - palette_presentation, - ), - .profile => try alloc.dupe(u8, "Profile"), - .search => try buildSearchOverlayLabel(alloc, search_total, search_selected), - .tab_overview => try buildTabOverviewOverlayLabel(alloc, host_status.index, host_status.total), - // Confirm overlays source their prompt title from the payload - // at paint time; this default only appears when the payload - // has already been dropped (mid-teardown). - .confirm => try alloc.dupe(u8, "Confirm"), - }; -} +const buildOverlayPaintLabelText = labels.buildOverlayPaintLabelText; -fn buildOverlayFeedbackText( - alloc: Allocator, - banner_kind: HostBannerKind, - banner_text: ?[]const u8, - mode: HostOverlayMode, - input_text: []const u8, - active_search_needle: ?[]const u8, - search_total: ?usize, - search_selected: ?usize, - host_status: HostTabStatus, - pane_count: usize, - palette: PaletteSnapshot, - mru: []const []const u8, - palette_presentation: PalettePresentation, -) ![]u8 { - if (banner_text) |value| { - return switch (banner_kind) { - .err => try std.fmt.allocPrint(alloc, "Error: {s}", .{value}), - .info => try std.fmt.allocPrint(alloc, "Info: {s}", .{value}), - .none => try alloc.dupe(u8, value), - }; - } - if (mode == .command_palette) { - return try buildCommandPaletteFeedbackText( - alloc, - input_text, - mru, - palette_presentation, - ); - } - return try buildOverlayHintText( - alloc, - mode, - input_text, - active_search_needle, - search_total, - search_selected, - host_status, - pane_count, - palette, - mru, - ); -} +const buildOverlayFeedbackText = labels.buildOverlayFeedbackText; -fn buildOverlayAcceptLabel( - alloc: Allocator, - mode: HostOverlayMode, - input_text: []const u8, - active_search_needle: ?[]const u8, - search_total: ?usize, - search_selected: ?usize, - palette_presentation: PalettePresentation, -) ![]u8 { - return switch (mode) { - .none => try alloc.dupe(u8, "OK"), - .command_palette => blk: { - if (input_text.len == 0) break :blk try alloc.dupe(u8, "Close"); - if (palette_presentation.match_count > 0 and !palette_presentation.available) { - break :blk try alloc.dupe(u8, "Resize"); - } - if (palette_presentation.match_count > 0) break :blk try alloc.dupe(u8, "Activate"); - break :blk try alloc.dupe(u8, "Check"); - }, - .profile => try alloc.dupe(u8, "Open"), - .search => blk: { - if (input_text.len == 0) break :blk try alloc.dupe(u8, "Close"); - if (active_search_needle) |needle| { - if (std.mem.eql(u8, needle, input_text) and search_selected != null and search_total != null) { - break :blk try alloc.dupe(u8, "Next"); - } - } - if (search_total != null) break :blk try alloc.dupe(u8, "Find"); - break :blk try alloc.dupe(u8, "Find"); - }, - .surface_title, .tab_title => if (input_text.len == 0) - try alloc.dupe(u8, "Close") - else - try alloc.dupe(u8, "Apply"), - .tab_overview => if (input_text.len == 0) - try alloc.dupe(u8, "Close") - else - try alloc.dupe(u8, "Go"), - // Confirm overlays source accept/cancel labels from the - // payload. This default is never visible because - // `syncOverlayButtons` has a special-case branch for the - // `.confirm` mode that reads the payload directly; the - // switch just needs coverage to compile. - .confirm => try alloc.dupe(u8, "OK"), - }; -} +const buildOverlayAcceptLabel = labels.buildOverlayAcceptLabel; -fn buildOverlayHintText( - alloc: Allocator, - mode: HostOverlayMode, - input_text: []const u8, - active_search_needle: ?[]const u8, - search_total: ?usize, - search_selected: ?usize, - host_status: HostTabStatus, - pane_count: usize, - palette: PaletteSnapshot, - mru: []const []const u8, -) ![]u8 { - return switch (mode) { - .none => try alloc.dupe(u8, ""), - .command_palette => blk: { - if (try commandPaletteBannerText(alloc, palette, input_text, mru)) |text| break :blk text; - break :blk try alloc.dupe(u8, "No matching action. Try: new_tab, start_search, reload_config"); - }, - .profile => try alloc.dupe(u8, "Choose a profile by number or name. Enter opens a new tab."), - .search => blk: { - if (input_text.len == 0) break :blk try alloc.dupe(u8, "Type to search live. Enter repeats the current match. Escape closes."); - if (search_selected) |selected| { - if (search_total) |total| { - if (active_search_needle) |needle| { - if (std.mem.eql(u8, needle, input_text)) { - break :blk try std.fmt.allocPrint( - alloc, - "Live matches {d}/{d}. Enter jumps to the next match. Escape closes.", - .{ selected, total }, - ); - } - } - break :blk try std.fmt.allocPrint( - alloc, - "Live matches {d}/{d}. Enter keeps this needle active.", - .{ selected, total }, - ); - } - } - if (search_total) |total| { - break :blk try std.fmt.allocPrint( - alloc, - "Live matches {d}. Enter keeps this needle active.", - .{total}, - ); - } - break :blk try alloc.dupe(u8, "No matches yet. Keep typing to search live."); - }, - .surface_title => try alloc.dupe(u8, "Apply a window title override for this host. Submit empty text to clear it."), - .tab_title => blk: { - if (pane_count > 1) { - break :blk try std.fmt.allocPrint( - alloc, - "Rename tab {d}/{d}. This tab currently has {d} panes. Submit empty text to clear the override.", - .{ host_status.index + 1, host_status.total, pane_count }, - ); - } - break :blk try std.fmt.allocPrint( - alloc, - "Rename tab {d}/{d}. Submit empty text to clear the override.", - .{ host_status.index + 1, host_status.total }, - ); - }, - .tab_overview => blk: { - if (host_status.total <= 1) break :blk try alloc.dupe(u8, "Only one tab is open in this window."); - if (input_text.len == 0) { - break :blk try std.fmt.allocPrint( - alloc, - "Jump directly to a tab number. Current tab: {d}/{d}.", - .{ host_status.index + 1, host_status.total }, - ); - } - const requested = std.fmt.parseUnsigned(usize, input_text, 10) catch { - break :blk try std.fmt.allocPrint( - alloc, - "Enter a tab number from 1 to {d}. Current tab: {d}/{d}.", - .{ host_status.total, host_status.index + 1, host_status.total }, - ); - }; - if (requested == 0 or requested > host_status.total) { - break :blk try std.fmt.allocPrint( - alloc, - "Tab {d} is out of range. Valid range: 1 to {d}.", - .{ requested, host_status.total }, - ); - } - break :blk try std.fmt.allocPrint( - alloc, - "Jump to tab {d} of {d}.", - .{ requested, host_status.total }, - ); - }, - // Confirm overlays source their hint text from the payload - // body at paint time (the hint HWND is re-used as the body - // line). This helper produces a default so the promptTitle - // call doesn't need a special case; the real text lives on - // the payload. - .confirm => try alloc.dupe(u8, ""), - }; -} +const buildOverlayHintText = labels.buildOverlayHintText; -fn overlayCancelLabel(mode: HostOverlayMode) []const u8 { - return switch (mode) { - .none => "Cancel", - .command_palette, .profile, .search, .tab_overview => "Close", - .surface_title, .tab_title => "Cancel", - // Confirm overlays override this via the payload. - .confirm => "Cancel", - }; -} +const overlayCancelLabel = labels.overlayCancelLabel; -fn buildHostBannerText( - alloc: Allocator, - banner_kind: HostBannerKind, - value: []const u8, -) ![]u8 { - return switch (banner_kind) { - .none => try alloc.dupe(u8, value), - .info => try std.fmt.allocPrint(alloc, "Info: {s}", .{value}), - .err => try std.fmt.allocPrint(alloc, "Error: {s}", .{value}), - }; -} +const buildHostBannerText = labels.buildHostBannerText; -fn overlayPaintCacheDirty( - overlay_text_dirty: bool, - overlay_mode: HostOverlayMode, - cached_label_w: ?[:0]const u16, - cached_feedback_w: ?[:0]const u16, - cached_badge_w: ?[:0]const u16, -) bool { - return win32_chrome_state.overlayPaintCacheDirty( - overlay_text_dirty, - overlay_mode, - cached_label_w != null, - cached_feedback_w != null, - cached_badge_w != null, - ); -} +const overlayPaintCacheDirty = labels.overlayPaintCacheDirty; -fn buildInspectorBannerText( - alloc: Allocator, - host: HostTabStatus, - pane_count: usize, - zoomed: bool, -) ![]u8 { - if (zoomed and pane_count > 1) { - return try std.fmt.allocPrint( - alloc, - "Inspector active | tab {d}/{d} | panes {d} | zoomed | toggle Inspect to return", - .{ host.index + 1, host.total, pane_count }, - ); - } - if (pane_count > 1) { - return try std.fmt.allocPrint( - alloc, - "Inspector active | tab {d}/{d} | panes {d} | toggle Inspect to return", - .{ host.index + 1, host.total, pane_count }, - ); - } - return try std.fmt.allocPrint( - alloc, - "Inspector active | tab {d}/{d} | toggle Inspect to return", - .{ host.index + 1, host.total }, - ); -} +const buildInspectorBannerText = labels.buildInspectorBannerText; -fn buildInspectorPanelTitleText( - alloc: Allocator, - host: HostTabStatus, - pane_count: usize, - zoomed: bool, -) ![]u8 { - if (zoomed and pane_count > 1) { - return try std.fmt.allocPrint( - alloc, - "Inspector • tab {d}/{d} • {d} panes • zoomed", - .{ host.index + 1, host.total, pane_count }, - ); - } - if (pane_count > 1) { - return try std.fmt.allocPrint( - alloc, - "Inspector • tab {d}/{d} • {d} panes", - .{ host.index + 1, host.total, pane_count }, - ); - } - if (zoomed) { - return try std.fmt.allocPrint( - alloc, - "Inspector • tab {d}/{d} • zoomed", - .{ host.index + 1, host.total }, - ); - } - return try std.fmt.allocPrint( - alloc, - "Inspector • tab {d}/{d}", - .{ host.index + 1, host.total }, - ); -} - -fn buildInspectorPanelHintText( - alloc: Allocator, - pane_count: usize, - zoomed: bool, -) ![]u8 { - if (zoomed) { - return try alloc.dupe( - u8, - "Core inspector is live for the zoomed pane. Toggle Inspect to return to terminal-only view.", - ); - } - if (pane_count > 1) { - return try std.fmt.allocPrint( - alloc, - "Core inspector is live across {d} panes in this tab. Toggle Inspect to return to terminal-only view.", - .{pane_count}, - ); - } - return try alloc.dupe(u8, "Core inspector is live for this tab. Toggle Inspect to return to terminal-only view."); -} +const buildInspectorPanelTitleText = labels.buildInspectorPanelTitleText; -fn buildInspectorDetailText( - alloc: Allocator, - host: HostTabStatus, - pane_count: usize, - zoomed: bool, -) ![]u8 { - if (zoomed and pane_count > 1) { - return try std.fmt.allocPrint( - alloc, - "Inspector is attached to tab {d}/{d}. This tab has {d} panes and split zoom is active.", - .{ host.index + 1, host.total, pane_count }, - ); - } - if (pane_count > 1) { - return try std.fmt.allocPrint( - alloc, - "Inspector is attached to tab {d}/{d}. This tab currently has {d} panes.", - .{ host.index + 1, host.total, pane_count }, - ); - } - return try std.fmt.allocPrint( - alloc, - "Inspector is attached to tab {d}/{d}. Toggle Inspect to return to the normal terminal view.", - .{ host.index + 1, host.total }, - ); -} - -fn buildSearchDetailText( - alloc: Allocator, - needle: ?[]const u8, - total: ?usize, - selected: ?usize, -) ![]u8 { - if (needle) |value| { - if (selected) |current| { - if (total) |count| { - return try std.fmt.allocPrint( - alloc, - "Live search for \"{s}\" is active. Match {d} of {d}. Enter moves to the next match.", - .{ value, current, count }, - ); - } - } - if (total) |count| { - return try std.fmt.allocPrint( - alloc, - "Live search for \"{s}\" is active. {d} matches currently visible.", - .{ value, count }, - ); - } - return try std.fmt.allocPrint( - alloc, - "Live search for \"{s}\" is active. Keep typing to refine the current needle.", - .{value}, - ); - } - return try alloc.dupe(u8, "Live search is active. Keep typing to refine the current needle."); -} - -fn buildCommandButtonLabel( - alloc: Allocator, - active: bool, - input_text: ?[]const u8, -) ![]u8 { - if (active) { - if (input_text) |value| { - if (value.len > 0) { - const compact = try compactHostLabel(alloc, value, 9); - defer alloc.free(compact); - return try std.fmt.allocPrint(alloc, "Cmd {s}", .{compact}); - } - } - return try alloc.dupe(u8, "[Cmd]"); - } - return try alloc.dupe(u8, "Cmd"); -} - -fn buildProfilesButtonLabel( - alloc: Allocator, - active: bool, - profiles_opt: ?[]const windows_shell.Profile, - selected_index: ?usize, - pinned_slot_ordinal: ?usize, -) ![]u8 { - _ = pinned_slot_ordinal; - const profiles = profiles_opt orelse return try alloc.dupe(u8, if (active) "Pick shell" else "Launch"); - if (profiles.len == 0) return try alloc.dupe(u8, if (active) "Pick shell" else "Launch"); - const index = selected_index orelse 0; - const profile = profiles[@min(index, profiles.len - 1)]; - const compact = try compactHostLabel(alloc, profile.label, 8); - defer alloc.free(compact); - if (active) return try std.fmt.allocPrint(alloc, "Pick {s}", .{compact}); - return try std.fmt.allocPrint(alloc, "Launch {s}", .{compact}); -} - -fn launchTargetButtonLabel( - alloc: Allocator, - target: ProfileOpenTarget, - selected_index: ?usize, - pinned_slot_ordinal: ?usize, -) ![]u8 { - const base = switch (target) { - .tab => "Tab", - .window => "Win", - .split => "Pane", - }; - if (selected_index) |index| { - if (index < 9) { - if (pinned_slot_ordinal != null and pinned_slot_ordinal.? == index) { - return try std.fmt.allocPrint(alloc, "*{d} {s}", .{ index + 1, base }); - } - return try std.fmt.allocPrint(alloc, "{d} {s}", .{ index + 1, base }); - } - } - return try alloc.dupe(u8, base); -} - -fn profileKindBadge(kind: windows_shell.ProfileKind) []const u8 { - return switch (kind) { - .wsl_default, .wsl_distro => "WSL", - .pwsh => "PWSH", - .powershell => "PS", - .git_bash => "GIT", - .cmd => "CMD", - }; -} - -fn profileKindGlyph(kind: windows_shell.ProfileKind) []const u8 { - return switch (kind) { - .wsl_default, .wsl_distro => "<>", - .pwsh => ">>", - .powershell => ">_", - .git_bash => "$>", - .cmd => "C>", - }; -} - -fn buildProfileChromeBadgeText(alloc: Allocator, kind: windows_shell.ProfileKind) ![]u8 { - return try std.fmt.allocPrint(alloc, "{s} {s}", .{ - profileKindBadge(kind), - profileKindGlyph(kind), - }); -} - -fn profileKindDetail(kind: windows_shell.ProfileKind) windows_shell.ShellIntegrationDiagnostic { - return windows_shell.shellIntegrationDiagnostic(kind); -} - -fn profileOpenTargetActionText(target: ProfileOpenTarget) []const u8 { - return switch (target) { - .tab => "new tab", - .window => "new window", - .split => "split", - }; -} - -fn buildProfileStatusBadgeText( - alloc: Allocator, - profile: *const windows_shell.Profile, - selected_index: ?usize, - pinned_slot_ordinal: ?usize, -) ![]u8 { - _ = selected_index; - _ = pinned_slot_ordinal; - const compact = try compactHostLabel(alloc, profile.label, 12); - defer alloc.free(compact); - return try alloc.dupe(u8, compact); -} - -fn profileStatusBadgeTextLen( - profile: *const windows_shell.Profile, - selected_index: ?usize, - pinned_slot_ordinal: ?usize, -) usize { - _ = selected_index; - _ = pinned_slot_ordinal; - return compactHostLabelLen(profile.label, 12); -} - -/// Build a label for the profile dropdown menu: "Profile Name\tCtrl+Shift+N" -fn buildDropdownProfileLabel( - alloc: Allocator, - profile: *const windows_shell.Profile, - index: usize, -) ![]u8 { - if (index < 9) { - return try std.fmt.allocPrint(alloc, "{s}\tCtrl+Shift+{d}", .{ profile.label, index + 1 }); - } - return try alloc.dupe(u8, profile.label); -} +const buildInspectorPanelHintText = labels.buildInspectorPanelHintText; -fn buildProfileQuickSlotChipText( - alloc: Allocator, - profile: *const windows_shell.Profile, - slot_index: usize, - pinned_slot_ordinal: ?usize, -) ![]u8 { - if (slot_index < 9) { - if (pinned_slot_ordinal != null and pinned_slot_ordinal.? == slot_index) { - return try std.fmt.allocPrint(alloc, "*{d} {s}", .{ - slot_index + 1, - profileKindBadge(profile.kind), - }); - } - return try std.fmt.allocPrint(alloc, "{d} {s}", .{ - slot_index + 1, - profileKindBadge(profile.kind), - }); - } - return try alloc.dupe(u8, profileKindBadge(profile.kind)); -} +const buildInspectorDetailText = labels.buildInspectorDetailText; -fn profileQuickSlotChipTextLen( - profile: *const windows_shell.Profile, - slot_index: usize, - pinned_slot_ordinal: ?usize, -) usize { - const badge_len = profileKindBadge(profile.kind).len; - if (slot_index < 9) { - return badge_len + 2 + if (pinned_slot_ordinal != null and pinned_slot_ordinal.? == slot_index) @as(usize, 1) else 0; - } - return badge_len; -} +const buildSearchDetailText = labels.buildSearchDetailText; -fn launcherChipRightInset(has_slot_badge: bool, has_target_marker: bool) i32 { - if (has_slot_badge) return 16; - if (has_target_marker) return 12; - return 5; -} +const buildProfileChromeBadgeText = labels.buildProfileChromeBadgeText; -fn targetButtonLabelRightInset(target: ?ProfileOpenTarget) i32 { - return if (target != null) 12 else 0; -} +const buildProfileStatusBadgeText = labels.buildProfileStatusBadgeText; -fn buttonLabelRightInset(pinned_slot_ordinal: ?usize, target: ?ProfileOpenTarget) i32 { - const slot_inset: i32 = if (pinnedSlotBadgeDigit(pinned_slot_ordinal) != null) 16 else 0; - return @max(targetButtonLabelRightInset(target), slot_inset); -} +const profileStatusBadgeTextLen = labels.profileStatusBadgeTextLen; +/// Build a label for the profile dropdown menu: "Profile Name\tCtrl+Shift+N" +const buildDropdownProfileLabel = labels.buildDropdownProfileLabel; -fn shouldPaintQuickSlotTargetMarker(hovered: bool, focused: bool) bool { - return hovered or focused; -} +const buildProfileQuickSlotChipText = labels.buildProfileQuickSlotChipText; + +const profileQuickSlotChipTextLen = labels.profileQuickSlotChipTextLen; + +const launcherChipRightInset = labels.launcherChipRightInset; + +const buttonLabelRightInset = labels.buttonLabelRightInset; + +const shouldPaintQuickSlotTargetMarker = labels.shouldPaintQuickSlotTargetMarker; fn paintPinnedChipMarker(hdc: HDC, chip_rect: RECT, color: u32, dpi: u32) void { const s = Host.scaledBy; @@ -20298,11 +17477,7 @@ fn paintPinnedChipMarker(hdc: HDC, chip_rect: RECT, color: u32, dpi: u32) void { }, color); } -fn pinnedSlotBadgeDigit(pinned_slot_ordinal: ?usize) ?u8 { - const ordinal = pinned_slot_ordinal orelse return null; - if (ordinal >= 9) return null; - return @as(u8, @intCast('1' + ordinal)); -} +const pinnedSlotBadgeDigit = labels.pinnedSlotBadgeDigit; fn paintPinnedSlotBadge(hdc: HDC, rect: RECT, digit: u8, border: u32, bg: u32, fg: u32, dpi: u32) void { const s = Host.scaledBy; @@ -20314,33 +17489,21 @@ fn paintPinnedSlotBadge(hdc: HDC, rect: RECT, digit: u8, border: u32, bg: u32, f }; drawRoundedRect(hdc, badge_rect, bg, border, s(3, dpi)); var text_buf = [_]u16{ digit, 0 }; - _ = SetBkMode(hdc, TRANSPARENT); - _ = SetTextColor(hdc, fg); + _ = sys.SetBkMode(hdc, c.TRANSPARENT); + _ = sys.SetTextColor(hdc, fg); var text_rect = badge_rect; - _ = DrawTextW( + _ = sys.DrawTextW( hdc, @ptrCast(&text_buf), 1, &text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX, ); } -fn profileOpenTargetMarkerColor(target: ProfileOpenTarget) u32 { - return switch (target) { - .tab => rgb(132, 172, 238), - .window => rgb(236, 182, 118), - .split => rgb(126, 204, 148), - }; -} +const profileOpenTargetMarkerColor = labels.profileOpenTargetMarkerColor; -fn profileOpenTargetBadgeGlyph(target: ProfileOpenTarget) u8 { - return switch (target) { - .tab => 'T', - .window => 'W', - .split => 'S', - }; -} +const profileOpenTargetBadgeGlyph = labels.profileOpenTargetBadgeGlyph; fn paintTargetButtonBadge(hdc: HDC, rect: RECT, glyph: u8, border: u32, bg: u32, fg: u32, dpi: u32) void { const s = Host.scaledBy; @@ -20352,15 +17515,15 @@ fn paintTargetButtonBadge(hdc: HDC, rect: RECT, glyph: u8, border: u32, bg: u32, }; drawRoundedRect(hdc, badge_rect, bg, border, s(3, dpi)); var text_buf = [_]u16{ glyph, 0 }; - _ = SetBkMode(hdc, TRANSPARENT); - _ = SetTextColor(hdc, fg); + _ = sys.SetBkMode(hdc, c.TRANSPARENT); + _ = sys.SetTextColor(hdc, fg); var text_rect = badge_rect; - _ = DrawTextW( + _ = sys.DrawTextW( hdc, @ptrCast(&text_buf), 1, &text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX, ); } @@ -20374,546 +17537,42 @@ fn paintTargetChipBadge(hdc: HDC, rect: RECT, glyph: u8, border: u32, bg: u32, f }; drawRoundedRect(hdc, badge_rect, bg, border, s(3, dpi)); var text_buf = [_]u16{ glyph, 0 }; - _ = SetBkMode(hdc, TRANSPARENT); - _ = SetTextColor(hdc, fg); + _ = sys.SetBkMode(hdc, c.TRANSPARENT); + _ = sys.SetTextColor(hdc, fg); var text_rect = badge_rect; - _ = DrawTextW( + _ = sys.DrawTextW( hdc, @ptrCast(&text_buf), 1, &text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX, + c.DT_CENTER | c.DT_VCENTER | c.DT_SINGLELINE | c.DT_NOPREFIX, ); } -fn buildProfileCommandPreviewText( - alloc: Allocator, - profile: *const windows_shell.Profile, - max_len: usize, -) ![]u8 { - const command = try profile.command.string(alloc); - defer alloc.free(command); - return try compactHostLabel(alloc, command, max_len); -} - -fn buildProfileOrderSummaryText( - alloc: Allocator, - order_hint_opt: ?[]const u8, - max_items: usize, -) !?[]u8 { - const order_hint = order_hint_opt orelse return null; - var parts: std.ArrayListUnmanaged(u8) = .empty; - errdefer parts.deinit(alloc); - - var count: usize = 0; - var more = false; - var it = std.mem.splitAny(u8, order_hint, ",;"); - while (it.next()) |raw_token| { - const token = std.mem.trim(u8, raw_token, " \t\r\n"); - if (token.len == 0) continue; - if (count >= max_items) { - more = true; - break; - } - const compact = try compactHostLabel(alloc, token, 12); - defer alloc.free(compact); - if (parts.items.len > 0) try parts.appendSlice(alloc, " > "); - try parts.appendSlice(alloc, compact); - count += 1; - } - - if (count == 0) return null; - if (more) try parts.appendSlice(alloc, " > ..."); - return try parts.toOwnedSlice(alloc); -} - -fn buildProfileQuickPickText( - alloc: Allocator, - profiles: []const windows_shell.Profile, - max_items: usize, - max_label_len: usize, -) !?[]u8 { - if (profiles.len == 0 or max_items == 0) return null; - - var parts: std.ArrayListUnmanaged(u8) = .empty; - errdefer parts.deinit(alloc); - - const limit = @min(@min(max_items, profiles.len), 9); - var index: usize = 0; - while (index < limit) : (index += 1) { - const badge = try buildProfileChromeBadgeText(alloc, profiles[index].kind); - defer alloc.free(badge); - const label = try compactHostLabel(alloc, profiles[index].label, max_label_len); - defer alloc.free(label); - - if (parts.items.len > 0) try parts.appendSlice(alloc, " | "); - const item = try std.fmt.allocPrint(alloc, "{d} {s} {s}", .{ - index + 1, - badge, - label, - }); - defer alloc.free(item); - try parts.appendSlice(alloc, item); - } - - if (limit < profiles.len) try parts.appendSlice(alloc, " | ..."); - return try parts.toOwnedSlice(alloc); -} - -fn buildSearchButtonLabel( - alloc: Allocator, - active: bool, - total: ?usize, - selected: ?usize, -) ![]u8 { - if (selected) |current| { - if (total) |count| { - return try std.fmt.allocPrint(alloc, "{s}{d}/{d}", .{ - if (active) "[F] " else "Find ", - current, - count, - }); - } - } - if (total) |count| { - return try std.fmt.allocPrint(alloc, "{s}{d}", .{ - if (active) "[F] " else "Find ", - count, - }); - } - return try alloc.dupe(u8, if (active) "[Find]" else "Find"); -} - -fn buildProfileOverlayLabel( - alloc: Allocator, - profiles: []const windows_shell.Profile, - input_text: []const u8, - selected_index: usize, -) ![]u8 { - if (profiles.len == 0) return try alloc.dupe(u8, "Profile"); - return switch (resolveProfileSelection(profiles, input_text, selected_index)) { - .exact => |index| blk: { - const badge = try buildProfileChromeBadgeText(alloc, profiles[index].kind); - defer alloc.free(badge); - break :blk try std.fmt.allocPrint( - alloc, - "Profile {d}/{d} {s}", - .{ index + 1, profiles.len, badge }, - ); - }, - .ambiguous => |count| try std.fmt.allocPrint(alloc, "Profile {d}", .{count}), - .invalid => try alloc.dupe(u8, "Profile ?"), - }; -} - -fn buildProfileAcceptLabel( - alloc: Allocator, - profiles_opt: ?[]const windows_shell.Profile, - input_text: []const u8, - selected_index: usize, - default_target: ProfileOpenTarget, -) ![]u8 { - const profiles = profiles_opt orelse return try alloc.dupe(u8, "Check"); - if (profiles.len == 0) return try alloc.dupe(u8, "Check"); - return switch (resolveProfileSelection(profiles, input_text, selected_index)) { - .exact => switch (default_target) { - .tab => try alloc.dupe(u8, "Open Tab"), - .window => try alloc.dupe(u8, "Open Win"), - .split => try alloc.dupe(u8, "Split"), - }, - .ambiguous => try alloc.dupe(u8, "Pick"), - .invalid => try alloc.dupe(u8, "Check"), - }; -} - -fn buildProfileHintText( - alloc: Allocator, - profiles_opt: ?[]const windows_shell.Profile, - input_text: []const u8, - selected_index: usize, - default_target: ProfileOpenTarget, - pinned_slot_keys: [3]?[:0]const u8, -) ![]u8 { - const profiles = profiles_opt orelse return try alloc.dupe(u8, "No supported Windows profiles detected."); - if (profiles.len == 0) return try alloc.dupe(u8, "No supported Windows profiles detected."); - const quick_picks = try buildProfileQuickPickText(alloc, profiles, 4, 10); - defer if (quick_picks) |value| alloc.free(value); - const quick_suffix = if (quick_picks) |value| - try std.fmt.allocPrint(alloc, " Quick picks: {s}.", .{value}) - else - try alloc.dupe(u8, ""); - defer alloc.free(quick_suffix); - return switch (resolveProfileSelection(profiles, input_text, selected_index)) { - .exact => |index| blk: { - const preview = try buildProfileCommandPreviewText(alloc, &profiles[index], 36); - defer alloc.free(preview); - const badge = try buildProfileChromeBadgeText(alloc, profiles[index].kind); - defer alloc.free(badge); - const pinned_slot = try buildPinnedProfileSlotText( - alloc, - findLauncherQuickSlotOrdinal(pinned_slot_keys, profiles[index].key), - ); - defer alloc.free(pinned_slot); - break :blk try std.fmt.allocPrint( - alloc, - "{s} {s} | key {s} | run {s}.{s} Enter opens a {s}. Ctrl+Enter splits here. Shift+Enter opens a new window. Ctrl+1-9 launches directly. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning.{s}", - .{ - badge, - profiles[index].label, - profiles[index].key, - preview, - pinned_slot, - profileOpenTargetActionText(default_target), - quick_suffix, - }, - ); - }, - .ambiguous => |count| try std.fmt.allocPrint( - alloc, - "{d} profiles match. Keep typing a name or use Up/Down to cycle the current selection. Ctrl+1-9 launches directly. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning.{s}", - .{ count, quick_suffix }, - ), - .invalid => try std.fmt.allocPrint( - alloc, - "No matching profile. Try 1-{d} or a profile name like pwsh, ubuntu, git, or cmd. Ctrl+1-9 launches directly. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning. Space keeps the picker open.{s}", - .{ profiles.len, quick_suffix }, - ), - }; -} - -fn buildProfileDetailText( - alloc: Allocator, - profile: *const windows_shell.Profile, - profiles_opt: ?[]const windows_shell.Profile, - overlay_open: bool, - default_target: ProfileOpenTarget, - order_hint: ?[]const u8, - pinned_slot_keys: [3]?[:0]const u8, -) ![]u8 { - const preview = try buildProfileCommandPreviewText(alloc, profile, 32); - defer alloc.free(preview); - const badge = try buildProfileChromeBadgeText(alloc, profile.kind); - defer alloc.free(badge); - const pinned_slot = try buildPinnedProfileSlotText( - alloc, - findLauncherQuickSlotOrdinal(pinned_slot_keys, profile.key), - ); - defer alloc.free(pinned_slot); - const quick_picks = if (overlay_open) - try buildProfileQuickPickText(alloc, profiles_opt orelse &.{}, 4, 10) - else - try buildProfileQuickPickText(alloc, profiles_opt orelse &.{}, 3, 9); - defer if (quick_picks) |value| alloc.free(value); - const quick_suffix = if (overlay_open) - if (quick_picks) |value| - try std.fmt.allocPrint(alloc, " Quick picks: {s}.", .{value}) - else - try alloc.dupe(u8, "") - else if (quick_picks) |value| - try std.fmt.allocPrint(alloc, " Top slots: {s}.", .{value}) - else - try alloc.dupe(u8, ""); - defer alloc.free(quick_suffix); - const order_summary = try buildProfileOrderSummaryText(alloc, order_hint, 4); - defer if (order_summary) |value| alloc.free(value); - const order_suffix = if (order_summary) |value| - try std.fmt.allocPrint(alloc, " Order: {s}.", .{value}) - else - try alloc.dupe(u8, ""); - defer alloc.free(order_suffix); - const overlay_suffix = try std.fmt.allocPrint(alloc, "{s}{s}", .{ quick_suffix, order_suffix }); - defer alloc.free(overlay_suffix); - const idle_suffix = try std.fmt.allocPrint(alloc, "{s}{s}", .{ quick_suffix, order_suffix }); - defer alloc.free(idle_suffix); - const detail = profileKindDetail(profile.kind); - return if (overlay_open) - std.fmt.allocPrint( - alloc, - "Selected profile: {s} {s}. Run {s}.{s} Enter opens a {s}, Ctrl+Enter splits here, and Shift+Enter opens a new window. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning.{s}", - .{ - badge, - profile.label, - preview, - pinned_slot, - profileOpenTargetActionText(default_target), - overlay_suffix, - }, - ) - else - std.fmt.allocPrint( - alloc, - "Default profile: {s} {s}. Run {s}.{s} New hosts inherit this {s}.{s}{s} + opens a {s}, middle-click + splits here, and right-click + opens a new window. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning.{s}", - .{ - badge, - profile.label, - preview, - pinned_slot, - detail.summary, - if (detail.next_step != null) " " else "", - detail.next_step orelse "", - profileOpenTargetActionText(default_target), - idle_suffix, - }, - ); -} +const buildProfileOverlayLabel = labels.buildProfileOverlayLabel; -fn buildPinnedProfileSlotText(alloc: Allocator, pinned_slot_ordinal: ?usize) ![]u8 { - if (pinned_slot_ordinal) |ordinal| { - return try std.fmt.allocPrint(alloc, " Pinned slot {d}.", .{ordinal + 1}); - } - return try alloc.dupe(u8, ""); -} +const buildProfileAcceptLabel = labels.buildProfileAcceptLabel; -fn buildInspectorButtonLabel( - alloc: Allocator, - visible: bool, - pane_count: usize, -) ![]u8 { - if (visible and pane_count > 1) { - return try std.fmt.allocPrint(alloc, "[Inspect {d}]", .{pane_count}); - } - if (visible) return try alloc.dupe(u8, "[Inspect]"); - if (pane_count > 1) return try std.fmt.allocPrint(alloc, "Inspect {d}", .{pane_count}); - return try alloc.dupe(u8, "Inspect"); -} +const buildProfileHintText = labels.buildProfileHintText; -fn commandPaletteMatchCount(snap: PaletteSnapshot, input_text: []const u8) usize { - return win32_palette.matchCount(snap, input_text); -} +const buildProfileDetailText = labels.buildProfileDetailText; /// Returns the top-ranked match used for Enter-to-run, or null if no entry /// matches. -fn commandPaletteUniqueMatch(snap: PaletteSnapshot, input_text: []const u8) ?[]const u8 { - if (input_text.len == 0) return null; - var buf: [palette_max_tokens][]const u8 = undefined; - const tokens = tokenizePaletteQuery(input_text, &buf); - if (tokens.len == 0) return null; - - var best_rank: f64 = std.math.inf(f64); - var best_index: ?usize = null; - for (snap.commands, snap.cvals, 0..) |cmd, c, i| { - const r = rankPaletteEntry(cmd, c, tokens) orelse continue; - if (r < best_rank) { - best_rank = r; - best_index = i; - } - } - return if (best_index) |i| std.mem.span(snap.cvals[i].action) else null; -} - -fn commandPaletteCompletionCandidate( - snap: PaletteSnapshot, - seed: []const u8, - current_text: []const u8, - reverse: bool, -) ?[]const u8 { - var ranked_buf: [palette_max_ranked]RankedIndex = undefined; - const ranked = rankedIndicesForQuery(snap, seed, &ranked_buf); - if (ranked.len == 0) return null; - if (ranked.len == 1) return std.mem.span(snap.cvals[ranked[0].index].action); - - var current_rank_index: ?usize = null; - for (ranked, 0..) |r, i| { - const action = std.mem.span(snap.cvals[r.index].action); - if (std.mem.eql(u8, action, current_text)) { - current_rank_index = i; - break; - } - } - - const target = if (current_rank_index) |value| - if (reverse) - (value + ranked.len - 1) % ranked.len - else - (value + 1) % ranked.len - else if (reverse) - ranked.len - 1 - else - 0; - return std.mem.span(snap.cvals[ranked[target].index].action); -} - -/// Look up the Command.description for a given formatted action string. -/// Returns null if no entry matches or the description is empty. -fn commandPaletteDescriptionFor(snap: PaletteSnapshot, action_text: []const u8) ?[]const u8 { - for (snap.commands, snap.cvals) |cmd, c| { - const action = std.mem.span(c.action); - if (!std.mem.eql(u8, action, action_text)) continue; - const desc: []const u8 = cmd.description; - return if (desc.len == 0) null else desc; - } - return null; -} - -fn nextTabOverviewSelection(current: usize, total: usize, reverse: bool) usize { - if (total == 0) return 0; - const clamped = std.math.clamp(current, @as(usize, 1), total); - if (reverse) { - return if (clamped <= 1) total else clamped - 1; - } - return if (clamped >= total) 1 else clamped + 1; -} - -fn tabDirectionFromWheelDelta(delta: i16) apprt.action.GotoTab { - return if (delta > 0) .previous else .next; -} - -fn buildCommandPaletteOverlayLabel( - alloc: Allocator, - input_text: []const u8, - presentation: PalettePresentation, -) ![]u8 { - if (input_text.len == 0) return try alloc.dupe(u8, "Command"); - if (presentation.match_count > 0) { - return try std.fmt.allocPrint(alloc, "Command {d}", .{presentation.match_count}); - } - return try alloc.dupe(u8, "Command ?"); -} - -fn buildCommandPaletteFeedbackText( - alloc: Allocator, - input_text: []const u8, - mru: []const []const u8, - presentation: PalettePresentation, -) ![]u8 { - if (input_text.len == 0) { - if (mru.len > 0) { - return try std.fmt.allocPrint( - alloc, - "Recent: {s}. Type to search actions, themes, tabs, panes, settings, and help.", - .{mru[0]}, - ); - } - return try alloc.dupe( - u8, - "Type to search actions, themes, tabs, panes, settings, and help.", - ); - } - if (presentation.match_count == 0) { - return try alloc.dupe( - u8, - "No matching command. Try > actions, % themes, @ tabs, / panes, : settings, or ? help.", - ); - } - if (!presentation.available) { - return try std.fmt.allocPrint( - alloc, - "{d} matches. Make the window larger to show and activate results.", - .{presentation.match_count}, - ); - } - - const title = presentation.title orelse "Selected result"; - const subtitle = presentation.subtitle orelse ""; - if (presentation.match_count == 1) { - if (subtitle.len > 0) { - return try std.fmt.allocPrint( - alloc, - "{s} — {s}. Enter activates; Escape closes.", - .{ title, subtitle }, - ); - } - return try std.fmt.allocPrint( - alloc, - "{s}. Enter activates; Escape closes.", - .{title}, - ); - } - if (subtitle.len > 0) { - return try std.fmt.allocPrint( - alloc, - "{d} matches. Selected: {s} — {s}. Up/Down selects; Enter activates.", - .{ presentation.match_count, title, subtitle }, - ); - } - return try std.fmt.allocPrint( - alloc, - "{d} matches. Selected: {s}. Up/Down selects; Enter activates.", - .{ presentation.match_count, title }, - ); -} - -fn paletteCompletionText(descriptor: win32_palette.catalog.Descriptor) []const u8 { - return switch (descriptor.payload) { - .action => |payload| payload.action, - .recent_command => |payload| payload.action, - .profile => |key| key, - .setting => |key| key, - .theme => |name| name, - .tab, .pane, .help => descriptor.item.title, - }; -} +const nextTabOverviewSelection = labels.nextTabOverviewSelection; -fn commandPaletteBannerText( - alloc: Allocator, - snap: PaletteSnapshot, - input_text: []const u8, - mru: []const []const u8, -) !?[]u8 { - if (input_text.len == 0) { - if (mru.len == 0) { - return try alloc.dupe(u8, "Try: new_tab (new tab), start_search (find), toggle_tab_overview (tab list)"); - } - var buf: std.ArrayListUnmanaged(u8) = .empty; - errdefer buf.deinit(alloc); - try buf.appendSlice(alloc, "Recent: "); - const visible = @min(mru.len, 5); - for (mru[0..visible], 0..) |action, i| { - if (i > 0) try buf.appendSlice(alloc, ", "); - try buf.appendSlice(alloc, action); - } - return try buf.toOwnedSlice(alloc); - } +const tabDirectionFromWheelDelta = labels.tabDirectionFromWheelDelta; - if (input.Binding.Action.parse(input_text)) |_| { - if (commandPaletteDescriptionFor(snap, input_text)) |summary| { - return try std.fmt.allocPrint(alloc, "Ready: {s} - {s}", .{ input_text, summary }); - } - return try std.fmt.allocPrint(alloc, "Ready to run: {s}", .{input_text}); - } else |_| {} +const buildCommandPaletteOverlayLabel = labels.buildCommandPaletteOverlayLabel; - // Cap visible matches to keep the banner readable. With ~90 - // entries a bare query like "t" can rank dozens of matches; - // showing all of them produces a wall of text. - const max_visible: usize = 5; - var ranked_buf: [palette_max_ranked]RankedIndex = undefined; - const ranked = rankedIndicesForQuery(snap, input_text, &ranked_buf); - if (ranked.len == 0) return null; +const buildCommandPaletteFeedbackText = labels.buildCommandPaletteFeedbackText; - // Single match → "Ready: {action} - {description}" so Enter - // telegraphs what it'll run. Multi-match falls through to the - // "Matches: …" list so the user sees the candidate set. - if (ranked.len == 1) { - const action = std.mem.span(snap.cvals[ranked[0].index].action); - if (commandPaletteDescriptionFor(snap, action)) |summary| { - return try std.fmt.allocPrint(alloc, "Ready: {s} - {s}", .{ action, summary }); - } - return try std.fmt.allocPrint(alloc, "Ready to run: {s}", .{action}); - } +const paletteCompletionText = labels.paletteCompletionText; - var buf: std.ArrayListUnmanaged(u8) = .empty; - errdefer buf.deinit(alloc); - try buf.appendSlice(alloc, "Matches: "); - const visible_len = @min(ranked.len, max_visible); - for (ranked[0..visible_len], 0..) |r, i| { - const action = std.mem.span(snap.cvals[r.index].action); - if (i > 0) try buf.appendSlice(alloc, ", "); - try buf.appendSlice(alloc, action); - if (commandPaletteDescriptionFor(snap, action)) |summary| { - try buf.appendSlice(alloc, " ("); - try buf.appendSlice(alloc, summary); - try buf.appendSlice(alloc, ")"); - } - } - if (ranked.len > visible_len) { - const rest = ranked.len - visible_len; - try std.fmt.format(buf.writer(alloc), " + {d} more", .{rest}); - } - return try buf.toOwnedSlice(alloc); -} +const commandPaletteBannerText = labels.commandPaletteBannerText; fn readWindowTextUtf8Alloc(alloc: Allocator, hwnd: HWND) ![:0]u8 { - const len_w = GetWindowTextLengthW(hwnd); + const len_w = sys.GetWindowTextLengthW(hwnd); if (len_w <= 0) return try alloc.dupeZ(u8, ""); const needed: usize = @intCast(len_w + 1); @@ -20921,8 +17580,8 @@ fn readWindowTextUtf8Alloc(alloc: Allocator, hwnd: HWND) ![:0]u8 { defer alloc.free(buf_w); buf_w[needed - 1] = 0; - const copied = GetWindowTextW(hwnd, buf_w.ptr, @intCast(needed)); - if (copied < 0) return windows.unexpectedError(windows.kernel32.GetLastError()); + const copied = sys.GetWindowTextW(hwnd, buf_w.ptr, @intCast(needed)); + if (copied < 0) return lastError(); return try std.unicode.utf16LeToUtf8AllocZ(alloc, buf_w[0..@intCast(copied)]); } @@ -20933,18 +17592,16 @@ fn wmCommandChildHwnd(lParam: LPARAM) ?HWND { } fn expectedButtonClick(notify: u16, child: ?HWND, expected: ?HWND) bool { - return notify == BN_CLICKED and child != null and expected != null and child.? == expected.?; + return notify == c.BN_CLICKED and child != null and expected != null and child.? == expected.?; } fn getHost(hwnd: HWND) ?*Host { - const raw = GetWindowLongPtrW(hwnd, GWLP_USERDATA); - if (raw == 0) return null; - return @ptrFromInt(@as(usize, @intCast(raw))); + return windowData(Host, hwnd); } fn refocusActiveSurface(host: *Host) void { if (host.activeSurface()) |surface| { - if (surface.hwnd) |surface_hwnd| _ = SetFocus(surface_hwnd); + if (surface.hwnd) |surface_hwnd| _ = sys.SetFocus(surface_hwnd); } } @@ -20977,15 +17634,15 @@ fn refocusHostAfterActivation(host: *Host) void { host.overlay_cancel_hwnd, surface_hwnd, )) |target| { - _ = SetFocus(target); + _ = sys.SetFocus(target); } } fn postDeferredUiaDisconnect(thread_id: DWORD, pending: *DeferredUiaDisconnect) bool { if (thread_id == 0) return false; - return PostThreadMessageW( + return sys.PostThreadMessageW( thread_id, - WM_WINHOSTTY_UIA_DISCONNECT, + c.WM_WINHOSTTY_UIA_DISCONNECT, 0, @as(LPARAM, @bitCast(@as(usize, @intFromPtr(pending)))), ) != 0; @@ -21009,17 +17666,17 @@ fn processDeferredUiaDisconnect(thread_id: DWORD, pending: *DeferredUiaDisconnec fn drainDeferredUiaDisconnects(thread_id: DWORD) void { var msg: MSG = undefined; - while (PeekMessageW( + while (sys.PeekMessageW( &msg, null, - WM_WINHOSTTY_UIA_DISCONNECT, - WM_WINHOSTTY_UIA_DISCONNECT, - PM_REMOVE, + c.WM_WINHOSTTY_UIA_DISCONNECT, + c.WM_WINHOSTTY_UIA_DISCONNECT, + c.PM_REMOVE, ) != 0) { // PeekMessage always returns WM_QUIT regardless of the filter. // Consume it during shutdown, but never interpret its lParam as a // deferred-disconnect pointer. - if (msg.message != WM_WINHOSTTY_UIA_DISCONNECT) continue; + if (msg.message != c.WM_WINHOSTTY_UIA_DISCONNECT) continue; if (msg.lParam == 0) { log.warn("win32 UIA deferred disconnect message had no context", .{}); continue; @@ -21074,15 +17731,15 @@ test "win32 deferred UIA disconnect drain retries and releases once" { var fake: Fake = .{}; var queue_probe: MSG = undefined; - _ = PeekMessageW(&queue_probe, null, 0, 0, PM_NOREMOVE); + _ = sys.PeekMessageW(&queue_probe, null, 0, 0, c.PM_NOREMOVE); const pending = try std.heap.page_allocator.create(DeferredUiaDisconnect); pending.* = .{ .ctx = @ptrCast(&fake), .disconnect = &Fake.disconnect, .release = &Fake.release, }; - const thread_id = GetCurrentThreadId(); - PostQuitMessage(0); + const thread_id = sys.GetCurrentThreadId(); + sys.PostQuitMessage(0); try std.testing.expect(postDeferredUiaDisconnect(thread_id, pending)); drainDeferredUiaDisconnects(thread_id); try std.testing.expectEqual(@as(usize, 2), fake.disconnects); @@ -21155,9 +17812,9 @@ fn hostButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon // (hidden) EDIT has focus. Tab/Shift+Tab cycle between the // two buttons; arrow keys also cycle for screen-reader // users who navigate via arrows. - if (msg == WM_KEYDOWN and v.isOverlayButton(hwnd)) { + if (msg == c.WM_KEYDOWN and v.isOverlayButton(hwnd)) { switch (wParam) { - VK_ESCAPE => { + c.VK_ESCAPE => { if (v.overlay_mode == .confirm) { v.invokeConfirmCancel(); } else { @@ -21165,12 +17822,12 @@ fn hostButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon } return 0; }, - VK_TAB, VK_LEFT, VK_RIGHT => { - const reverse = if (wParam == VK_TAB) keyPressed(VK_SHIFT) else wParam == VK_LEFT; + c.VK_TAB, c.VK_LEFT, c.VK_RIGHT => { + const reverse = if (wParam == c.VK_TAB) keyPressed(c.VK_SHIFT) else wParam == c.VK_LEFT; _ = v.focusRelativeOverlayControl(hwnd, reverse); return 0; }, - VK_RETURN, VK_SPACE => { + c.VK_RETURN, c.VK_SPACE => { if (v.overlay_mode == .confirm) { if (hwnd == v.overlay_accept_hwnd) { v.invokeConfirmAccept(); @@ -21183,9 +17840,9 @@ fn hostButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon else => {}, } } - if (msg == WM_KEYDOWN and v.isSearchBarButton(hwnd)) { + if (msg == c.WM_KEYDOWN and v.isSearchBarButton(hwnd)) { if (v.searchControlSurface(hwnd)) |surface| { - if (searchButtonKeyAction(wParam, keyPressed(VK_SHIFT))) |action| { + if (searchButtonKeyAction(wParam, keyPressed(c.VK_SHIFT))) |action| { if (dockedSearchCoreDirectionFromKeyAction(action)) |dir| { _ = surface.navigateDockedSearch(dir) catch |err| { logUiActionError("docked search button navigation failed", err); @@ -21203,33 +17860,33 @@ fn hostButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon } } switch (msg) { - WM_SETFOCUS => { + c.WM_SETFOCUS => { if (v.searchControlSurface(hwnd)) |surface| { v.app.activateSurfaceNoFocus(surface); } }, - WM_KILLFOCUS => { + c.WM_KILLFOCUS => { v.setFocusedQuickSlot(null); }, - WM_MOUSEMOVE => { + c.WM_MOUSEMOVE => { var track: TRACKMOUSEEVENT = .{ .cbSize = @sizeOf(TRACKMOUSEEVENT), - .dwFlags = TME_LEAVE, + .dwFlags = c.TME_LEAVE, .hwndTrack = hwnd, .dwHoverTime = 0, }; - _ = TrackMouseEvent(&track); + _ = sys.TrackMouseEvent(&track); v.setHoveredButton(hwnd); }, - WM_MOUSELEAVE => { + c.WM_MOUSELEAVE => { if (v.isHoveredButton(hwnd)) v.setHoveredButton(null); }, - WM_RBUTTONUP => { + c.WM_RBUTTONUP => { if (v.new_tab_hwnd != null and hwnd == v.new_tab_hwnd.?) { if (v.openSelectedProfileOrFallback(.window)) return 0; } }, - WM_MBUTTONUP => { + c.WM_MBUTTONUP => { if (v.new_tab_hwnd != null and hwnd == v.new_tab_hwnd.?) { if (v.openSelectedProfileOrFallback(.split)) return 0; } @@ -21244,10 +17901,10 @@ fn hostButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon else v.chrome_button_prev_proc; if (prev) |proc| { - return CallWindowProcW(proc, hwnd, msg, wParam, lParam); + return sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); } fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { @@ -21255,32 +17912,32 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv if (host) |v| { if (v.tabIndexForButton(hwnd)) |index| { switch (msg) { - WM_LBUTTONDOWN => { + c.WM_LBUTTONDOWN => { const down_x = signedLowWord(lParamBits(lParam)); var btn_rect: RECT = undefined; - _ = GetClientRect(hwnd, &btn_rect); + _ = sys.GetClientRect(hwnd, &btn_rect); const close_left = btn_rect.right - v.scaled(host_tab_close_zone_width); if (down_x >= close_left and v.tabs.items.len > 1) { // Click in close zone — capture for mouseup but don't start drag - _ = SetCapture(hwnd); + _ = sys.SetCapture(hwnd); } else { // Start potential drag: record origin v.tab_drag_index = index; v.tab_drag_start_x = down_x; v.tab_drag_start_y = signedHighWord(lParamBits(lParam)); v.tab_drag_active = false; - _ = SetCapture(hwnd); + _ = sys.SetCapture(hwnd); } return 0; }, - WM_LBUTTONUP => { + c.WM_LBUTTONUP => { const drag_index = v.tab_drag_index; const was_drag = v.tab_drag_active; const drop_operation = v.tab_drop_operation; const drop_target_surface = v.tab_drop_target_surface; v.tab_drag_index = null; v.tab_drag_active = false; - _ = ReleaseCapture(); + _ = sys.ReleaseCapture(); if (was_drag) { if (drag_index) |source_index| { v.tab_drop_operation = drop_operation; @@ -21293,7 +17950,7 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv v.hideTabDropPreview(); const click_x = signedLowWord(lParamBits(lParam)); var btn_rect: RECT = undefined; - _ = GetClientRect(hwnd, &btn_rect); + _ = sys.GetClientRect(hwnd, &btn_rect); const close_left = btn_rect.right - v.scaled(host_tab_close_zone_width); switch (tabButtonMouseUpAction( drag_index, @@ -21318,19 +17975,19 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv } return 0; }, - WM_CAPTURECHANGED => { + c.WM_CAPTURECHANGED => { v.tab_drag_index = null; v.tab_drag_active = false; v.hideTabDropPreview(); }, - WM_MOUSEMOVE => { + c.WM_MOUSEMOVE => { var track: TRACKMOUSEEVENT = .{ .cbSize = @sizeOf(TRACKMOUSEEVENT), - .dwFlags = TME_LEAVE, + .dwFlags = c.TME_LEAVE, .hwndTrack = hwnd, .dwHoverTime = 0, }; - _ = TrackMouseEvent(&track); + _ = sys.TrackMouseEvent(&track); v.setHoveredButton(hwnd); // Handle tab drag reorder @@ -21356,7 +18013,7 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv // labeled split operation. Staying in the tab row // keeps the established ordered-reorder gesture. var pt: POINT = .{ .x = mouse_x, .y = mouse_y }; - _ = ClientToScreen(hwnd, &pt); + _ = sys.ClientToScreen(hwnd, &pt); v.updateTabDropPreview(drag_idx, pt); if (v.tab_drop_operation == .none) { if (v.tabIndexAtScreenX(pt.x)) |target_idx| { @@ -21371,11 +18028,11 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv } } }, - WM_MOUSELEAVE => { + c.WM_MOUSELEAVE => { if (v.isHoveredButton(hwnd)) v.setHoveredButton(null); }, - WM_KEYDOWN => { - if (tabButtonKeyAction(wParam, keyPressed(VK_CONTROL))) |action| { + c.WM_KEYDOWN => { + if (tabButtonKeyAction(wParam, keyPressed(c.VK_CONTROL))) |action| { switch (action) { .previous => { _ = v.activateTabByDirection(.previous); @@ -21460,7 +18117,7 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv } } }, - WM_MBUTTONUP => { + c.WM_MBUTTONUP => { if (v.tabs.items.len > 1 and v.activateTabIndex(index)) { if (v.activeSurface()) |surface| { _ = v.app.closeTab(.{ .surface = surface.core() }, .this) catch |err| { @@ -21470,7 +18127,7 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv } } }, - WM_LBUTTONDBLCLK => { + c.WM_LBUTTONDBLCLK => { if (v.activateTabIndex(index)) { if (v.activeSurface()) |surface| { runUiActionOrLog("tab double-click rename failed", surface.promptTitle(.tab)); @@ -21478,7 +18135,7 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv } } }, - WM_RBUTTONUP => { + c.WM_RBUTTONUP => { if (v.activateTabIndex(index)) { v.showTabContextMenu(hwnd, index); return 0; @@ -21488,16 +18145,16 @@ fn tabButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv } if (v.tabs.items[index].button_prev_proc) |proc| { - return CallWindowProcW(proc, hwnd, msg, wParam, lParam); + return sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); } } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); } fn searchEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { const host = getHost(hwnd); - if (msg == WM_GETOBJECT) { + if (msg == c.WM_GETOBJECT) { if (host) |v| { if (v.searchControlSurface(hwnd)) |surface| { if (surface.search_bar_edit_uia_provider) |provider| { @@ -21508,19 +18165,19 @@ fn searchEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon } if (host) |v| { if (v.searchControlSurface(hwnd)) |surface| switch (msg) { - WM_SETFOCUS => { + c.WM_SETFOCUS => { v.app.activateSurfaceNoFocus(surface); surface.invalidateSearchBarChildPaint(); }, - WM_KILLFOCUS => { + c.WM_KILLFOCUS => { surface.invalidateSearchBarChildPaint(); }, - WM_CHAR => { - if (wParam == VK_RETURN or wParam == VK_ESCAPE) return 0; + c.WM_CHAR => { + if (wParam == c.VK_RETURN or wParam == c.VK_ESCAPE) return 0; }, - WM_KEYDOWN, WM_SYSKEYDOWN => { - if (wParam == VK_RETURN) { - _ = surface.navigateDockedSearch(dockedSearchEnterDirection(keyPressed(VK_SHIFT))) catch |err| { + c.WM_KEYDOWN, c.WM_SYSKEYDOWN => { + if (wParam == c.VK_RETURN) { + _ = surface.navigateDockedSearch(dockedSearchEnterDirection(keyPressed(c.VK_SHIFT))) catch |err| { logUiActionError("docked search enter navigation failed", err); }; return 0; @@ -21531,7 +18188,7 @@ fn searchEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon }; return 0; } - if (searchButtonKeyAction(wParam, keyPressed(VK_SHIFT))) |action| { + if (searchButtonKeyAction(wParam, keyPressed(c.VK_SHIFT))) |action| { if (dockedSearchCoreDirectionFromKeyAction(action)) |dir| { _ = surface.navigateDockedSearch(dir) catch |err| { logUiActionError("docked search key navigation failed", err); @@ -21556,16 +18213,16 @@ fn searchEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon const result = if (host) |v| blk: { if (v.searchControlSurface(hwnd)) |surface| { if (surface.search_bar_edit_prev_proc) |proc| { - break :blk CallWindowProcW(proc, hwnd, msg, wParam, lParam); + break :blk sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); } } - break :blk DefWindowProcW(hwnd, msg, wParam, lParam); - } else DefWindowProcW(hwnd, msg, wParam, lParam); + break :blk sys.DefWindowProcW(hwnd, msg, wParam, lParam); + } else sys.DefWindowProcW(hwnd, msg, wParam, lParam); if (host) |v| { if (v.searchControlSurface(hwnd)) |surface| { if (surface.search_bar_edit_uia_provider) |provider| switch (msg) { - WM_SETFOCUS => win32_uia.events.raiseFocusChanged(&provider.base), - WM_KEYUP, WM_LBUTTONUP, EM_SETSEL => surface.raiseSearchEditSelectionChangedIfNeeded(), + c.WM_SETFOCUS => win32_uia.events.raiseFocusChanged(&provider.base), + c.WM_KEYUP, c.WM_LBUTTONUP, c.EM_SETSEL => surface.raiseSearchEditSelectionChangedIfNeeded(), else => {}, }; } @@ -21575,7 +18232,7 @@ fn searchEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon fn overlayEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { const host = getHost(hwnd); - if (msg == WM_GETOBJECT) { + if (msg == c.WM_GETOBJECT) { if (host) |v| { if (v.overlay_edit_uia_provider) |provider| { if (win32_uia.returnTerminalProvider(hwnd, wParam, lParam, provider)) |lr| return lr; @@ -21583,79 +18240,86 @@ fn overlayEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callco } } if (host) |v| switch (msg) { - WM_CHAR => { - if (wParam == VK_ESCAPE) return 0; - if (wParam == VK_RETURN) { + c.WM_CHAR => { + if (wParam == c.VK_ESCAPE) return 0; + if (wParam == c.VK_RETURN) { if (v.overlay_mode != .profile) { - _ = v.submitOverlay() catch {}; + runUiActionOrLog("overlay submit failed", v.submitOverlay()); } return 0; } }, - WM_KEYDOWN, WM_SYSKEYDOWN => { + c.WM_KEYDOWN, c.WM_SYSKEYDOWN => { if (v.commandPaletteToggleKeyMessage(msg, wParam, lParam)) { _ = v.dismissCommandPalette(); return 0; } if (v.overlay_mode == .command_palette) { - if (wParam == VK_UP or wParam == VK_DOWN) { + if (wParam == c.VK_UP or wParam == c.VK_DOWN) { // Up/Down move through the live result list instead of // editing the query text. - if (v.moveListSelection(wParam == VK_UP)) return 0; + if (v.moveListSelection(wParam == c.VK_UP)) return 0; } - if (wParam == VK_TAB) { - const reverse = keyPressed(VK_SHIFT); + if (wParam == c.VK_TAB) { + const reverse = keyPressed(c.VK_SHIFT); if ((v.completeCommandPalette(reverse) catch false)) return 0; } } - if (wParam == VK_TAB and v.focusRelativeOverlayControl(hwnd, keyPressed(VK_SHIFT))) return 0; + if (wParam == c.VK_TAB and v.focusRelativeOverlayControl(hwnd, keyPressed(c.VK_SHIFT))) return 0; if (v.overlay_mode == .tab_overview) { - if (wParam == VK_UP or wParam == VK_DOWN) { - if ((v.stepTabOverviewSelection(wParam == VK_UP) catch false)) return 0; + if (wParam == c.VK_UP or wParam == c.VK_DOWN) { + if ((v.stepTabOverviewSelection(wParam == c.VK_UP) catch false)) return 0; } } if (v.overlay_mode == .profile) { - if ((v.ensureProfiles() catch false)) { - if (clearQuickSlotPinsRequested(wParam, keyPressed(VK_MENU), keyPressed(VK_SHIFT))) { + const profiles_ready = v.ensureProfiles() catch |err| blk: { + logUiActionError("profile keyboard load failed", err); + break :blk false; + }; + if (profiles_ready) { + if (clearQuickSlotPinsRequested(wParam, keyPressed(c.VK_MENU), keyPressed(c.VK_SHIFT))) { if (v.clearQuickSlotPins()) return 0; } - if (quickSlotPinOrdinalFromKey(wParam, keyPressed(VK_MENU), keyPressed(VK_SHIFT))) |slot_ordinal| { + if (quickSlotPinOrdinalFromKey(wParam, keyPressed(c.VK_MENU), keyPressed(c.VK_SHIFT))) |slot_ordinal| { if (v.assignSelectedProfileToQuickSlot(slot_ordinal)) return 0; } - if (quickSlotShortcutProfileIndex(v.profiles.?.len, v.selectedProfileIndex(), wParam, keyPressed(VK_MENU))) |index| { + if (quickSlotShortcutProfileIndex(v.profiles.?.len, v.selectedProfileIndex(), wParam, keyPressed(c.VK_MENU))) |index| { if (v.quickOpenProfileIndex(index, v.app.launcher_profile_target)) return 0; } } - if (keyPressed(VK_CONTROL)) { + if (keyPressed(c.VK_CONTROL)) { if (profileShortcutIndexFromKey(wParam)) |index| { if (v.quickOpenProfileIndex(index, v.app.launcher_profile_target)) return 0; } } - if (wParam == VK_RETURN) { - _ = v.submitProfileOverlay(resolveProfileOpenTarget( - v.app.launcher_profile_target, - keyPressed(VK_SHIFT), - keyPressed(VK_CONTROL), - )) catch {}; + if (wParam == c.VK_RETURN) { + runUiActionOrLog( + "profile overlay submit failed", + v.submitProfileOverlay(resolveProfileOpenTarget( + v.app.launcher_profile_target, + keyPressed(c.VK_SHIFT), + keyPressed(c.VK_CONTROL), + )), + ); return 0; } - if (wParam == VK_UP or wParam == VK_DOWN) { - if ((v.stepProfileSelection(wParam == VK_UP) catch false)) return 0; + if (wParam == c.VK_UP or wParam == c.VK_DOWN) { + if ((v.stepProfileSelection(wParam == c.VK_UP) catch false)) return 0; } } if (v.overlay_mode == .search) { - if (wParam == VK_UP) { + if (wParam == c.VK_UP) { if ((v.navigateSearchOverlay(.previous) catch false)) return 0; } - if (wParam == VK_DOWN) { + if (wParam == c.VK_DOWN) { if ((v.navigateSearchOverlay(.next) catch false)) return 0; } - if (wParam == VK_RETURN and keyPressed(VK_SHIFT)) { + if (wParam == c.VK_RETURN and keyPressed(c.VK_SHIFT)) { if ((v.navigateSearchOverlay(.previous) catch false)) return 0; } } - if (wParam == VK_ESCAPE) { + if (wParam == c.VK_ESCAPE) { if (v.overlay_mode == .confirm) { // Same no-access-after-dispatch discipline as // the mouse-click confirm paths — cancel @@ -21668,7 +18332,7 @@ fn overlayEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callco return 0; } v.hideOverlay(); - v.layout() catch {}; + runUiActionOrLog("overlay escape layout failed", v.layout()); refocusActiveSurface(v); return 0; } @@ -21679,14 +18343,14 @@ fn overlayEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callco const result = if (host) |v| blk: { if (v.overlay_edit_prev_proc) |proc| { - break :blk CallWindowProcW(proc, hwnd, msg, wParam, lParam); + break :blk sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); } - break :blk DefWindowProcW(hwnd, msg, wParam, lParam); - } else DefWindowProcW(hwnd, msg, wParam, lParam); + break :blk sys.DefWindowProcW(hwnd, msg, wParam, lParam); + } else sys.DefWindowProcW(hwnd, msg, wParam, lParam); if (host) |v| { if (v.overlay_edit_uia_provider) |provider| switch (msg) { - WM_SETFOCUS => win32_uia.events.raiseFocusChanged(&provider.base), - WM_KEYUP, WM_LBUTTONUP, EM_SETSEL => v.raiseOverlayEditSelectionChangedIfNeeded(), + c.WM_SETFOCUS => win32_uia.events.raiseFocusChanged(&provider.base), + c.WM_KEYUP, c.WM_LBUTTONUP, c.EM_SETSEL => v.raiseOverlayEditSelectionChangedIfNeeded(), else => {}, }; } @@ -21694,96 +18358,96 @@ fn overlayEditProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callco } fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { - if (msg == WM_NCCREATE) { + if (msg == c.WM_NCCREATE) { const cs: *const CREATESTRUCTW = @ptrFromInt(@as(usize, @bitCast(lParam))); if (cs.lpCreateParams) |ptr| { - _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, @as(LONG_PTR, @intCast(@intFromPtr(ptr)))); + setWindowData(hwnd, ptr); } } const host = getHost(hwnd); switch (msg) { - WM_WINHOSTTY_HOST_NEW_TAB => { + c.WM_WINHOSTTY_HOST_NEW_TAB => { if (host) |v| v.dispatchDeferredNewTab(if (wParam == 0) null else @as(u64, @intCast(wParam))); return 0; }, - WM_ACTIVATE => { - if ((wParam & 0xFFFF) == WA_INACTIVE) { + c.WM_ACTIVATE => { + if ((wParam & 0xFFFF) == c.WA_INACTIVE) { if (host) |v| { if (v.app.config.@"quick-terminal-autohide") { if (v.app.quickTerminalSurfaceForHost(v)) |surface| { if (surface.window_visible) { - const top_level = surface.windowHwnd() orelse return DefWindowProcW(hwnd, msg, wParam, lParam); - if (IsWindowVisible(top_level) != 0) { - v.app.hideQuickTerminalSurface(surface); + const top_level = surface.windowHwnd() orelse return sys.DefWindowProcW(hwnd, msg, wParam, lParam); + if (sys.IsWindowVisible(top_level) != 0) { + App.hideQuickTerminalSurface(surface); } } } } } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, // Per-host tween heartbeat. Runs at ~16 ms while any chrome // animation is active; stops as soon as the scheduler empties. // Separate from the App-level quit timer, which uses // `SetTimer(null, …)` on the thread queue. - WM_TIMER => { - if (wParam == TWEEN_TIMER_ID) { + c.WM_TIMER => { + if (wParam == c.TWEEN_TIMER_ID) { if (host) |v| v.tickTweens(); return 0; } - if (wParam == SEARCH_TIMER_ID) { + if (wParam == c.SEARCH_TIMER_ID) { if (host) |v| v.tickSearchBars(); return 0; } - if (wParam == SCROLLBAR_TIMER_ID) { + if (wParam == c.SCROLLBAR_TIMER_ID) { if (host) |v| v.tickScrollbars(); return 0; } - if (wParam == RESIZE_SETTLE_TIMER_ID) { + if (wParam == c.RESIZE_SETTLE_TIMER_ID) { if (host) |v| v.tickResizeSettleRepaint(); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_DRAWITEM => { + c.WM_DRAWITEM => { if (host) |v| { const draw: *const DRAWITEMSTRUCT = @ptrFromInt(@as(usize, @bitCast(lParam))); if (v.drawSearchBarBackground(draw)) return 1; v.drawButton(draw); return 1; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_ERASEBKGND => return 1, + c.WM_ERASEBKGND => return 1, // Integrated-titlebar non-client handlers. All gated on the // host actively using the integrated titlebar (Win11 build / // config gate + visible tab/caption row); otherwise they fall // through to DefWindowProcW and the system paints its default // caption. - WM_NCCALCSIZE => { + c.WM_NCCALCSIZE => { if (host) |v| { if (v.handleNcCalcSize(hwnd, wParam, lParam)) |lr| return lr; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_NCHITTEST => { + c.WM_NCHITTEST => { if (host) |v| { if (v.usingIntegratedTitlebar()) { if (dispatchDwmNcMessage(hwnd, msg, wParam, lParam)) |lr| return lr; } if (v.handleNcHitTest(hwnd, lParam)) |lr| return lr; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_NCMOUSEMOVE => { + c.WM_NCMOUSEMOVE => { if (host) |v| { if (v.handleNcMouseMove(hwnd, wParam)) |lr| return lr; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_NCMOUSELEAVE => { + c.WM_NCMOUSELEAVE => { if (host) |v| { const dwm_result = if (v.usingIntegratedTitlebar()) dispatchDwmNcMessage(hwnd, msg, wParam, lParam) @@ -21792,55 +18456,55 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon v.handleNcMouseLeave(); if (dwm_result) |lr| return lr; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, // Integrated-titlebar caption-button click dispatch. The // custom NC frame still returns HTMINBUTTON / HTMAXBUTTON for // shell affordances such as Snap Layout hover, but it cannot // rely on DefWindowProc owning the full caption-button mouse // loop once WM_NCCALCSIZE has removed the top NC margin. - WM_NCLBUTTONDOWN => { + c.WM_NCLBUTTONDOWN => { if (host) |v| { if (v.usingIntegratedTitlebar()) { const ht: i32 = @intCast(@as(i64, @bitCast(wParam))); v.caption_pressed = titlebarCaptionFromHitTest(ht); if (v.caption_pressed != .none) v.repaintTopChrome(); - if (captionButtonSysCommand(ht, IsZoomed(hwnd) != 0)) |cmd| { + if (captionButtonSysCommand(ht, sys.IsZoomed(hwnd) != 0)) |cmd| { v.clearCaptionPressed(); - _ = SendMessageW(hwnd, WM_SYSCOMMAND, cmd, lParam); + _ = sys.SendMessageW(hwnd, c.WM_SYSCOMMAND, cmd, lParam); return 0; } - if (ht == HTCLOSE) return 0; + if (ht == c.HTCLOSE) return 0; } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_NCLBUTTONUP => { + c.WM_NCLBUTTONUP => { if (host) |v| { if (v.usingIntegratedTitlebar()) { if (dispatchDwmNcMessage(hwnd, msg, wParam, lParam)) |lr| return lr; const ht: i32 = @intCast(@as(i64, @bitCast(wParam))); - if (captionButtonSysCommand(ht, IsZoomed(hwnd) != 0) != null) { + if (captionButtonSysCommand(ht, sys.IsZoomed(hwnd) != 0) != null) { v.clearCaptionPressed(); return 0; } switch (ht) { - HTCLOSE => { + c.HTCLOSE => { v.clearCaptionPressed(); - _ = SendMessageW(hwnd, WM_SYSCOMMAND, SC_CLOSE, lParam); + _ = sys.SendMessageW(hwnd, c.WM_SYSCOMMAND, c.SC_CLOSE, lParam); return 0; }, else => v.clearCaptionPressed(), } } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, // UIA root object request. Screen readers (Narrator, NVDA) call // `AccessibleObjectFromWindow` which turns into WM_GETOBJECT on // the target HWND. We only handle the UIA root-object ID; MSAA // and other IDs fall through to the default proc. - WM_GETOBJECT => { + c.WM_GETOBJECT => { if (host) |v| { const provider = v.root_uia_provider orelse provider: { const created = win32_uia.RootProvider.create( @@ -21848,46 +18512,46 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon hwnd, ) catch |err| { log.warn("uia: root provider init failed err={}", .{err}); - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }; v.root_uia_provider = created; break :provider created; }; if (win32_uia.returnRootProvider(hwnd, wParam, lParam, provider)) |lr| return lr; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_SETTINGCHANGE => { + c.WM_SETTINGCHANGE => { if (host) |v| { v.app.refreshSystemWheelSettings(); v.app.refreshSystemScrollbarPreference(); v.app.reconfigureTheme(); } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_THEMECHANGED, WM_SYSCOLORCHANGE => { + c.WM_THEMECHANGED, c.WM_SYSCOLORCHANGE => { if (host) |v| { v.app.reconfigureTheme(); } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, // Fired by DWM when the user's accent color changes. Re-resolve // the theme so any accent-derived tokens pick up the new value // without requiring a config reload. - WM_DWMCOLORIZATIONCOLORCHANGED => { + c.WM_DWMCOLORIZATIONCOLORCHANGED => { if (host) |v| { v.app.reconfigureTheme(); } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_DPICHANGED => { + c.WM_DPICHANGED => { if (host) |v| { - const new_dpi = GetDpiForWindow(hwnd); + const new_dpi = sys.GetDpiForWindow(hwnd); if (new_dpi > 0) v.current_dpi = new_dpi; // Resize window to the suggested rectangle from lParam const suggested: *const RECT = @ptrFromInt(@as(usize, @bitCast(lParam))); - _ = SetWindowPos(hwnd, null, suggested.left, suggested.top, suggested.right - suggested.left, suggested.bottom - suggested.top, SWP_NOZORDER | SWP_NOACTIVATE); + _ = sys.SetWindowPos(hwnd, null, suggested.left, suggested.top, suggested.right - suggested.left, suggested.bottom - suggested.top, c.SWP_NOZORDER | c.SWP_NOACTIVATE); // Update content_scale on active tab surfaces const scale_val: f32 = @as(f32, @floatFromInt(v.current_dpi)) / 96.0; @@ -21905,9 +18569,9 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon v.recreateChromeFont(); // Relayout and repaint - v.layout() catch {}; + runUiActionOrLog("DPI update layout failed", v.layout()); v.app.resizeShellCompositorWindow(hwnd); - if (chromeTextNeedsFullInvalidation(v.statusBarHeight())) { + if (chromeTextNeedsFullInvalidation(Host.statusBarHeight())) { v.invalidateChromeText(); } else { v.invalidateTopChromeText(); @@ -21915,21 +18579,21 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon } return 0; }, - WM_CTLCOLOREDIT, WM_CTLCOLORSTATIC, WM_CTLCOLORBTN => { + c.WM_CTLCOLOREDIT, c.WM_CTLCOLORSTATIC, c.WM_CTLCOLORBTN => { if (host) |v| { - v.ensureThemeBrushes() catch return DefWindowProcW(hwnd, msg, wParam, lParam); + v.ensureThemeBrushes() catch return sys.DefWindowProcW(hwnd, msg, wParam, lParam); const hdc: HDC = @ptrFromInt(@as(usize, @intCast(wParam))); const child: HWND = @ptrFromInt(@as(usize, @intCast(lParam))); switch (msg) { - WM_CTLCOLOREDIT => { - _ = SetBkMode(hdc, OPAQUE); - _ = SetBkColor(hdc, v.app.resolved_theme.edit_bg); - _ = SetTextColor(hdc, v.app.resolved_theme.edit_fg); + c.WM_CTLCOLOREDIT => { + _ = sys.SetBkMode(hdc, c.OPAQUE); + _ = sys.SetBkColor(hdc, v.app.resolved_theme.edit_bg); + _ = sys.SetTextColor(hdc, v.app.resolved_theme.edit_fg); return @as(LRESULT, @bitCast(@as(usize, @intFromPtr(v.edit_brush.?)))); }, - WM_CTLCOLORSTATIC => { - _ = SetBkMode(hdc, TRANSPARENT); - _ = SetTextColor(hdc, if (v.isSearchBarResultsLabel(child)) + c.WM_CTLCOLORSTATIC => { + _ = sys.SetBkMode(hdc, c.TRANSPARENT); + _ = sys.SetTextColor(hdc, if (v.isSearchBarResultsLabel(child)) if (v.searchControlSurface(child)) |surface| searchBarResultsVisual(&v.app.resolved_theme, &surface.search_bar).fg else @@ -21940,13 +18604,13 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon v.app.resolved_theme.text_primary); return @as(LRESULT, @bitCast(@as(usize, @intFromPtr(v.overlay_brush.?)))); }, - WM_CTLCOLORBTN => { - _ = SetBkMode(hdc, TRANSPARENT); + c.WM_CTLCOLORBTN => { + _ = sys.SetBkMode(hdc, c.TRANSPARENT); if (v.isOverlayStyledButton(child)) { - _ = SetTextColor(hdc, v.app.resolved_theme.button_overlay_fg); + _ = sys.SetTextColor(hdc, v.app.resolved_theme.button_overlay_fg); return @as(LRESULT, @bitCast(@as(usize, @intFromPtr(v.overlay_brush.?)))); } - _ = SetTextColor(hdc, if (v.isActiveChromeButton(child)) + _ = sys.SetTextColor(hdc, if (v.isActiveChromeButton(child)) v.app.resolved_theme.button_active_fg else v.app.resolved_theme.button_chrome_fg); @@ -21955,16 +18619,16 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon else => {}, } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_COMMAND => { + c.WM_COMMAND => { if (host) |v| { const command_id = lowWord(wParam); const notify_code = highWord(wParam); const child_hwnd = wmCommandChildHwnd(lParam); switch (command_id) { 2002 => { - if (notify_code == EN_CHANGE) { + if (notify_code == c.EN_CHANGE) { // Skip re-entry from our own programmatic // `SetWindowTextW` on the edit HWND. The // caller already wraps the write in @@ -21975,17 +18639,20 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon // racing borrows across the sync cascade // caused palette edit re-entry crashes. if (v.suppress_edit_events) return 0; - appendOwnedString(v.app.core_app.alloc, &v.cached_overlay_edit, null) catch {}; + runUiActionOrLog( + "overlay edit cache clear failed", + appendOwnedString(v.app.core_app.alloc, &v.cached_overlay_edit, null), + ); if (v.overlay_edit_uia_provider) |provider| { provider.raiseTextChanged(); provider.raiseValueChanged(); } v.raiseOverlayEditSelectionChangedIfNeeded(); if (v.overlay_mode == .search) { - _ = v.syncSearchOverlay() catch {}; + runUiActionOrLog("search overlay sync failed", v.syncSearchOverlay()); } else if (v.overlay_mode == .command_palette) { - v.syncOverlayCompletionState() catch {}; - _ = v.syncCommandPaletteBanner() catch {}; + runUiActionOrLog("overlay completion sync failed", v.syncOverlayCompletionState()); + runUiActionOrLog("command palette banner sync failed", v.syncCommandPaletteBanner()); } else { _ = v.syncOverlayLabel() catch false; _ = v.syncOverlayHint() catch false; @@ -21994,8 +18661,8 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon return 0; } }, - SEARCH_EDIT_ID => { - if (notify_code == EN_CHANGE) { + c.SEARCH_EDIT_ID => { + if (notify_code == c.EN_CHANGE) { if (v.suppress_edit_events) return 0; if (child_hwnd) |child| { if (v.searchControlSurface(child)) |surface| { @@ -22026,8 +18693,8 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon if (v.overlay_mode == .profile) { runUiActionOrLog("profile overlay submit failed", v.submitProfileOverlay(resolveProfileOpenTarget( v.app.launcher_profile_target, - keyPressed(VK_SHIFT), - keyPressed(VK_CONTROL), + keyPressed(c.VK_SHIFT), + keyPressed(c.VK_CONTROL), ))); } else { runUiActionOrLog("overlay submit failed", v.submitOverlay()); @@ -22051,8 +18718,8 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon refocusActiveSurface(v); return 0; }, - SEARCH_PREV_ID => { - if (notify_code == BN_CLICKED) { + c.SEARCH_PREV_ID => { + if (notify_code == c.BN_CLICKED) { if (child_hwnd) |child| { if (v.searchControlSurface(child)) |surface| { if (dockedSearchButtonDirection(command_id)) |dir| { @@ -22065,8 +18732,8 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon } } }, - SEARCH_NEXT_ID => { - if (notify_code == BN_CLICKED) { + c.SEARCH_NEXT_ID => { + if (notify_code == c.BN_CLICKED) { if (child_hwnd) |child| { if (v.searchControlSurface(child)) |surface| { if (dockedSearchButtonDirection(command_id)) |dir| { @@ -22079,8 +18746,8 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon } } }, - SEARCH_REGEX_ID, SEARCH_CASE_ID, SEARCH_WORD_ID => { - if (notify_code == BN_CLICKED) { + c.SEARCH_REGEX_ID, c.SEARCH_CASE_ID, c.SEARCH_WORD_ID => { + if (notify_code == c.BN_CLICKED) { if (child_hwnd) |child| { if (v.searchControlSurface(child)) |surface| { _ = surface.handleSearchToggleClick(command_id) catch |err| { @@ -22091,8 +18758,8 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon } } }, - SEARCH_CLOSE_ID => { - if (notify_code == BN_CLICKED) { + c.SEARCH_CLOSE_ID => { + if (notify_code == c.BN_CLICKED) { if (child_hwnd) |child| { if (v.searchControlSurface(child)) |surface| { surface.closeDockedSearchBar(true, true) catch |err| { @@ -22161,34 +18828,34 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon } } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_SETFOCUS => { + c.WM_SETFOCUS => { if (host) |v| { refocusHostAfterActivation(v); } return 0; }, - WM_KEYDOWN, WM_SYSKEYDOWN, WM_KEYUP, WM_SYSKEYUP => { + c.WM_KEYDOWN, c.WM_SYSKEYDOWN, c.WM_KEYUP, c.WM_SYSKEYUP => { if (host) |v| { if (v.handleEmptyHostKeyMessage(msg, wParam, lParam)) return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_GETMINMAXINFO => { + c.WM_GETMINMAXINFO => { if (host) |v| { if (v.activeSurface()) |surface| { surface.updateMinMaxInfo(lParam); return 0; } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_SIZE => { + c.WM_SIZE => { if (host) |v| { // Defensive: maximize / minimize jumps don't always // route through WM_ENTERSIZEMOVE → WM_EXITSIZEMOVE, so @@ -22197,7 +18864,7 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon // Clear it here so the renderer doesn't stay in // coarse-mode after the user clicks maximize. const size_kind: u32 = @truncate(wParam); - if (size_kind == SIZE_MAXIMIZED or size_kind == SIZE_MINIMIZED) { + if (size_kind == c.SIZE_MAXIMIZED or size_kind == c.SIZE_MINIMIZED) { v.is_live_resize.store(false, .release); // Win32 silently cancels NC mouse tracking when // the window minimises or its maximised state @@ -22212,122 +18879,122 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon v.repaintTopChrome(); } } - v.layout() catch {}; + runUiActionOrLog("window resize layout failed", v.layout()); v.app.resizeShellCompositorWindow(hwnd); - if (size_kind == SIZE_MAXIMIZED or - (size_kind == SIZE_RESTORED and !v.is_live_resize.load(.acquire))) + if (size_kind == c.SIZE_MAXIMIZED or + (size_kind == c.SIZE_RESTORED and !v.is_live_resize.load(.acquire))) { v.startResizeSettleRepaints(); } return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_ENTERSIZEMOVE => { + c.WM_ENTERSIZEMOVE => { // User started a drag-resize or drag-move. We don't know // which yet, but setting the flag on both is harmless — // pure move generates no WM_SIZE so the throttle has // nothing to coalesce. WM_EXITSIZEMOVE clears it either // way. if (host) |v| v.is_live_resize.store(true, .release); - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_EXITSIZEMOVE => { + c.WM_EXITSIZEMOVE => { if (host) |v| { v.is_live_resize.store(false, .release); v.defer_surface_repaints_until_flush = true; - v.layout() catch {}; + runUiActionOrLog("resize completion layout failed", v.layout()); v.defer_surface_repaints_until_flush = false; v.flushDeferredVisibleSurfaceRepaints(); v.startResizeSettleRepaints(); v.forceVisibleSurfaceRepaintsNow(); v.forceHostCompositionPaint(); } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_MOUSEMOVE => { + c.WM_MOUSEMOVE => { if (host) |v| { var track: TRACKMOUSEEVENT = .{ .cbSize = @sizeOf(TRACKMOUSEEVENT), - .dwFlags = TME_LEAVE, + .dwFlags = c.TME_LEAVE, .hwndTrack = hwnd, .dwHoverTime = 0, }; - _ = TrackMouseEvent(&track); + _ = sys.TrackMouseEvent(&track); const point = POINT{ .x = signedLowWord(lParamBits(lParam)), .y = signedHighWord(lParamBits(lParam)), }; v.setHoveredQuickSlot(v.quickSlotProfileIndexAtPoint(point)); } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_MOUSELEAVE => { + c.WM_MOUSELEAVE => { if (host) |v| v.setHoveredQuickSlot(null); - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_POINTERWHEEL, WM_POINTERHWHEEL => { + c.WM_MOUSEWHEEL, c.WM_MOUSEHWHEEL, c.WM_POINTERWHEEL, c.WM_POINTERHWHEEL => { if (host) |v| { var point = POINT{ .x = signedLowWord(lParamBits(lParam)), .y = signedHighWord(lParamBits(lParam)), }; - if (ScreenToClient(hwnd, &point) != 0 and point.y >= 0 and point.y < v.tabBarHeight()) { + if (sys.ScreenToClient(hwnd, &point) != 0 and point.y >= 0 and point.y < v.tabBarHeight()) { _ = v.activateTabByDirection(tabDirectionFromWheelDelta(signedHighWord(wParam))); return 0; } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_LBUTTONUP, WM_MBUTTONUP, WM_RBUTTONUP => { + c.WM_LBUTTONUP, c.WM_MBUTTONUP, c.WM_RBUTTONUP => { if (host) |v| { const point = POINT{ .x = signedLowWord(lParamBits(lParam)), .y = signedHighWord(lParamBits(lParam)), }; - if (msg == WM_LBUTTONUP and v.handleUpdateNoticeClick(point)) { + if (msg == c.WM_LBUTTONUP and v.handleUpdateNoticeClick(point)) { return 0; } if (v.quickSlotProfileIndexAtPoint(point)) |profile_index| { _ = v.setSelectedProfileIndex(profile_index) catch return 0; const open_target = switch (msg) { - WM_MBUTTONUP => ProfileOpenTarget.split, - WM_RBUTTONUP => ProfileOpenTarget.window, + c.WM_MBUTTONUP => ProfileOpenTarget.split, + c.WM_RBUTTONUP => ProfileOpenTarget.window, else => v.app.launcher_profile_target, }; if (v.openSelectedProfile(open_target)) return 0; return 0; } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_PAINT => { + c.WM_PAINT => { if (host) |v| { v.chrome_paint_pending = false; v.chrome_paint_mask = .{}; v.paintChrome(); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_CLOSE => { + c.WM_CLOSE => { if (host) |v| { v.close(); return 0; } - _ = DestroyWindow(hwnd); + _ = sys.DestroyWindow(hwnd); return 0; }, - WM_DESTROY => { + c.WM_DESTROY => { if (host) |v| { v.app.detachShellCompositorWindow(hwnd); v.hwnd = null; @@ -22335,7 +19002,7 @@ fn hostWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callcon return 0; }, - else => return DefWindowProcW(hwnd, msg, wParam, lParam), + else => return sys.DefWindowProcW(hwnd, msg, wParam, lParam), } } @@ -22390,23 +19057,7 @@ fn movedIndexAfterReorder(active: usize, from: usize, to: usize) usize { return active; } -fn quickSlotProfileIndex( - profiles_len: usize, - selected_index: ?usize, - slot_ordinal: usize, - max_slots: usize, -) ?usize { - if (profiles_len == 0 or slot_ordinal >= max_slots) return null; - var drawn: usize = 0; - var index: usize = 0; - while (index < profiles_len) : (index += 1) { - if (selected_index != null and index == selected_index.?) continue; - if (drawn == slot_ordinal) return index; - drawn += 1; - if (drawn >= max_slots) break; - } - return null; -} +const quickSlotProfileIndex = labels.quickSlotProfileIndex; fn nextQuickSlotFocus( profiles_len: usize, @@ -22481,14 +19132,7 @@ fn applyQuickSlotPreferenceOrder( } } -fn findLauncherQuickSlotOrdinal(slot_keys: [3]?[:0]const u8, key: []const u8) ?usize { - for (slot_keys, 0..) |slot_key, index| { - if (slot_key) |value| { - if (std.ascii.eqlIgnoreCase(value, key)) return index; - } - } - return null; -} +const findLauncherQuickSlotOrdinal = labels.findLauncherQuickSlotOrdinal; fn launcherQuickSlotAssignmentNeedsChange( slot_keys: [3]?[:0]const u8, @@ -22570,17 +19214,13 @@ fn pointInRect(point: POINT, rect: RECT) bool { point.y < rect.bottom; } -fn lParamBits(lParam: LPARAM) usize { - return @as(usize, @bitCast(lParam)); -} +const lParamBits = win32_input.lParamBits; fn lowWord(value: usize) u16 { return @truncate(value & 0xFFFF); } -fn highWord(value: usize) u16 { - return @truncate((value >> 16) & 0xFFFF); -} +const highWord = win32_input.highWord; fn signedLowWord(value: usize) i16 { return @bitCast(lowWord(value)); @@ -22590,18 +19230,6 @@ fn signedHighWord(value: usize) i16 { return @bitCast(highWord(value)); } -fn scanCodeFromLParam(lParam: LPARAM) u32 { - return @as(u32, highWord(lParamBits(lParam))) & 0xFF; -} - -fn isExtendedKey(lParam: LPARAM) bool { - return (lParamBits(lParam) & KF_EXTENDED) != 0; -} - -fn isRepeatedKey(lParam: LPARAM) bool { - return (lParamBits(lParam) & KF_REPEAT) != 0; -} - fn cursorPosFromLParam(lParam: LPARAM) apprt.CursorPos { const bits = lParamBits(lParam); return .{ @@ -22632,13 +19260,13 @@ fn imeWindowForms(ime_pos: apprt.IMEPos, content_scale: apprt.ContentScale) ImeW return .{ .composition = .{ - .dwStyle = CFS_POINT, + .dwStyle = c.CFS_POINT, .ptCurrentPos = point, .rcArea = .{ .left = 0, .top = 0, .right = 0, .bottom = 0 }, }, .candidate = .{ .dwIndex = 0, - .dwStyle = CFS_EXCLUDE, + .dwStyle = c.CFS_EXCLUDE, .ptCurrentPos = point, .rcArea = .{ .left = point.x, @@ -22705,393 +19333,49 @@ fn writeImeWindowFormsTrace( .rcArea = .{ .Left = forms.composition.rcArea.left, .Top = forms.composition.rcArea.top, - .Right = forms.composition.rcArea.right, - .Bottom = forms.composition.rcArea.bottom, - }, - }, - .candidate = .{ - .dwIndex = forms.candidate.dwIndex, - .dwStyle = forms.candidate.dwStyle, - .ptCurrentPos = .{ - .X = forms.candidate.ptCurrentPos.x, - .Y = forms.candidate.ptCurrentPos.y, - }, - .rcArea = .{ - .Left = forms.candidate.rcArea.left, - .Top = forms.candidate.rcArea.top, - .Right = forms.candidate.rcArea.right, - .Bottom = forms.candidate.rcArea.bottom, - }, - }, - }, .{})}) catch |err| { - log.warn("win32 IME form trace write failed path={s} err={}", .{ trace_path, err }); - return; - }; - stream.flush() catch |err| { - log.warn("win32 IME form trace flush failed path={s} err={}", .{ trace_path, err }); - return; - }; -} - -fn readSystemWheelSetting(action: UINT, fallback: u32) u32 { - var value: UINT = fallback; - if (SystemParametersInfoW(action, 0, @ptrCast(&value), 0) == 0) { - return fallback; - } - return value; -} - -fn wheelDeltaFromWParam(wParam: WPARAM) i16 { - const bits = @as(usize, @intCast(wParam)); - return @bitCast(highWord(bits)); -} - -fn wheelSettingForAxis(settings: SystemWheelSettings, axis: MouseWheelAxis) u32 { - return switch (axis) { - .vertical => settings.lines, - .horizontal => settings.chars, - }; -} - -fn wheelUnitSize(ctx: WheelNormalizationContext, axis: MouseWheelAxis) f64 { - const dim: u32 = switch (axis) { - .vertical => ctx.cell_size.height, - .horizontal => ctx.cell_size.width, - }; - return @floatFromInt(@max(dim, 1)); -} - -fn wheelViewportSize(ctx: WheelNormalizationContext, axis: MouseWheelAxis) f64 { - const dim: u32 = switch (axis) { - .vertical => ctx.viewport.height, - .horizontal => ctx.viewport.width, - }; - const viewport: f64 = @floatFromInt(@max(dim, 1)); - const unit = wheelUnitSize(ctx, axis); - return @max(unit, viewport - unit); -} - -fn normalizeWheelDelta( - ctx: WheelNormalizationContext, - axis: MouseWheelAxis, - delta: i16, -) NormalizedWheelScroll { - if (delta == 0) return .{}; - - const precision = @rem(delta, WHEEL_DELTA) != 0; - const notch_delta = @as(f64, @floatFromInt(delta)) / WHEEL_DELTA; - const pixels = if (precision) - notch_delta * wheelUnitSize(ctx, axis) - else discrete: { - const setting = wheelSettingForAxis(ctx.settings, axis); - if (setting == 0) return .{}; - - break :discrete if (setting == WHEEL_PAGESCROLL) - notch_delta * wheelViewportSize(ctx, axis) - else - notch_delta * @as(f64, @floatFromInt(setting)) * wheelUnitSize(ctx, axis); - }; - - return switch (axis) { - .vertical => .{ - .yoff = pixels, - .mods = .{ - .precision = precision, - .pixel_delta = true, + .Right = forms.composition.rcArea.right, + .Bottom = forms.composition.rcArea.bottom, }, }, - .horizontal => .{ - .xoff = pixels, - .mods = .{ - .precision = precision, - .pixel_delta = true, + .candidate = .{ + .dwIndex = forms.candidate.dwIndex, + .dwStyle = forms.candidate.dwStyle, + .ptCurrentPos = .{ + .X = forms.candidate.ptCurrentPos.x, + .Y = forms.candidate.ptCurrentPos.y, + }, + .rcArea = .{ + .Left = forms.candidate.rcArea.left, + .Top = forms.candidate.rcArea.top, + .Right = forms.candidate.rcArea.right, + .Bottom = forms.candidate.rcArea.bottom, }, }, + }, .{})}) catch |err| { + log.warn("win32 IME form trace write failed path={s} err={}", .{ trace_path, err }); + return; }; -} - -fn keyPressed(vk: i32) bool { - return GetKeyState(vk) < 0; -} - -fn keyToggled(vk: i32) bool { - return (GetKeyState(vk) & 1) != 0; -} - -fn modsFromKeyboardState(state: *const [256]u8) input.Mods { - const pressed = struct { - fn check(state_: *const [256]u8, vk: usize) bool { - return (state_[vk] & 0x80) != 0; - } - }; - - return .{ - .shift = pressed.check(state, VK_SHIFT), - .ctrl = pressed.check(state, VK_CONTROL), - .alt = pressed.check(state, VK_MENU), - .super = pressed.check(state, VK_LWIN) or pressed.check(state, VK_RWIN), - .caps_lock = (state[VK_CAPITAL] & 1) != 0, - .num_lock = (state[VK_NUMLOCK] & 1) != 0, - .sides = .{ - .shift = if (pressed.check(state, VK_RSHIFT)) .right else .left, - .ctrl = if (pressed.check(state, VK_RCONTROL)) .right else .left, - .alt = if (pressed.check(state, VK_RMENU)) .right else .left, - .super = if (pressed.check(state, VK_RWIN)) .right else .left, - }, - }; -} - -fn fallbackMods() input.Mods { - return .{ - .shift = keyPressed(VK_SHIFT), - .ctrl = keyPressed(VK_CONTROL), - .alt = keyPressed(VK_MENU), - .super = keyPressed(VK_LWIN) or keyPressed(VK_RWIN), - .caps_lock = keyToggled(VK_CAPITAL), - .num_lock = keyToggled(VK_NUMLOCK), - .sides = .{ - .shift = if (keyPressed(VK_RSHIFT)) .right else .left, - .ctrl = if (keyPressed(VK_RCONTROL)) .right else .left, - .alt = if (keyPressed(VK_RMENU)) .right else .left, - .super = if (keyPressed(VK_RWIN)) .right else .left, - }, - }; -} - -fn currentKeyboardState(state: *[256]u8) ?*const [256]u8 { - if (GetKeyboardState(state) == 0) return null; - return state; -} - -fn currentModsFromKeyboardState(state: ?*const [256]u8) input.Mods { - return if (state) |keyboard_state| modsFromKeyboardState(keyboard_state) else fallbackMods(); -} - -fn currentMods() input.Mods { - var state: [256]u8 = [_]u8{0} ** 256; - return currentModsFromKeyboardState(currentKeyboardState(&state)); -} - -fn keyFromVirtualKey(vk: UINT, lParam: LPARAM) input.Key { - return switch (vk) { - VK_BACK => .backspace, - VK_TAB => .tab, - VK_RETURN => if (isExtendedKey(lParam)) .numpad_enter else .enter, - VK_SHIFT => if (scanCodeFromLParam(lParam) == 0x36) .shift_right else .shift_left, - VK_LSHIFT => .shift_left, - VK_RSHIFT => .shift_right, - VK_CONTROL, VK_LCONTROL => if (isExtendedKey(lParam) or vk == VK_RCONTROL) .control_right else .control_left, - VK_RCONTROL => .control_right, - VK_MENU, VK_LMENU => if (isExtendedKey(lParam) or vk == VK_RMENU) .alt_right else .alt_left, - VK_RMENU => .alt_right, - VK_PAUSE => .pause, - VK_CAPITAL => .caps_lock, - VK_ESCAPE => .escape, - VK_SPACE => .space, - VK_PRIOR => .page_up, - VK_NEXT => .page_down, - VK_END => .end, - VK_HOME => .home, - VK_LEFT => .arrow_left, - VK_UP => .arrow_up, - VK_RIGHT => .arrow_right, - VK_DOWN => .arrow_down, - VK_SNAPSHOT => .print_screen, - VK_INSERT => .insert, - VK_DELETE => .delete, - VK_LWIN => .meta_left, - VK_RWIN => .meta_right, - VK_APPS => .context_menu, - VK_MULTIPLY => .numpad_multiply, - VK_ADD => .numpad_add, - VK_SEPARATOR => .numpad_separator, - VK_SUBTRACT => .numpad_subtract, - VK_DECIMAL => .numpad_decimal, - VK_DIVIDE => .numpad_divide, - VK_NUMLOCK => .num_lock, - VK_SCROLL => .scroll_lock, - VK_OEM_1 => .semicolon, - VK_OEM_PLUS => .equal, - VK_OEM_COMMA => .comma, - VK_OEM_MINUS => .minus, - VK_OEM_PERIOD => .period, - VK_OEM_2 => .slash, - VK_OEM_3 => .backquote, - VK_OEM_4 => .bracket_left, - VK_OEM_5 => .backslash, - VK_OEM_6 => .bracket_right, - VK_OEM_7 => .quote, - VK_0...VK_9 => input.Key.fromASCII(@as(u8, @intCast('0' + (vk - VK_0)))) orelse .unidentified, - VK_A...VK_Z => input.Key.fromASCII(@as(u8, @intCast('a' + (vk - VK_A)))) orelse .unidentified, - VK_NUMPAD0...VK_NUMPAD9 => @enumFromInt( - @intFromEnum(input.Key.numpad_0) + @as(c_int, @intCast(vk - VK_NUMPAD0)), - ), - VK_F1...VK_F24 => @enumFromInt( - @intFromEnum(input.Key.f1) + @as(c_int, @intCast(vk - VK_F1)), - ), - else => .unidentified, + stream.flush() catch |err| { + log.warn("win32 IME form trace flush failed path={s} err={}", .{ trace_path, err }); + return; }; } -fn unshiftedCodepointForVirtualKey(vk: UINT) u21 { - return switch (vk) { - VK_0...VK_9 => @as(u21, @intCast('0' + (vk - VK_0))), - VK_A...VK_Z => @as(u21, @intCast('a' + (vk - VK_A))), - VK_SPACE => ' ', - VK_OEM_1 => ';', - VK_OEM_PLUS => '=', - VK_OEM_COMMA => ',', - VK_OEM_MINUS => '-', - VK_OEM_PERIOD => '.', - VK_OEM_2 => '/', - VK_OEM_3 => '`', - VK_OEM_4 => '[', - VK_OEM_5 => '\\', - VK_OEM_6 => ']', - VK_OEM_7 => '\'', - else => 0, - }; -} +const readSystemWheelSetting = win32_input.readSystemWheelSetting; -fn hotkeyModifiers(mods: input.Mods) UINT { - var result: UINT = 0; - if (mods.alt) result |= MOD_ALT; - if (mods.ctrl) result |= MOD_CONTROL; - if (mods.shift) result |= MOD_SHIFT; - if (mods.super) result |= MOD_WIN; - return result; -} +const wheelDeltaFromWParam = win32_input.wheelDeltaFromWParam; -fn hotkeyPhysicalVirtualKey(key: input.Key) ?UINT { - const key_int = @intFromEnum(key); - if (key_int >= @intFromEnum(input.Key.key_a) and key_int <= @intFromEnum(input.Key.key_z)) { - return VK_A + @as(UINT, @intCast(key_int - @intFromEnum(input.Key.key_a))); - } - if (key_int >= @intFromEnum(input.Key.digit_0) and key_int <= @intFromEnum(input.Key.digit_9)) { - return VK_0 + @as(UINT, @intCast(key_int - @intFromEnum(input.Key.digit_0))); - } - if (key_int >= @intFromEnum(input.Key.numpad_0) and key_int <= @intFromEnum(input.Key.numpad_9)) { - return VK_NUMPAD0 + @as(UINT, @intCast(key_int - @intFromEnum(input.Key.numpad_0))); - } - if (key_int >= @intFromEnum(input.Key.f1) and key_int <= @intFromEnum(input.Key.f24)) { - return VK_F1 + @as(UINT, @intCast(key_int - @intFromEnum(input.Key.f1))); - } - - return switch (key) { - .backspace => VK_BACK, - .tab => VK_TAB, - .enter, .numpad_enter => VK_RETURN, - .escape => VK_ESCAPE, - .space => VK_SPACE, - .page_up, .numpad_page_up => VK_PRIOR, - .page_down, .numpad_page_down => VK_NEXT, - .end, .numpad_end => VK_END, - .home, .numpad_home => VK_HOME, - .arrow_left, .numpad_left => VK_LEFT, - .arrow_up, .numpad_up => VK_UP, - .arrow_right, .numpad_right => VK_RIGHT, - .arrow_down, .numpad_down => VK_DOWN, - .print_screen => VK_SNAPSHOT, - .insert, .numpad_insert => VK_INSERT, - .delete, .numpad_delete => VK_DELETE, - .meta_left => VK_LWIN, - .meta_right => VK_RWIN, - .context_menu => VK_APPS, - .numpad_multiply => VK_MULTIPLY, - .numpad_add => VK_ADD, - .numpad_separator => VK_SEPARATOR, - .numpad_subtract => VK_SUBTRACT, - .numpad_decimal => VK_DECIMAL, - .numpad_divide => VK_DIVIDE, - .num_lock => VK_NUMLOCK, - .scroll_lock => VK_SCROLL, - .semicolon => VK_OEM_1, - .equal => VK_OEM_PLUS, - .comma => VK_OEM_COMMA, - .minus => VK_OEM_MINUS, - .period => VK_OEM_PERIOD, - .slash => VK_OEM_2, - .backquote => VK_OEM_3, - .bracket_left => VK_OEM_4, - .backslash => VK_OEM_5, - .bracket_right => VK_OEM_6, - .quote => VK_OEM_7, - else => null, - }; -} +const normalizeWheelDelta = win32_input.normalizeWheelDelta; -fn hotkeyUnicodeVirtualKey(cp: u21) ?struct { vk: UINT, shift: bool } { - return switch (cp) { - 'a'...'z' => .{ .vk = VK_A + @as(UINT, @intCast(cp - 'a')), .shift = false }, - 'A'...'Z' => .{ .vk = VK_A + @as(UINT, @intCast(cp - 'A')), .shift = true }, - '0'...'9' => .{ .vk = VK_0 + @as(UINT, @intCast(cp - '0')), .shift = false }, - ')' => .{ .vk = VK_0, .shift = true }, - '!' => .{ .vk = VK_0 + 1, .shift = true }, - '@' => .{ .vk = VK_0 + 2, .shift = true }, - '#' => .{ .vk = VK_0 + 3, .shift = true }, - '$' => .{ .vk = VK_0 + 4, .shift = true }, - '%' => .{ .vk = VK_0 + 5, .shift = true }, - '^' => .{ .vk = VK_0 + 6, .shift = true }, - '&' => .{ .vk = VK_0 + 7, .shift = true }, - '*' => .{ .vk = VK_0 + 8, .shift = true }, - '(' => .{ .vk = VK_0 + 9, .shift = true }, - ' ' => .{ .vk = VK_SPACE, .shift = false }, - ';' => .{ .vk = VK_OEM_1, .shift = false }, - ':' => .{ .vk = VK_OEM_1, .shift = true }, - '=' => .{ .vk = VK_OEM_PLUS, .shift = false }, - '+' => .{ .vk = VK_OEM_PLUS, .shift = true }, - ',' => .{ .vk = VK_OEM_COMMA, .shift = false }, - '<' => .{ .vk = VK_OEM_COMMA, .shift = true }, - '-' => .{ .vk = VK_OEM_MINUS, .shift = false }, - '_' => .{ .vk = VK_OEM_MINUS, .shift = true }, - '.' => .{ .vk = VK_OEM_PERIOD, .shift = false }, - '>' => .{ .vk = VK_OEM_PERIOD, .shift = true }, - '/' => .{ .vk = VK_OEM_2, .shift = false }, - '?' => .{ .vk = VK_OEM_2, .shift = true }, - '`' => .{ .vk = VK_OEM_3, .shift = false }, - '~' => .{ .vk = VK_OEM_3, .shift = true }, - '[' => .{ .vk = VK_OEM_4, .shift = false }, - '{' => .{ .vk = VK_OEM_4, .shift = true }, - '\\' => .{ .vk = VK_OEM_5, .shift = false }, - '|' => .{ .vk = VK_OEM_5, .shift = true }, - ']' => .{ .vk = VK_OEM_6, .shift = false }, - '}' => .{ .vk = VK_OEM_6, .shift = true }, - '\'' => .{ .vk = VK_OEM_7, .shift = false }, - '"' => .{ .vk = VK_OEM_7, .shift = true }, - else => null, - }; -} +const keyPressed = win32_input.keyPressed; -fn hotkeySpecForTrigger(trigger: input.Binding.Trigger) ?GlobalHotkeySpec { - var mods = trigger.mods.binding(); - const vk = switch (trigger.key) { - .catch_all => return null, - .physical => |key| hotkeyPhysicalVirtualKey(key) orelse return null, - .unicode => |cp| unicode: { - const mapped = hotkeyUnicodeVirtualKey(cp) orelse return null; - if (mapped.shift) mods.shift = true; - break :unicode mapped.vk; - }, - }; +const currentMods = win32_input.currentMods; - return .{ - .modifiers = hotkeyModifiers(mods), - .vk = vk, - }; -} +const hotkeySpecForTrigger = win32_input.hotkeySpecForTrigger; -fn hotkeySpecEql(a: GlobalHotkeySpec, b: GlobalHotkeySpec) bool { - return a.modifiers == b.modifiers and a.vk == b.vk; -} +const hotkeySpecEql = win32_input.hotkeySpecEql; -fn hotkeyRegistrationFailureReason(err: windows.Win32Error) []const u8 { - return switch (err) { - .HOTKEY_ALREADY_REGISTERED => "already registered by another app or another noctty instance", - .ACCESS_DENIED => "access denied; hotkey may be reserved, occupied by an elevated app, or blocked by policy", - .INVALID_PARAMETER => "invalid modifier or virtual-key combination", - else => "unknown Win32 RegisterHotKey failure", - }; -} +const hotkeyRegistrationFailureReason = win32_input.hotkeyRegistrationFailureReason; fn quitTimerDelayMs(delay: configpkg.Config.Duration) UINT { const clamped_ns = @max(delay.duration, std.time.ns_per_s); @@ -23101,17 +19385,17 @@ fn quitTimerDelayMs(delay: configpkg.Config.Duration) UINT { fn mouseButtonFromMessage(msg: UINT) ?input.MouseButton { return switch (msg) { - WM_LBUTTONDOWN, WM_LBUTTONUP => .left, - WM_RBUTTONDOWN, WM_RBUTTONUP => .right, - WM_MBUTTONDOWN, WM_MBUTTONUP => .middle, + c.WM_LBUTTONDOWN, c.WM_LBUTTONUP => .left, + c.WM_RBUTTONDOWN, c.WM_RBUTTONUP => .right, + c.WM_MBUTTONDOWN, c.WM_MBUTTONUP => .middle, else => null, }; } fn mouseButtonStateFromMessage(msg: UINT) ?input.MouseButtonState { return switch (msg) { - WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN => .press, - WM_LBUTTONUP, WM_RBUTTONUP, WM_MBUTTONUP => .release, + c.WM_LBUTTONDOWN, c.WM_RBUTTONDOWN, c.WM_MBUTTONDOWN => .press, + c.WM_LBUTTONUP, c.WM_RBUTTONUP, c.WM_MBUTTONUP => .release, else => null, }; } @@ -23119,9 +19403,9 @@ fn mouseButtonStateFromMessage(msg: UINT) ?input.MouseButtonState { fn mouseModsFromWParam(wParam: WPARAM) input.Mods { const bits = @as(usize, @intCast(wParam)); var mods = currentMods(); - if ((bits & MK_SHIFT) != 0) mods.shift = true; - if ((bits & MK_CONTROL) != 0) mods.ctrl = true; - _ = bits & (MK_LBUTTON | MK_RBUTTON | MK_MBUTTON); + if ((bits & c.MK_SHIFT) != 0) mods.shift = true; + if ((bits & c.MK_CONTROL) != 0) mods.ctrl = true; + _ = bits & (c.MK_LBUTTON | c.MK_RBUTTON | c.MK_MBUTTON); return mods; } @@ -23161,273 +19445,28 @@ fn scrollbarProxyPointFromSurfaceCursor( ); } -const KeyText = struct { - utf8: [8]u8 = [_]u8{0} ** 8, - len: usize = 0, - consumed_mods: input.Mods = .{}, - unshifted_codepoint: u21 = 0, - deferred_utf16_units: usize = 0, -}; - -fn isControlCodepoint(codepoint: u21) bool { - return codepoint < 0x20 or codepoint == 0x7F; -} - -fn utf16CodeUnitCount(codepoint: u21) usize { - if (codepoint == 0) return 0; - return if (codepoint <= std.math.maxInt(u16)) 1 else 2; -} - -fn shouldDeferTextToCharMessage( - action: input.Action, - key: input.Key, - mods: input.Mods, - translated: KeyText, -) bool { - if (action == .release) return false; - // Windows reports AltGr as synthetic left Ctrl plus right Alt. The - // resulting WM_CHAR is layout text, not a Ctrl+Alt terminal chord. - const alt_gr = mods.ctrl and mods.alt and - mods.sides.ctrl == .left and mods.sides.alt == .right; - if (mods.super or ((mods.ctrl or mods.alt) and !alt_gr)) return false; - if (key.modifier()) return false; - - switch (key) { - .enter, .backspace, .tab, .escape => return false, - else => {}, - } - - if (translated.deferred_utf16_units == 0) return false; - if (translated.len > 0) return true; - if (translated.unshifted_codepoint == 0) return true; - return !isControlCodepoint(translated.unshifted_codepoint); -} - -fn shouldAuthorizeDeferredCharMessage(effect: CoreSurface.InputEffect) bool { - return effect == .ignored; -} - -const DeferredCharState = struct { - pending_units: usize = 0, - high_surrogate: ?u16 = null, - - fn authorize(self: *DeferredCharState, expected_units: usize) void { - self.pending_units = self.pending_units +| expected_units; - } - - fn clear(self: *DeferredCharState) void { - self.* = .{}; - } - - fn consumeDeadChar(self: *DeferredCharState) void { - if (self.pending_units > 0) self.pending_units -= 1; - self.high_surrogate = null; - } - - fn consumeCodeUnit( - self: *DeferredCharState, - code_unit: u16, - ime_composing: bool, - ) ?u21 { - if (ime_composing) { - self.clear(); - return null; - } - if (self.pending_units == 0) { - self.high_surrogate = null; - return null; - } - self.pending_units -= 1; - - if (self.high_surrogate) |high| { - if (!std.unicode.utf16IsLowSurrogate(code_unit)) { - self.clear(); - return null; - } - const codepoint = std.unicode.utf16DecodeSurrogatePair( - &.{ high, code_unit }, - ) catch { - self.clear(); - return null; - }; - self.high_surrogate = null; - return codepoint; - } - - if (std.unicode.utf16IsHighSurrogate(code_unit)) { - self.high_surrogate = code_unit; - return null; - } - if (std.unicode.utf16IsLowSurrogate(code_unit)) { - self.clear(); - return null; - } - - return code_unit; - } -}; - -fn charCommitEvent( - codepoint: u21, - lParam: LPARAM, - utf8_buf: *[8]u8, -) ?input.KeyEvent { - const utf8_len = std.unicode.utf8Encode(codepoint, utf8_buf) catch return null; - return .{ - .action = if (isRepeatedKey(lParam)) .repeat else .press, - .key = .unidentified, - .mods = .{}, - .unshifted_codepoint = codepoint, - .utf8 = utf8_buf[0..utf8_len], - }; -} - -fn translateKeyTextToUnicode( - vk: UINT, - scan_code: UINT, - state: *const [256]u8, - utf16: *[4]u16, -) i32 { - // TranslateMessage owns the stateful dead-key composition path. This - // helper only probes text metadata for key events, so it must not consume - // or reset the layout's pending dead key. - return ToUnicode(vk, scan_code, state, utf16, utf16.len, TO_UNICODE_NO_STATE_CHANGE); -} - -fn translateKeyText( - vk: UINT, - lParam: LPARAM, - mods: input.Mods, - keyboard_state: ?*const [256]u8, -) KeyText { - const state = keyboard_state orelse { - const unshifted = unshiftedCodepointForVirtualKey(vk); - return .{ - .unshifted_codepoint = unshifted, - .deferred_utf16_units = utf16CodeUnitCount(unshifted), - }; - }; - - var utf16: [4]u16 = [_]u16{0} ** 4; - const count = translateKeyTextToUnicode(vk, scanCodeFromLParam(lParam), state, &utf16); - if (count < 0) { - return .{ - .unshifted_codepoint = unshiftedCodepointForVirtualKey(vk), - .deferred_utf16_units = 1, - }; - } - if (count == 0) { - return .{ .unshifted_codepoint = unshiftedCodepointForVirtualKey(vk) }; - } - - var result: KeyText = .{ - .unshifted_codepoint = unshiftedCodepointForVirtualKey(vk), - .deferred_utf16_units = @intCast(count), - }; - - const codepoint: u21 = cp: { - if (count >= 2 and std.unicode.utf16IsHighSurrogate(utf16[0]) and std.unicode.utf16IsLowSurrogate(utf16[1])) { - break :cp std.unicode.utf16DecodeSurrogatePair(&.{ utf16[0], utf16[1] }) catch return result; - } - if (utf16[0] < 0x20 or utf16[0] == 0x7F) return result; - break :cp utf16[0]; - }; - - result.len = std.unicode.utf8Encode(codepoint, &result.utf8) catch 0; - if (result.len > 0) { - result.consumed_mods = .{ - .shift = mods.shift, - }; - } - - return result; -} - -const Win32KeyMessage = struct { - event: input.KeyEvent, - deferred_utf16_units: usize = 0, -}; - -fn packetKeyMessage(action: input.Action) Win32KeyMessage { - const commit_pending = action != .release; - return .{ - .event = .{ - .action = action, - .key = .unidentified, - .mods = .{}, - .consumed_mods = .{}, - .unshifted_codepoint = 0, - .utf8 = "", - .composing = commit_pending, - }, - .deferred_utf16_units = if (commit_pending) 1 else 0, - }; -} +const shouldAuthorizeDeferredCharMessage = win32_input.shouldAuthorizeDeferredCharMessage; -fn keyEventFromWin32Message( - msg: UINT, - wParam: WPARAM, - lParam: LPARAM, -) ?Win32KeyMessage { - const action: input.Action = switch (msg) { - WM_KEYUP, WM_SYSKEYUP => .release, - WM_KEYDOWN, WM_SYSKEYDOWN => if (isRepeatedKey(lParam)) .repeat else .press, - else => return null, - }; +const DeferredCharState = win32_input.DeferredCharState; - const vk: UINT = @intCast(wParam & 0xFFFF); - // KEYEVENTF_UNICODE arrives as VK_PACKET followed by one WM_CHAR UTF-16 - // code unit. Authorize that unit explicitly without consulting live - // keyboard modifiers or ToUnicode; both belong to physical-key handling. - if (vk == VK_PACKET) return packetKeyMessage(action); - - const key = keyFromVirtualKey(vk, lParam); - var keyboard_state_storage: [256]u8 = [_]u8{0} ** 256; - const keyboard_state = currentKeyboardState(&keyboard_state_storage); - const mods = currentModsFromKeyboardState(keyboard_state); - - var result: Win32KeyMessage = .{ .event = .{ - .action = action, - .key = key, - .mods = mods, - .unshifted_codepoint = unshiftedCodepointForVirtualKey(vk), - } }; - - if (action != .release) { - const translated = translateKeyText(vk, lParam, mods, keyboard_state); - result.event.utf8 = translated.utf8[0..translated.len]; - result.event.consumed_mods = translated.consumed_mods; - if (translated.unshifted_codepoint != 0) { - result.event.unshifted_codepoint = translated.unshifted_codepoint; - } - if (shouldDeferTextToCharMessage(action, key, mods, translated)) { - // Keep the physical-key event visible to bindings/modifier state - // but defer text emission to WM_CHAR so plain typing doesn't rely - // on ToUnicode/GetKeyboardState timing. - result.event.utf8 = ""; - result.event.consumed_mods = .{}; - result.event.composing = true; - result.deferred_utf16_units = translated.deferred_utf16_units; - } - } +const charCommitEvent = win32_input.charCommitEvent; - return result; -} +const keyEventFromWin32Message = win32_input.keyEventFromWin32Message; fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { - if (msg == WM_NCCREATE) { + if (msg == c.WM_NCCREATE) { const cs: *const CREATESTRUCTW = @ptrFromInt(@as(usize, @bitCast(lParam))); if (cs.lpCreateParams) |ptr| { - _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, @as(LONG_PTR, @intCast(@intFromPtr(ptr)))); + setWindowData(hwnd, ptr); } } const surface = getSurface(hwnd); switch (msg) { - WM_WINHOSTTY_WAKE => return 0, + c.WM_WINHOSTTY_WAKE => return 0, - WM_WINHOSTTY_UIA_QUERY_REFRESH => { + c.WM_WINHOSTTY_UIA_QUERY_REFRESH => { if (surface) |v| { v.drainTerminalAccessibilityOutput(); if (v.terminal_accessibility) |session| session.handleQueryRefresh(wParam != 0); @@ -23435,12 +19474,12 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w return 0; }, - WM_SETFOCUS => { + c.WM_SETFOCUS => { if (surface) |v| v.focusChanged(true); return 0; }, - WM_KILLFOCUS => { + c.WM_KILLFOCUS => { if (surface) |v| { v.deferred_char.clear(); v.focusChanged(false); @@ -23448,32 +19487,32 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w return 0; }, - WM_SIZE => { + c.WM_SIZE => { if (surface) |v| v.windowSizeChanged(); return 0; }, - WM_TIMER => { - if (wParam == TERMINAL_UIA_TIMER_ID) { + c.WM_TIMER => { + if (wParam == c.TERMINAL_UIA_TIMER_ID) { if (surface) |v| { v.drainTerminalAccessibilityOutput(); if (v.terminal_accessibility) |session| session.handleTimer(); } return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_GETOBJECT => { + c.WM_GETOBJECT => { if (surface) |v| { // UIA may query the child HWND reentrantly while DestroyWindow // is unwinding. Surface.destroy clears this flag before it // releases the owner context, so never recreate that context // once teardown has begun (or before core init completes). - if (!v.destroy_on_wm_destroy) return DefWindowProcW(hwnd, msg, wParam, lParam); + if (!v.destroy_on_wm_destroy) return sys.DefWindowProcW(hwnd, msg, wParam, lParam); const provider = v.terminalUiaProvider() catch |err| { log.warn("uia: terminal provider init failed err={}", .{err}); - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }; if (win32_uia.returnTerminalProvider( hwnd, @@ -23482,32 +19521,32 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w provider, )) |lr| return lr; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_GETMINMAXINFO => { + c.WM_GETMINMAXINFO => { if (surface) |v| { v.updateMinMaxInfo(lParam); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_KEYDOWN, WM_SYSKEYDOWN, WM_KEYUP, WM_SYSKEYUP => { + c.WM_KEYDOWN, c.WM_SYSKEYDOWN, c.WM_KEYUP, c.WM_SYSKEYUP => { if (surface) |v| { // VK_APPS (Menu key) -> show context menu when not mouse reporting const vk: UINT = @intCast(wParam & 0xFFFF); - if (vk == VK_APPS and (msg == WM_KEYDOWN or msg == WM_SYSKEYDOWN)) { + if (vk == c.VK_APPS and (msg == c.WM_KEYDOWN or msg == c.WM_SYSKEYDOWN)) { if (!v.core_initialized or v.core_surface.io.terminal.flags.mouse_event == .none) { if (v.host) |h| { // Keyboard invoke: use center of surface var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) != 0) { + if (sys.GetClientRect(hwnd, &rect) != 0) { var pt: POINT = .{ .x = @divTrunc(rect.right, 2), .y = @divTrunc(rect.bottom, 2), }; - _ = ClientToScreen(hwnd, &pt); + _ = sys.ClientToScreen(hwnd, &pt); h.showContextMenu(pt.x, pt.y); } } @@ -23517,10 +19556,10 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w v.handleKeyMessage(msg, wParam, lParam); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_IME_STARTCOMPOSITION => { + c.WM_IME_STARTCOMPOSITION => { if (surface) |v| { v.deferred_char.clear(); v.ime_composing = true; @@ -23529,23 +19568,21 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w return 0; }, - WM_IME_COMPOSITION => { + c.WM_IME_COMPOSITION => { if (surface) |v| { v.deferred_char.clear(); - // Handle finalized (committed) text first - if ((@as(u32, @intCast(lParam)) & GCS_RESULTSTR) != 0) { + if ((@as(u32, @intCast(lParam)) & c.GCS_RESULTSTR) != 0) { v.handleImeResult(); } - // Then handle in-progress composition text - if ((@as(u32, @intCast(lParam)) & GCS_COMPSTR) != 0) { + if ((@as(u32, @intCast(lParam)) & c.GCS_COMPSTR) != 0) { v.handleImeComposition(); } return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_IME_ENDCOMPOSITION => { + c.WM_IME_ENDCOMPOSITION => { if (surface) |v| { v.deferred_char.clear(); v.ime_composing = false; @@ -23554,42 +19591,42 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w return 0; }, - WM_IME_SETCONTEXT => { - // Mask out the default composition window; we handle preedit via the core renderer - const masked_lParam = lParam & ~ISC_SHOWUICOMPOSITIONWINDOW; - return DefWindowProcW(hwnd, msg, wParam, masked_lParam); + c.WM_IME_SETCONTEXT => { + // We handle preedit via the core renderer. + const masked_lParam = lParam & ~c.ISC_SHOWUICOMPOSITIONWINDOW; + return sys.DefWindowProcW(hwnd, msg, wParam, masked_lParam); }, - WM_CHAR => { + c.WM_CHAR => { if (surface) |v| { v.handleCharMessage(wParam, lParam); } return 0; }, - WM_DEADCHAR, WM_SYSDEADCHAR => { + c.WM_DEADCHAR, c.WM_SYSDEADCHAR => { if (surface) |v| v.deferred_char.consumeDeadChar(); return 0; }, - WM_MOUSEMOVE => { + c.WM_MOUSEMOVE => { if (surface) |v| { if (v.handleScrollbarProxyMouseMove(hwnd, lParam)) return 0; v.handleMouseMove(lParam, mouseModsFromWParam(wParam)); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_MOUSELEAVE => { + c.WM_MOUSELEAVE => { if (surface) |v| { v.handleScrollbarMouseLeave(); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_RBUTTONUP => { + c.WM_RBUTTONUP => { if (surface) |v| { // If terminal has mouse reporting active and Shift is not held, // pass through as a mouse event. Shift+Right-Click always opens @@ -23600,38 +19637,38 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w return 0; } // Otherwise show context menu - _ = ReleaseCapture(); + _ = sys.ReleaseCapture(); if (v.host) |h| { var pt: POINT = .{ .x = signedLowWord(lParamBits(lParam)), .y = signedHighWord(lParamBits(lParam)), }; - _ = ClientToScreen(hwnd, &pt); + _ = sys.ClientToScreen(hwnd, &pt); h.showContextMenu(pt.x, pt.y); } return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_LBUTTONDOWN, WM_LBUTTONUP => { + c.WM_LBUTTONDOWN, c.WM_LBUTTONUP => { if (surface) |v| { if (v.handleScrollbarProxyButton(hwnd, msg, lParam)) return 0; v.handleMouseButton(msg, wParam, lParam); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_RBUTTONDOWN, WM_MBUTTONDOWN, WM_MBUTTONUP => { + c.WM_RBUTTONDOWN, c.WM_MBUTTONDOWN, c.WM_MBUTTONUP => { if (surface) |v| { v.handleMouseButton(msg, wParam, lParam); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_XBUTTONDOWN, WM_XBUTTONUP => { + c.WM_XBUTTONDOWN, c.WM_XBUTTONUP => { if (surface) |v| { v.handleXMouseButton(msg, wParam, lParam); } @@ -23639,14 +19676,14 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w return 1; }, - WM_DROPFILES => { + c.WM_DROPFILES => { if (surface) |v| { v.handleDropFiles(wParam); } return 0; }, - WM_MOUSEWHEEL, WM_POINTERWHEEL => { + c.WM_MOUSEWHEEL, c.WM_POINTERWHEEL => { if (surface) |v| { v.handleMouseWheel(normalizeWheelDelta( v.wheelNormalizationContext(), @@ -23655,10 +19692,10 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w )); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_MOUSEHWHEEL, WM_POINTERHWHEEL => { + c.WM_MOUSEHWHEEL, c.WM_POINTERHWHEEL => { if (surface) |v| { v.handleMouseWheel(normalizeWheelDelta( v.wheelNormalizationContext(), @@ -23667,20 +19704,20 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w )); return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_ERASEBKGND => return 1, + c.WM_ERASEBKGND => return 1, - WM_SETTINGCHANGE => { + c.WM_SETTINGCHANGE => { if (surface) |v| v.app.refreshSystemWheelSettings(); - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_SETCURSOR => { + c.WM_SETCURSOR => { if (surface) |v| { - if (lowWord(@as(usize, @intCast(lParam))) == HTCLIENT) { - const now_ms = GetTickCount64(); + if (lowWord(@as(usize, @intCast(lParam))) == c.HTCLIENT) { + const now_ms = sys.GetTickCount64(); const was_hovered = v.scrollbar_state.hovered; const was_visibility = v.scrollbar_state.visibility; const over_scrollbar = v.updateScrollbarHoverFromScreenCursor(hwnd, now_ms); @@ -23693,7 +19730,7 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w }; } if (over_scrollbar or v.scrollbar_state.dragging) { - _ = SetCursor(LoadCursorW(null, IDC_ARROW)); + _ = sys.SetCursor(sys.LoadCursorW(null, c.IDC_ARROW)); return 1; } if (v.applyCursor()) { @@ -23701,28 +19738,28 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w } } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_CAPTURECHANGED => { + c.WM_CAPTURECHANGED => { if (surface) |v| v.handleScrollbarCaptureChanged(); - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - WM_PAINT => { + c.WM_PAINT => { var ps: PAINTSTRUCT = undefined; - _ = BeginPaint(hwnd, &ps) orelse return 0; - defer _ = EndPaint(hwnd, &ps); + _ = sys.BeginPaint(hwnd, &ps) orelse return 0; + defer _ = sys.EndPaint(hwnd, &ps); if (surface) |v| { const draw_content = v.shouldDrawRendererPaintContent(); v.paint_pending = false; if (draw_content) { - const paint_start_ms = if (v.render_trace.enabled()) GetTickCount64() else 0; + const paint_start_ms = if (v.render_trace.enabled()) sys.GetTickCount64() else 0; v.render_trace.notePaintDraw(); defer v.finishRendererRepaintRequest(); defer if (paint_start_ms != 0) { - const paint_end_ms = GetTickCount64(); + const paint_end_ms = sys.GetTickCount64(); if (paint_end_ms > paint_start_ms) { v.render_trace.notePaintDrawDuration(paint_end_ms - paint_start_ms); } @@ -23739,12 +19776,12 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w return 0; }, - WM_CLOSE => { + c.WM_CLOSE => { if (surface) |v| _ = v.requestClose(); return 0; }, - WM_DESTROY => { + c.WM_DESTROY => { if (surface) |v| { if (v.destroy_on_wm_destroy) { v.destroy(); @@ -23755,14 +19792,12 @@ fn windowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.w return 0; }, - else => return DefWindowProcW(hwnd, msg, wParam, lParam), + else => return sys.DefWindowProcW(hwnd, msg, wParam, lParam), } } fn getSurface(hwnd: HWND) ?*Surface { - const raw = GetWindowLongPtrW(hwnd, GWLP_USERDATA); - if (raw == 0) return null; - return @ptrFromInt(@as(usize, @intCast(raw))); + return windowData(Surface, hwnd); } fn terminalAccessibilityHwnd(ctx: *anyopaque) ?HWND { @@ -24097,7 +20132,7 @@ pub const Surface = struct { self.drop_target.surface_ctx = @ptrCast(self); self.background_opacity_default = normalizedBackgroundOpacity(config.@"background-opacity"); - const hwnd = CreateWindowExW( + const hwnd = sys.CreateWindowExW( 0, class_name, std.unicode.utf8ToUtf16LeStringLiteral(""), @@ -24110,8 +20145,8 @@ pub const Surface = struct { null, app.hinstance, self, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); - errdefer _ = DestroyWindow(hwnd); + ) orelse return lastError(); + errdefer _ = sys.DestroyWindow(hwnd); self.hwnd = hwnd; // Register the OLE drop target so Explorer / browser drops @@ -24249,9 +20284,9 @@ pub const Surface = struct { // rendering before activation ordering runs. const activate_during_init = shouldShowSurfaceImmediately(opts.host_id); self.setVisible(activate_during_init); - DragAcceptFiles(hwnd, 1); + sys.DragAcceptFiles(hwnd, 1); - if (GetFocus() == hwnd) { + if (sys.GetFocus() == hwnd) { self.window_focused = true; } @@ -24369,7 +20404,7 @@ pub const Surface = struct { // Detached structural-history objects are deliberately outside // active ShellState and can be destroyed during history disposal. if (self.host) |host| if (host.structural_history_disposing) { - return DestroyWindow(hwnd) != 0; + return sys.DestroyWindow(hwnd) != 0; }; log.err("refusing unpreflighted surface close: native tab mapping missing", .{}); return false; @@ -24413,7 +20448,7 @@ pub const Surface = struct { next_tree = null; self.pending_shell_close = shell_close; shell_close = null; - if (DestroyWindow(hwnd) != 0) return true; + if (sys.DestroyWindow(hwnd) != 0) return true; if (self.pending_close_tree) |*tree| tree.deinit(); self.pending_close_tree = null; @@ -24494,7 +20529,7 @@ pub const Surface = struct { .clear_screen => .{ .clear_screen = .{ .state_bytes = state_bytes } }, .reset => .{ .reset = .{ .state_bytes = state_bytes } }, }, - .timestamp_ms = GetTickCount64(), + .timestamp_ms = sys.GetTickCount64(), .sequence_id = self.app.nextUndoSequence(), }; errdefer entry.deinit(self.app.core_app.alloc); @@ -24659,13 +20694,13 @@ pub const Surface = struct { fn presentWindow(self: *Surface) void { if (self.host) |host| { host.present(); - if (self.hwnd) |hwnd| _ = SetFocus(hwnd); + if (self.hwnd) |hwnd| _ = sys.SetFocus(hwnd); return; } const hwnd = self.hwnd orelse return; - _ = ShowWindow(hwnd, SW_SHOW); - _ = SetForegroundWindow(hwnd); - _ = SetFocus(hwnd); + _ = sys.ShowWindow(hwnd, c.SW_SHOW); + _ = sys.SetForegroundWindow(hwnd); + _ = sys.SetFocus(hwnd); } fn windowHwnd(self: *const Surface) ?HWND { @@ -24689,8 +20724,8 @@ pub const Surface = struct { .capture = terminalAccessibilityCapture, .defer_provider_release = terminalAccessibilityDeferProviderRelease, }, - WM_WINHOSTTY_UIA_QUERY_REFRESH, - TERMINAL_UIA_TIMER_ID, + c.WM_WINHOSTTY_UIA_QUERY_REFRESH, + c.TERMINAL_UIA_TIMER_ID, ); } const provider = try self.terminal_accessibility.?.acquireProvider(); @@ -24942,19 +20977,19 @@ pub const Surface = struct { self.paint_pending = true; errdefer self.paint_pending = false; - if (InvalidateRect(hwnd, null, 0) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.InvalidateRect(hwnd, null, 0) == 0) { + return lastError(); } - if (update_now) _ = UpdateWindow(hwnd); + if (update_now) _ = sys.UpdateWindow(hwnd); } fn forcePaintRequestNow(self: *Surface) !void { const hwnd = self.hwnd orelse return error.NoWindow; self.render_trace.noteForcePaintNow(); self.paint_pending = false; - const flags: UINT = RDW_INVALIDATE | RDW_INTERNALPAINT | RDW_UPDATENOW; - if (RedrawWindow(hwnd, null, null, flags) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + const flags: UINT = c.RDW_INVALIDATE | c.RDW_INTERNALPAINT | c.RDW_UPDATENOW; + if (sys.RedrawWindow(hwnd, null, null, flags) == 0) { + return lastError(); } } @@ -25017,7 +21052,7 @@ pub const Surface = struct { pub fn redraw(self: *Surface) !void { if (!self.core_initialized) return; const hwnd = self.hwnd orelse return; - if (IsWindowVisible(hwnd) == 0) return; + if (sys.IsWindowVisible(hwnd) == 0) return; if (self.size.width == 0 or self.size.height == 0) return; if (self.hdc == null or self.hglrc == null) return; if (self.draw_in_progress) return; @@ -25053,11 +21088,11 @@ pub const Surface = struct { return error.NoOpenGLContext; }; - if (wglGetCurrentContext() == hglrc and wglGetCurrentDC() == hdc) { + if (sys.wglGetCurrentContext() == hglrc and sys.wglGetCurrentDC() == hdc) { return; } - if (wglMakeCurrent(hdc, hglrc) == 0) { + if (sys.wglMakeCurrent(hdc, hglrc) == 0) { const err = windows.kernel32.GetLastError(); recordOpenGLStartupWin32Failure(.make_current, err); return windows.unexpectedError(err); @@ -25068,18 +21103,18 @@ pub const Surface = struct { const hdc = self.hdc orelse return; const hglrc = self.hglrc orelse return; - if (wglGetCurrentContext() != hglrc or wglGetCurrentDC() != hdc) { + if (sys.wglGetCurrentContext() != hglrc or sys.wglGetCurrentDC() != hdc) { return; } - _ = wglMakeCurrent(null, null); + _ = sys.wglMakeCurrent(null, null); } pub fn swapGLBuffers(self: *Surface) !void { const hdc = self.hdc orelse return error.NoDeviceContext; _ = self.hglrc orelse return error.NoOpenGLContext; - if (SwapBuffers(hdc) == 0) { + if (sys.SwapBuffers(hdc) == 0) { const err = windows.kernel32.GetLastError(); return windows.unexpectedError(err); } @@ -25174,8 +21209,7 @@ pub const Surface = struct { return self.scrollbar.total > self.scrollbar.len and self.scrollbar.len > 0; } - fn scrollbarAnimationsEnabled(self: *const Surface) bool { - _ = self; + fn scrollbarAnimationsEnabled() bool { return Host.clientAnimationsEnabled() and !isHighContrastActive(); } @@ -25288,11 +21322,11 @@ pub const Surface = struct { const host = self.host orelse return; const hwnd = host.hwnd orelse return; - self.scrollbar_hwnd = CreateWindowExW( - WS_EX_LAYERED, + self.scrollbar_hwnd = sys.CreateWindowExW( + c.WS_EX_LAYERED, scrollbar_class_name, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_CHILD, + c.WS_CHILD, 0, 0, self.scrollbarHoverWidthPx(), @@ -25301,28 +21335,24 @@ pub const Surface = struct { null, self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); const scrollbar_hwnd = self.scrollbar_hwnd.?; - _ = SetWindowLongPtrW( - scrollbar_hwnd, - GWLP_USERDATA, - @as(LONG_PTR, @intCast(@intFromPtr(self))), - ); - if (SetLayeredWindowAttributes( + setWindowData(scrollbar_hwnd, self); + if (sys.SetLayeredWindowAttributes( scrollbar_hwnd, scrollbar_transparent_key, 0, - LWA_ALPHA | LWA_COLORKEY, + c.LWA_ALPHA | c.LWA_COLORKEY, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } fn destroyScrollbarWindow(self: *Surface) void { - if (self.scrollbar_state.dragging) _ = ReleaseCapture(); + if (self.scrollbar_state.dragging) _ = sys.ReleaseCapture(); self.clearScrollbarMarkers(); - if (self.scrollbar_hwnd) |hwnd| _ = DestroyWindow(hwnd); + if (self.scrollbar_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); self.scrollbar_hwnd = null; self.scrollbar_placement = .{}; self.scrollbar_leave_armed = false; @@ -25334,11 +21364,11 @@ pub const Surface = struct { } fn invalidateScrollbarWindow(self: *Surface) void { - if (self.scrollbar_hwnd) |hwnd| _ = InvalidateRect(hwnd, null, 0); + if (self.scrollbar_hwnd) |hwnd| _ = sys.InvalidateRect(hwnd, null, 0); } fn settleReducedScrollbarState(self: *Surface, now_ms: u64) void { - if (self.scrollbarAnimationsEnabled()) return; + if (scrollbarAnimationsEnabled()) return; switch (self.scrollbar_state.visibility) { .fading_in, .fading_out => _ = self.scrollbar_state.tick( now_ms + win32_scrollbar_geometry.auto_hide_fade_ms, @@ -25357,11 +21387,11 @@ pub const Surface = struct { if (self.scrollbar_leave_armed and self.scrollbar_leave_hwnd == hwnd) return; var track: TRACKMOUSEEVENT = .{ .cbSize = @sizeOf(TRACKMOUSEEVENT), - .dwFlags = TME_LEAVE, + .dwFlags = c.TME_LEAVE, .hwndTrack = hwnd, .dwHoverTime = 0, }; - _ = TrackMouseEvent(&track); + _ = sys.TrackMouseEvent(&track); self.scrollbar_leave_armed = true; self.scrollbar_leave_hwnd = hwnd; } @@ -25393,7 +21423,7 @@ pub const Surface = struct { } fn handleScrollbarMouseMoveLocal(self: *Surface, tracking_hwnd: HWND, x: f32, y: f32) void { - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); _ = self.updateScrollbarFromLocalCursor(tracking_hwnd, x, y, now_ms); self.syncScrollbarWindowNow(now_ms) catch |err| { log.warn("win32 scrollbar move failed err={}", .{err}); @@ -25402,7 +21432,7 @@ pub const Surface = struct { fn handleScrollbarLeftButtonDownLocal(self: *Surface, hwnd: HWND, x: f32, y: f32) void { const layout = self.scrollbarLocalLayout() orelse return; - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); const thumb = win32_scrollbar_geometry.thumbRect(layout); self.setScrollbarHovered(true, now_ms); self.setScrollbarDragging(true, now_ms); @@ -25411,7 +21441,7 @@ pub const Surface = struct { std.math.clamp(y - thumb.top, 0.0, thumb.bottom - thumb.top) else @max(0.0, (thumb.bottom - thumb.top) / 2.0); - _ = SetCapture(hwnd); + _ = sys.SetCapture(hwnd); self.scrollbarScrollToRow(win32_scrollbar_geometry.rowFromCursor( layout, y, @@ -25425,9 +21455,9 @@ pub const Surface = struct { } fn handleScrollbarLeftButtonUpLocal(self: *Surface, x: f32, y: f32) void { - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); const layout = self.scrollbarLocalLayout(); - _ = ReleaseCapture(); + _ = sys.ReleaseCapture(); self.setScrollbarDragging(false, now_ms); if (layout) |value| { self.setScrollbarHovered( @@ -25462,7 +21492,7 @@ pub const Surface = struct { self.placement.rect_known and self.placement.visible; if (!should_exist or policy == .never) { - if (self.scrollbar_state.dragging) _ = ReleaseCapture(); + if (self.scrollbar_state.dragging) _ = sys.ReleaseCapture(); self.setScrollbarHovered(false, now_ms); self.setScrollbarDragging(false, now_ms); self.scrollbar_state.visibility = .hidden; @@ -25501,7 +21531,7 @@ pub const Surface = struct { const alpha: BYTE = switch (policy) { .never => 0, .always => 255, - .dynamic => alphaByteForOpacity(if (self.scrollbarAnimationsEnabled()) + .dynamic => alphaByteForOpacity(if (scrollbarAnimationsEnabled()) @as(f64, @floatCast(self.scrollbar_state.alpha(now_ms))) else if (self.scrollbar_state.visibility == .hidden) @as(f64, @floatFromInt(hiddenScrollbarAlphaByte())) / 255.0 @@ -25511,13 +21541,13 @@ pub const Surface = struct { const paint_key = self.scrollbarPaintKey(child_rect); const paint_changed = scrollbarPaintKeyChanged(self.scrollbar_paint_cache, paint_key); if (alpha != self.scrollbar_alpha_cache) { - if (SetLayeredWindowAttributes( + if (sys.SetLayeredWindowAttributes( scrollbar_hwnd, scrollbar_transparent_key, alpha, - LWA_ALPHA | LWA_COLORKEY, + c.LWA_ALPHA | c.LWA_COLORKEY, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } self.scrollbar_alpha_cache = alpha; } @@ -25532,7 +21562,7 @@ pub const Surface = struct { } fn refreshScrollbarWindow(self: *Surface) !void { - try self.syncScrollbarWindowNow(GetTickCount64()); + try self.syncScrollbarWindowNow(sys.GetTickCount64()); } fn scrollbarNeedsHeartbeat(self: *const Surface) bool { @@ -25585,7 +21615,7 @@ pub const Surface = struct { self.scrollbar_leave_armed = false; self.scrollbar_leave_hwnd = null; if (self.scrollbar_state.dragging) return; - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); self.setScrollbarHovered(false, now_ms); self.syncScrollbarWindowNow(now_ms) catch |err| { log.warn("win32 scrollbar leave failed err={}", .{err}); @@ -25609,8 +21639,8 @@ pub const Surface = struct { if (self.scrollbarPolicy() == .never or !self.placement.rect_known) return null; var point: POINT = .{ .x = 0, .y = 0 }; - if (GetCursorPos(&point) == 0) return null; - if (ScreenToClient(hwnd, &point) == 0) return null; + if (sys.GetCursorPos(&point) == 0) return null; + if (sys.ScreenToClient(hwnd, &point) == 0) return null; return scrollbarProxyPointFromSurfaceCoords( self.placement.rect, @@ -25658,7 +21688,7 @@ pub const Surface = struct { } if (self.scrollbar_state.hovered) { - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); self.setScrollbarHovered(false, now_ms); self.syncScrollbarWindowNow(now_ms) catch |err| { log.warn("win32 scrollbar proxy leave failed err={}", .{err}); @@ -25677,12 +21707,12 @@ pub const Surface = struct { lParam, ); return switch (msg) { - WM_LBUTTONDOWN => blk: { + c.WM_LBUTTONDOWN => blk: { if (!proxy.over_band) break :blk false; self.handleScrollbarLeftButtonDownLocal(hwnd, proxy.local_x, proxy.local_y); break :blk true; }, - WM_LBUTTONUP => blk: { + c.WM_LBUTTONUP => blk: { if (!self.scrollbar_state.dragging and !proxy.over_band) break :blk false; self.handleScrollbarLeftButtonUpLocal(proxy.local_x, proxy.local_y); break :blk true; @@ -25693,7 +21723,7 @@ pub const Surface = struct { fn handleScrollbarCaptureChanged(self: *Surface) void { if (!self.scrollbar_state.dragging) return; - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); self.setScrollbarDragging(false, now_ms); self.syncScrollbarWindowNow(now_ms) catch |err| { log.warn("win32 scrollbar capture reset failed err={}", .{err}); @@ -25703,11 +21733,11 @@ pub const Surface = struct { fn paintScrollbar(self: *Surface) void { const hwnd = self.scrollbar_hwnd orelse return; var ps: PAINTSTRUCT = undefined; - const hdc = BeginPaint(hwnd, &ps) orelse return; - defer _ = EndPaint(hwnd, &ps); + const hdc = sys.BeginPaint(hwnd, &ps) orelse return; + defer _ = sys.EndPaint(hwnd, &ps); var client_rect: RECT = undefined; - if (GetClientRect(hwnd, &client_rect) == 0) return; + if (sys.GetClientRect(hwnd, &client_rect) == 0) return; fillSolidRect(hdc, client_rect, scrollbar_transparent_key); if (!self.scrollbarScrollable()) return; @@ -25778,16 +21808,16 @@ pub const Surface = struct { fn applySearchBarFont(self: *Surface, font: ?*anyopaque) void { const value = if (font) |f| @intFromPtr(f) else 0; - if (self.search_bar_edit_hwnd) |hwnd| _ = SendMessageW(hwnd, WM_SETFONT, value, 1); + if (self.search_bar_edit_hwnd) |hwnd| _ = sys.SendMessageW(hwnd, c.WM_SETFONT, value, 1); for (search_bar_button_roles) |role| { - if (self.searchBarButtonHwnd(role)) |hwnd| _ = SendMessageW(hwnd, WM_SETFONT, value, 1); + if (self.searchBarButtonHwnd(role)) |hwnd| _ = sys.SendMessageW(hwnd, c.WM_SETFONT, value, 1); } - if (self.search_bar_results_hwnd) |hwnd| _ = SendMessageW(hwnd, WM_SETFONT, value, 1); + if (self.search_bar_results_hwnd) |hwnd| _ = sys.SendMessageW(hwnd, c.WM_SETFONT, value, 1); } fn invalidateSearchButtons(self: *Surface) void { for (search_bar_button_roles) |role| { - if (self.searchBarButtonHwnd(role)) |hwnd| _ = InvalidateRect(hwnd, null, 0); + if (self.searchBarButtonHwnd(role)) |hwnd| _ = sys.InvalidateRect(hwnd, null, 0); } } @@ -25805,7 +21835,7 @@ pub const Surface = struct { self.search_bar_results_hwnd, }; for (handles) |maybe_hwnd| { - if (maybe_hwnd) |hwnd| _ = InvalidateRect(hwnd, null, 0); + if (maybe_hwnd) |hwnd| _ = sys.InvalidateRect(hwnd, null, 0); } } @@ -25813,7 +21843,7 @@ pub const Surface = struct { if (rect.right <= rect.left or rect.bottom <= rect.top) return; const host = self.host orelse return; const hwnd = host.hwnd orelse return; - _ = InvalidateRect(hwnd, &rect, 0); + _ = sys.InvalidateRect(hwnd, &rect, 0); } fn invalidateSearchBarFrame(self: *Surface) void { @@ -25873,11 +21903,11 @@ pub const Surface = struct { } fn createSearchBarButton(self: *Surface, host: *Host, parent: HWND, role: SearchBarButtonRole) !HWND { - const hwnd = CreateWindowExW( + const hwnd = sys.CreateWindowExW( 0, prompt_button_class, searchBarButtonLabel(role), - WS_CHILD | WS_TABSTOP | BS_OWNERDRAW, + c.WS_CHILD | c.WS_TABSTOP | c.BS_OWNERDRAW, 0, 0, 28, @@ -25886,7 +21916,7 @@ pub const Surface = struct { @ptrFromInt(searchBarButtonCommandId(role)), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); host.subclassButton( hwnd, &hostButtonProc, @@ -25901,45 +21931,41 @@ pub const Surface = struct { const host = self.host orelse return; const hwnd = host.hwnd orelse return; - self.search_bar_bg_hwnd = CreateWindowExW( + self.search_bar_bg_hwnd = sys.CreateWindowExW( 0, prompt_label_class, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_CHILD | WS_CLIPSIBLINGS | SS_OWNERDRAW, + c.WS_CHILD | c.WS_CLIPSIBLINGS | c.SS_OWNERDRAW, 0, 0, 160, host_search_bar_height, hwnd, - @ptrFromInt(SEARCH_BG_ID), + @ptrFromInt(c.SEARCH_BG_ID), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); - self.search_bar_edit_hwnd = CreateWindowExW( + self.search_bar_edit_hwnd = sys.CreateWindowExW( 0, prompt_edit_class, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_CHILD | WS_TABSTOP | ES_AUTOHSCROLL, + c.WS_CHILD | c.WS_TABSTOP | c.ES_AUTOHSCROLL, 0, 0, 160, host_search_bar_height - 10, hwnd, - @ptrFromInt(SEARCH_EDIT_ID), + @ptrFromInt(c.SEARCH_EDIT_ID), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); const edit_hwnd = self.search_bar_edit_hwnd.?; - _ = SetWindowLongPtrW( - edit_hwnd, - GWLP_USERDATA, - @as(LONG_PTR, @intCast(@intFromPtr(host))), - ); - const edit_prev = SetWindowLongPtrW( + setWindowData(edit_hwnd, host); + const edit_prev = sys.SetWindowLongPtrW( edit_hwnd, - GWLP_WNDPROC, + c.GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(&searchEditProc))), ); self.search_bar_edit_prev_proc = if (edit_prev == 0) @@ -25948,13 +21974,13 @@ pub const Surface = struct { @ptrFromInt(@as(usize, @intCast(edit_prev))); const edit_margin = @as(u16, @intCast(@max(host.scaled(10), 6))); const packed_margins: isize = @intCast(@as(usize, edit_margin) | (@as(usize, edit_margin) << 16)); - _ = SendMessageW( + _ = sys.SendMessageW( edit_hwnd, - EM_SETMARGINS, - EC_LEFTMARGIN | EC_RIGHTMARGIN, + c.EM_SETMARGINS, + c.EC_LEFTMARGIN | c.EC_RIGHTMARGIN, packed_margins, ); - _ = SendMessageW(edit_hwnd, EM_SETCUEBANNER, 0, @as(LPARAM, @intCast(@intFromPtr(search_edit_cue.ptr)))); + _ = sys.SendMessageW(edit_hwnd, c.EM_SETCUEBANNER, 0, @as(LPARAM, @intCast(@intFromPtr(search_edit_cue.ptr)))); self.search_bar_edit_uia_provider = if (self.app.com_initialized) win32_uia.TerminalProvider.create( std.heap.page_allocator, @@ -25965,7 +21991,7 @@ pub const Surface = struct { break :blk null; } else blk: { - log.warn("search edit UIA provider disabled: UI thread is not a confirmed STA", .{}); + log.debug("search edit UIA provider disabled: UI thread is not a confirmed STA", .{}); break :blk null; }; @@ -25976,20 +22002,20 @@ pub const Surface = struct { self.search_bar_word_hwnd = try self.createSearchBarButton(host, hwnd, .whole_word); self.search_bar_close_hwnd = try self.createSearchBarButton(host, hwnd, .close); - self.search_bar_results_hwnd = CreateWindowExW( + self.search_bar_results_hwnd = sys.CreateWindowExW( 0, prompt_label_class, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_CHILD | SS_RIGHT | SS_CENTERIMAGE, + c.WS_CHILD | c.SS_RIGHT | c.SS_CENTERIMAGE, 0, 0, 96, 18, hwnd, - @ptrFromInt(SEARCH_RESULTS_ID), + @ptrFromInt(c.SEARCH_RESULTS_ID), self.app.hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); + ) orelse return lastError(); self.applySearchBarFont(host.chrome_font); _ = try self.syncSearchBarResultsText(); @@ -26013,15 +22039,15 @@ pub const Surface = struct { alloc.free(value); self.search_bar_results_cache = null; } - if (self.search_bar_results_hwnd) |hwnd| _ = DestroyWindow(hwnd); - if (self.search_bar_close_hwnd) |hwnd| _ = DestroyWindow(hwnd); - if (self.search_bar_word_hwnd) |hwnd| _ = DestroyWindow(hwnd); - if (self.search_bar_case_hwnd) |hwnd| _ = DestroyWindow(hwnd); - if (self.search_bar_regex_hwnd) |hwnd| _ = DestroyWindow(hwnd); - if (self.search_bar_next_hwnd) |hwnd| _ = DestroyWindow(hwnd); - if (self.search_bar_prev_hwnd) |hwnd| _ = DestroyWindow(hwnd); - if (self.search_bar_edit_hwnd) |hwnd| _ = DestroyWindow(hwnd); - if (self.search_bar_bg_hwnd) |hwnd| _ = DestroyWindow(hwnd); + if (self.search_bar_results_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); + if (self.search_bar_close_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); + if (self.search_bar_word_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); + if (self.search_bar_case_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); + if (self.search_bar_regex_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); + if (self.search_bar_next_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); + if (self.search_bar_prev_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); + if (self.search_bar_edit_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); + if (self.search_bar_bg_hwnd) |hwnd| _ = sys.DestroyWindow(hwnd); self.search_bar_results_hwnd = null; self.search_bar_close_hwnd = null; @@ -26179,7 +22205,7 @@ pub const Surface = struct { ); changed = bg_visibility_changed or changed; if (changed) { - if (self.search_bar_bg_hwnd) |bg_hwnd| _ = InvalidateRect(bg_hwnd, null, 0); + if (self.search_bar_bg_hwnd) |bg_hwnd| _ = sys.InvalidateRect(bg_hwnd, null, 0); } return changed; @@ -26196,8 +22222,8 @@ pub const Surface = struct { const value_w = try std.unicode.utf8ToUtf16LeAllocZ(self.app.core_app.alloc, value); defer self.app.core_app.alloc.free(value_w); - if (SetWindowTextW(edit_hwnd, value_w.ptr) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.SetWindowTextW(edit_hwnd, value_w.ptr) == 0) { + return lastError(); } if (self.search_bar_edit_uia_provider) |provider| { provider.raiseTextChanged(); @@ -26263,7 +22289,7 @@ pub const Surface = struct { previous_query_len, text.len, ); - try self.search_bar.setQuery(text, @intCast(GetTickCount64())); + try self.search_bar.setQuery(text, @intCast(sys.GetTickCount64())); if (invalidate_core_search) { if (text.len == 0) { _ = try self.core_surface.setSearchQuery("", self.searchQueryOptions()); @@ -26302,11 +22328,11 @@ pub const Surface = struct { } fn handleSearchToggleClick(self: *Surface, command_id: usize) !bool { - const now_ms: i64 = @intCast(GetTickCount64()); + const now_ms: i64 = @intCast(sys.GetTickCount64()); switch (command_id) { - SEARCH_REGEX_ID => self.search_bar.toggleRegex(now_ms), - SEARCH_CASE_ID => self.search_bar.toggleCase(now_ms), - SEARCH_WORD_ID => self.search_bar.toggleWord(now_ms), + c.SEARCH_REGEX_ID => self.search_bar.toggleRegex(now_ms), + c.SEARCH_CASE_ID => self.search_bar.toggleCase(now_ms), + c.SEARCH_WORD_ID => self.search_bar.toggleWord(now_ms), else => return false, } @@ -26323,7 +22349,7 @@ pub const Surface = struct { if (stop_search) { _ = try self.core_surface.endSearch(); if (refocus_terminal) { - if (self.hwnd) |hwnd| _ = SetFocus(hwnd); + if (self.hwnd) |hwnd| _ = sys.SetFocus(hwnd); } return; } @@ -26339,7 +22365,7 @@ pub const Surface = struct { try host.layout(); host.forceHostCompositionPaint(); if (refocus_terminal) { - if (self.hwnd) |hwnd| _ = SetFocus(hwnd); + if (self.hwnd) |hwnd| _ = sys.SetFocus(hwnd); } } @@ -26380,8 +22406,8 @@ pub const Surface = struct { host.forceHostCompositionPaint(); if (self.search_bar_edit_hwnd) |edit_hwnd| { - _ = SetFocus(edit_hwnd); - _ = SendMessageW(edit_hwnd, EM_SETSEL, 0, -1); + _ = sys.SetFocus(edit_hwnd); + _ = sys.SendMessageW(edit_hwnd, c.EM_SETSEL, 0, -1); } if (initial.len > 0 and (!self.search_active or !same_query)) { @@ -26419,7 +22445,7 @@ pub const Surface = struct { try h.setBanner(.none, null); } } - const status_h = h.statusBarHeight(); + const status_h = Host.statusBarHeight(); if (!chrome_invalidated and inspectorChromeVisible(h.overlay_mode, status_h)) { if (chromeTextNeedsFullInvalidation(status_h)) { h.invalidateChromeText(); @@ -26454,7 +22480,7 @@ pub const Surface = struct { fn toggleMaximize(self: *Surface) void { const hwnd = self.windowHwnd() orelse return; if (self.fullscreen) return; - _ = ShowWindow(hwnd, if (IsZoomed(hwnd) != 0) SW_RESTORE else SW_MAXIMIZE); + _ = sys.ShowWindow(hwnd, if (sys.IsZoomed(hwnd) != 0) c.SW_RESTORE else c.SW_MAXIMIZE); } fn applyRuntimeConfig(self: *Surface, config: *const configpkg.Config) !void { @@ -26482,8 +22508,8 @@ pub const Surface = struct { fn resizeSplitFallback(self: *Surface, value: apprt.action.ResizeSplit) !bool { const hwnd = self.hwnd orelse return false; var client_rect: RECT = undefined; - if (GetClientRect(hwnd, &client_rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetClientRect(hwnd, &client_rect) == 0) { + return lastError(); } const delta = resizeSplitFallbackDelta(value); @@ -26532,7 +22558,7 @@ pub const Surface = struct { fn resetWindowSize(self: *Surface) !void { const size = self.default_client_size orelse return; if (self.fullscreen) try self.leaveFullscreen(); - if (self.windowHwnd()) |hwnd| _ = ShowWindow(hwnd, SW_RESTORE); + if (self.windowHwnd()) |hwnd| _ = sys.ShowWindow(hwnd, c.SW_RESTORE); const client_size = scaleInitialClientSize(size, self.content_scale); try self.resizeClientArea(client_size.width, client_size.height); } @@ -26545,14 +22571,14 @@ pub const Surface = struct { return; } const hwnd = self.windowHwnd() orelse return; - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, null, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED, + c.SWP_NOMOVE | c.SWP_NOSIZE | c.SWP_NOZORDER | c.SWP_NOACTIVATE | c.SWP_FRAMECHANGED, ); } @@ -26575,28 +22601,28 @@ pub const Surface = struct { self.fullscreen = true; try self.applyWindowStyle(); - const monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) orelse - return windows.unexpectedError(windows.kernel32.GetLastError()); + const monitor = sys.MonitorFromWindow(hwnd, c.MONITOR_DEFAULTTONEAREST) orelse + return lastError(); var info: MONITORINFO = .{ .cbSize = @sizeOf(MONITORINFO), .rcMonitor = .{ .left = 0, .top = 0, .right = 0, .bottom = 0 }, .rcWork = .{ .left = 0, .top = 0, .right = 0, .bottom = 0 }, .dwFlags = 0, }; - if (GetMonitorInfoW(monitor, &info) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetMonitorInfoW(monitor, &info) == 0) { + return lastError(); } - if (SetWindowPos( + if (sys.SetWindowPos( hwnd, if (self.topmost) HWND_TOPMOST else null, info.rcMonitor.left, info.rcMonitor.top, info.rcMonitor.right - info.rcMonitor.left, info.rcMonitor.bottom - info.rcMonitor.top, - SWP_FRAMECHANGED | SWP_NOACTIVATE, + c.SWP_FRAMECHANGED | c.SWP_NOACTIVATE, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } @@ -26605,25 +22631,25 @@ pub const Surface = struct { self.fullscreen = false; try self.applyWindowStyle(); - if (SetWindowPos( + if (sys.SetWindowPos( hwnd, if (self.topmost) HWND_TOPMOST else HWND_NOTOPMOST, self.restore_rect.left, self.restore_rect.top, self.restore_rect.right - self.restore_rect.left, self.restore_rect.bottom - self.restore_rect.top, - SWP_FRAMECHANGED | SWP_NOACTIVATE, + c.SWP_FRAMECHANGED | c.SWP_NOACTIVATE, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } - if (self.restore_maximized) _ = ShowWindow(hwnd, SW_MAXIMIZE); + if (self.restore_maximized) _ = sys.ShowWindow(hwnd, c.SW_MAXIMIZE); } fn captureRestoreState(self: *Surface) void { const hwnd = self.windowHwnd() orelse return; - self.restore_maximized = IsZoomed(hwnd) != 0; - if (GetWindowRect(hwnd, &self.restore_rect) == 0) { + self.restore_maximized = sys.IsZoomed(hwnd) != 0; + if (sys.GetWindowRect(hwnd, &self.restore_rect) == 0) { self.restore_rect = .{ .left = 0, .top = 0, .right = 1280, .bottom = 800 }; } } @@ -26693,12 +22719,12 @@ pub const Surface = struct { const hwnd = self.windowHwnd() orelse return; const hosted = self.host != null; var client_rect: RECT = undefined; - if (GetClientRect(hwnd, &client_rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetClientRect(hwnd, &client_rect) == 0) { + return lastError(); } var window_rect: RECT = undefined; - if (GetWindowRect(hwnd, &window_rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetWindowRect(hwnd, &window_rect) == 0) { + return lastError(); } const current_client_width = client_rect.right - client_rect.left; @@ -26708,16 +22734,16 @@ pub const Surface = struct { const frame_width = current_window_width - current_client_width; const frame_height = current_window_height - current_client_height; - if (SetWindowPos( + if (sys.SetWindowPos( hwnd, null, 0, 0, @as(i32, @intCast(client_width)) + frame_width, @as(i32, @intCast(client_height)) + frame_height, - SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE, + c.SWP_NOMOVE | c.SWP_NOZORDER | c.SWP_NOACTIVATE, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } if (hosted) { @@ -26784,14 +22810,14 @@ pub const Surface = struct { const host = self.host orelse return; if (host.activeSurface() != self) return; - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, null, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED, + c.SWP_NOMOVE | c.SWP_NOSIZE | c.SWP_NOZORDER | c.SWP_NOACTIVATE | c.SWP_FRAMECHANGED, ); } @@ -26805,26 +22831,26 @@ pub const Surface = struct { const hwnd = self.windowHwnd() orelse return; const source_hwnd = source.windowHwnd() orelse return; var rect: RECT = undefined; - if (GetWindowRect(source_hwnd, &rect) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.GetWindowRect(source_hwnd, &rect) == 0) { + return lastError(); } - if (SetWindowPos( + if (sys.SetWindowPos( hwnd, if (self.topmost) HWND_TOPMOST else HWND_NOTOPMOST, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, - SWP_NOACTIVATE | SWP_FRAMECHANGED, + c.SWP_NOACTIVATE | c.SWP_FRAMECHANGED, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } if (!self.fullscreen) { - if (IsZoomed(source_hwnd) != 0) { - _ = ShowWindow(hwnd, SW_MAXIMIZE); + if (sys.IsZoomed(source_hwnd) != 0) { + _ = sys.ShowWindow(hwnd, c.SW_MAXIMIZE); } else { - _ = ShowWindow(hwnd, SW_RESTORE); + _ = sys.ShowWindow(hwnd, c.SW_RESTORE); } } } @@ -26839,8 +22865,8 @@ pub const Surface = struct { const hwnd = self.windowHwnd() orelse return; var client_rect: RECT = undefined; var window_rect: RECT = undefined; - if (GetClientRect(hwnd, &client_rect) == 0) return; - if (GetWindowRect(hwnd, &window_rect) == 0) return; + if (sys.GetClientRect(hwnd, &client_rect) == 0) return; + if (sys.GetWindowRect(hwnd, &window_rect) == 0) return; const frame_width = (window_rect.right - window_rect.left) - (client_rect.right - client_rect.left); const frame_height = (window_rect.bottom - window_rect.top) - (client_rect.bottom - client_rect.top); @@ -26867,17 +22893,17 @@ pub const Surface = struct { self.host != null, ); - _ = SetWindowLongPtrW(hwnd, GWL_STYLE, @bitCast(@as(isize, @intCast(style)))); - if (SetWindowPos( + _ = sys.SetWindowLongPtrW(hwnd, c.GWL_STYLE, @bitCast(@as(isize, @intCast(style)))); + if (sys.SetWindowPos( hwnd, null, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED, + c.SWP_NOMOVE | c.SWP_NOSIZE | c.SWP_NOZORDER | c.SWP_NOACTIVATE | c.SWP_FRAMECHANGED, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } @@ -26887,48 +22913,48 @@ pub const Surface = struct { self.background_opacity_default, self.background_opacity_force_opaque, ); - const ex_style_raw = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + const ex_style_raw = sys.GetWindowLongPtrW(hwnd, c.GWL_EXSTYLE); var ex_style: u32 = @intCast(@as(usize, @bitCast(ex_style_raw))); if (opacity >= 0.999) { - if ((ex_style & WS_EX_LAYERED) != 0) { - ex_style &= ~@as(u32, WS_EX_LAYERED); - _ = SetWindowLongPtrW(hwnd, GWL_EXSTYLE, @bitCast(@as(isize, @intCast(ex_style)))); - _ = SetWindowPos( + if ((ex_style & c.WS_EX_LAYERED) != 0) { + ex_style &= ~@as(u32, c.WS_EX_LAYERED); + _ = sys.SetWindowLongPtrW(hwnd, c.GWL_EXSTYLE, @bitCast(@as(isize, @intCast(ex_style)))); + _ = sys.SetWindowPos( hwnd, null, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED, + c.SWP_NOMOVE | c.SWP_NOSIZE | c.SWP_NOZORDER | c.SWP_NOACTIVATE | c.SWP_FRAMECHANGED, ); } return; } - if ((ex_style & WS_EX_LAYERED) == 0) { - ex_style |= WS_EX_LAYERED; - _ = SetWindowLongPtrW(hwnd, GWL_EXSTYLE, @bitCast(@as(isize, @intCast(ex_style)))); + if ((ex_style & c.WS_EX_LAYERED) == 0) { + ex_style |= c.WS_EX_LAYERED; + _ = sys.SetWindowLongPtrW(hwnd, c.GWL_EXSTYLE, @bitCast(@as(isize, @intCast(ex_style)))); } - if (SetLayeredWindowAttributes(hwnd, 0, alphaByteForOpacity(opacity), LWA_ALPHA) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.SetLayeredWindowAttributes(hwnd, 0, alphaByteForOpacity(opacity), c.LWA_ALPHA) == 0) { + return lastError(); } } fn applyTopmost(self: *Surface) !void { const hwnd = self.activeSharedHostWindowHwnd() orelse return; - if (SetWindowPos( + if (sys.SetWindowPos( hwnd, if (self.topmost) HWND_TOPMOST else HWND_NOTOPMOST, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED, + c.SWP_NOMOVE | c.SWP_NOSIZE | c.SWP_NOACTIVATE | c.SWP_FRAMECHANGED, ) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } } @@ -26976,7 +23002,9 @@ pub const Surface = struct { if (self.host) |host| { const pane_count = if (host.activeTab()) |tab| tab.leafCount() else 0; if (hostChromeNeedsFocusRepaint(pane_count)) host.repaintContentChrome(); - if (focus_state_changed) host.refreshChrome() catch {}; + if (focus_state_changed) { + runUiActionOrLog("focus chrome refresh failed", host.refreshChrome()); + } } } @@ -27028,11 +23056,11 @@ pub const Surface = struct { const forms = imeWindowForms(ime_pos, self.content_scale); const surface_hwnd = self.hwnd orelse return; - const himc = ImmGetContext(surface_hwnd) orelse return; - defer _ = ImmReleaseContext(surface_hwnd, himc); + const himc = sys.ImmGetContext(surface_hwnd) orelse return; + defer _ = sys.ImmReleaseContext(surface_hwnd, himc); - const composition_set = ImmSetCompositionWindow(himc, &forms.composition) != 0; - const candidate_set = ImmSetCandidateWindow(himc, &forms.candidate) != 0; + const composition_set = sys.ImmSetCompositionWindow(himc, &forms.composition) != 0; + const candidate_set = sys.ImmSetCandidateWindow(himc, &forms.candidate) != 0; if (composition_set and candidate_set) { writeImeWindowFormsTrace(self.app.core_app.alloc, ime_pos, self.content_scale, forms); } @@ -27041,11 +23069,11 @@ pub const Surface = struct { fn handleImeResult(self: *Surface) void { if (!self.core_initialized) return; const surface_hwnd = self.hwnd orelse return; - const himc = ImmGetContext(surface_hwnd) orelse return; - defer _ = ImmReleaseContext(surface_hwnd, himc); + const himc = sys.ImmGetContext(surface_hwnd) orelse return; + defer _ = sys.ImmReleaseContext(surface_hwnd, himc); // Get the byte length of the result string - const byte_len = ImmGetCompositionStringW(himc, GCS_RESULTSTR, null, 0); + const byte_len = sys.ImmGetCompositionStringW(himc, c.GCS_RESULTSTR, null, 0); if (byte_len <= 0) return; const len: u32 = @intCast(byte_len); const wchar_count = len / 2; @@ -27060,7 +23088,7 @@ pub const Surface = struct { }; defer if (wchar_count > stack_buf.len) self.app.core_app.alloc.free(buf); - const read = ImmGetCompositionStringW(himc, GCS_RESULTSTR, buf.ptr, len); + const read = sys.ImmGetCompositionStringW(himc, c.GCS_RESULTSTR, buf.ptr, len); if (read <= 0) return; const actual_wchars: usize = @intCast(read); const result_slice = buf[0 .. actual_wchars / 2]; @@ -27089,11 +23117,11 @@ pub const Surface = struct { fn handleImeComposition(self: *Surface) void { if (!self.core_initialized) return; const surface_hwnd = self.hwnd orelse return; - const himc = ImmGetContext(surface_hwnd) orelse return; - defer _ = ImmReleaseContext(surface_hwnd, himc); + const himc = sys.ImmGetContext(surface_hwnd) orelse return; + defer _ = sys.ImmReleaseContext(surface_hwnd, himc); // Get the byte length of the composition string - const byte_len = ImmGetCompositionStringW(himc, GCS_COMPSTR, null, 0); + const byte_len = sys.ImmGetCompositionStringW(himc, c.GCS_COMPSTR, null, 0); if (byte_len <= 0) { self.core_surface.preeditCallback(null) catch {}; return; @@ -27110,7 +23138,7 @@ pub const Surface = struct { }; defer if (wchar_count > stack_buf.len) self.app.core_app.alloc.free(buf); - const read = ImmGetCompositionStringW(himc, GCS_COMPSTR, buf.ptr, len); + const read = sys.ImmGetCompositionStringW(himc, c.GCS_COMPSTR, buf.ptr, len); if (read <= 0) { self.core_surface.preeditCallback(null) catch {}; return; @@ -27132,9 +23160,9 @@ pub const Surface = struct { fn handleDropFiles(self: *Surface, wParam: WPARAM) void { if (!self.core_initialized) return; const hDrop: *anyopaque = @ptrFromInt(wParam); - defer DragFinish(hDrop); + defer sys.DragFinish(hDrop); - const file_count = DragQueryFileW(hDrop, 0xFFFFFFFF, null, 0); + const file_count = sys.DragQueryFileW(hDrop, 0xFFFFFFFF, null, 0); if (file_count == 0) return; const alloc = self.app.core_app.alloc; @@ -27144,13 +23172,13 @@ pub const Surface = struct { var i: UINT = 0; while (i < file_count) : (i += 1) { // Get required buffer length (not counting null terminator) - const wchar_len = DragQueryFileW(hDrop, i, null, 0); + const wchar_len = sys.DragQueryFileW(hDrop, i, null, 0); if (wchar_len == 0) continue; // Allocate UTF-16 buffer (+1 for null) const buf = alloc.alloc(u16, wchar_len + 1) catch continue; defer alloc.free(buf); - _ = DragQueryFileW(hDrop, i, buf.ptr, wchar_len + 1); + _ = sys.DragQueryFileW(hDrop, i, buf.ptr, wchar_len + 1); // Convert UTF-16 to UTF-8 var utf8_buf: [1024]u8 = undefined; @@ -27167,11 +23195,11 @@ pub const Surface = struct { if (needs_quoting) { result.append(alloc, '\'') catch continue; // Escape any existing single quotes within the path - for (path) |c| { - if (c == '\'') { + for (path) |char| { + if (char == '\'') { result.appendSlice(alloc, "'\\''") catch continue; } else { - result.append(alloc, c) catch continue; + result.append(alloc, char) catch continue; } } result.append(alloc, '\'') catch continue; @@ -27212,11 +23240,11 @@ pub const Surface = struct { self.cursor_pos = cursorPosFromLParam(lParam); if (state == .press) { if (self.hwnd) |hwnd| { - _ = SetFocus(hwnd); - _ = SetCapture(hwnd); + _ = sys.SetFocus(hwnd); + _ = sys.SetCapture(hwnd); } } else { - _ = ReleaseCapture(); + _ = sys.ReleaseCapture(); } self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| { @@ -27237,13 +23265,13 @@ pub const Surface = struct { const hi = highWord(@as(usize, @intCast(wParam))); const button: input.MouseButton = switch (hi) { - XBUTTON1 => .four, - XBUTTON2 => .five, + c.XBUTTON1 => .four, + c.XBUTTON2 => .five, else => return, }; const state: input.MouseButtonState = switch (msg) { - WM_XBUTTONDOWN => .press, - WM_XBUTTONUP => .release, + c.WM_XBUTTONDOWN => .press, + c.WM_XBUTTONUP => .release, else => return, }; const mods = mouseModsFromWParam(wParam); @@ -27251,11 +23279,11 @@ pub const Surface = struct { self.cursor_pos = cursorPosFromLParam(lParam); if (state == .press) { if (self.hwnd) |h| { - _ = SetFocus(h); - _ = SetCapture(h); + _ = sys.SetFocus(h); + _ = sys.SetCapture(h); } } else { - _ = ReleaseCapture(); + _ = sys.ReleaseCapture(); } self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| { @@ -27277,7 +23305,7 @@ pub const Surface = struct { fn handleMouseWheel(self: *Surface, scroll: NormalizedWheelScroll) void { if (!self.core_initialized) return; if (self.scrollbarScrollable()) { - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); self.noteScrollbarActivity(now_ms); self.syncScrollbarWindowNow(now_ms) catch |err| { log.warn("win32 scrollbar wheel refresh failed err={}", .{err}); @@ -27571,7 +23599,7 @@ pub const Surface = struct { if (!self.scrollbarScrollable()) { self.scrollbar_state = .{}; } else { - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); switch (self.scrollbarPolicy()) { .always => self.scrollbar_state.visibility = .visible, .dynamic => if (offset_changed) self.noteScrollbarActivity(now_ms), @@ -27601,12 +23629,12 @@ pub const Surface = struct { fn applyCursor(self: *Surface) bool { if (!self.mouse_visible) { - _ = SetCursor(null); + _ = sys.SetCursor(null); return true; } const cursor_name: INTRESOURCE = switch (self.mouse_shape) { - .default => IDC_ARROW, + .default => c.IDC_ARROW, .help => @as(INTRESOURCE, @ptrFromInt(32651)), .pointer, .grab, .grabbing => @as(INTRESOURCE, @ptrFromInt(32649)), .progress => @as(INTRESOURCE, @ptrFromInt(32650)), @@ -27624,10 +23652,10 @@ pub const Surface = struct { .copy, .zoom_in, .zoom_out, - => IDC_ARROW, + => c.IDC_ARROW, }; - _ = SetCursor(LoadCursorW(null, cursor_name)); + _ = sys.SetCursor(sys.LoadCursorW(null, cursor_name)); return true; } @@ -27707,10 +23735,10 @@ pub const Surface = struct { } alloc.free(owned); } - for (contents) |c| { - const mime_copy = try alloc.dupeZ(u8, c.mime); + for (contents) |content| { + const mime_copy = try alloc.dupeZ(u8, content.mime); errdefer alloc.free(mime_copy); - const data_copy = try alloc.dupeZ(u8, c.data); + const data_copy = try alloc.dupeZ(u8, content.data); // Both dupes succeeded — commit atomically into owned[] // and advance filled. The per-iter mime errdefer goes // out of scope without firing. @@ -27739,16 +23767,16 @@ pub const Surface = struct { } fn readClipboardText(self: *Surface) !?[:0]u8 { - if (OpenClipboard(self.hwnd) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.OpenClipboard(self.hwnd) == 0) { + return lastError(); } - defer _ = CloseClipboard(); + defer _ = sys.CloseClipboard(); - if (IsClipboardFormatAvailable(CF_UNICODETEXT) == 0) return null; + if (sys.IsClipboardFormatAvailable(c.CF_UNICODETEXT) == 0) return null; - const handle = GetClipboardData(CF_UNICODETEXT) orelse return null; - const locked = GlobalLock(handle) orelse return null; - defer _ = GlobalUnlock(handle); + const handle = sys.GetClipboardData(c.CF_UNICODETEXT) orelse return null; + const locked = sys.GlobalLock(handle) orelse return null; + defer _ = sys.GlobalUnlock(handle); const text_w: [*:0]const u16 = @ptrCast(@alignCast(locked)); const slice_w = std.mem.sliceTo(text_w, 0); @@ -27760,29 +23788,29 @@ pub const Surface = struct { const text_w = try std.unicode.utf8ToUtf16LeAllocZ(alloc, text); defer alloc.free(text_w); - if (OpenClipboard(self.hwnd) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.OpenClipboard(self.hwnd) == 0) { + return lastError(); } - defer _ = CloseClipboard(); + defer _ = sys.CloseClipboard(); - if (EmptyClipboard() == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.EmptyClipboard() == 0) { + return lastError(); } const bytes = (text_w.len + 1) * @sizeOf(u16); - const mem = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes) orelse - return windows.unexpectedError(windows.kernel32.GetLastError()); - errdefer _ = GlobalFree(mem); + const mem = sys.GlobalAlloc(c.GMEM_MOVEABLE | c.GMEM_ZEROINIT, bytes) orelse + return lastError(); + errdefer _ = sys.GlobalFree(mem); - const locked = GlobalLock(mem) orelse - return windows.unexpectedError(windows.kernel32.GetLastError()); - defer _ = GlobalUnlock(mem); + const locked = sys.GlobalLock(mem) orelse + return lastError(); + defer _ = sys.GlobalUnlock(mem); const dst: [*]u16 = @ptrCast(@alignCast(locked)); @memcpy(dst[0 .. text_w.len + 1], text_w[0 .. text_w.len + 1]); - if (SetClipboardData(CF_UNICODETEXT, mem) == null) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.SetClipboardData(c.CF_UNICODETEXT, mem) == null) { + return lastError(); } } @@ -27804,9 +23832,9 @@ pub const Surface = struct { // Lazily register CF_HTML once per App. The ID is process- // local and stable. if (self.app.cf_html_format == 0) { - const id = RegisterClipboardFormatW(CF_HTML_NAME); + const id = sys.RegisterClipboardFormatW(CF_HTML_NAME); if (id == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + return lastError(); } self.app.cf_html_format = id; } @@ -27831,13 +23859,13 @@ pub const Surface = struct { const plain_w = try std.unicode.utf8ToUtf16LeAllocZ(alloc, plain_src); defer alloc.free(plain_w); - if (OpenClipboard(self.hwnd) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.OpenClipboard(self.hwnd) == 0) { + return lastError(); } - defer _ = CloseClipboard(); + defer _ = sys.CloseClipboard(); - if (EmptyClipboard() == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.EmptyClipboard() == 0) { + return lastError(); } // CF_HTML payload: the wrapped byte stream + a trailing @@ -27846,37 +23874,37 @@ pub const Surface = struct { // free on that path. { const bytes = wrapped.len + 1; - const mem = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes) orelse - return windows.unexpectedError(windows.kernel32.GetLastError()); - errdefer _ = GlobalFree(mem); - const locked = GlobalLock(mem) orelse - return windows.unexpectedError(windows.kernel32.GetLastError()); + const mem = sys.GlobalAlloc(c.GMEM_MOVEABLE | c.GMEM_ZEROINIT, bytes) orelse + return lastError(); + errdefer _ = sys.GlobalFree(mem); + const locked = sys.GlobalLock(mem) orelse + return lastError(); { - defer _ = GlobalUnlock(mem); + defer _ = sys.GlobalUnlock(mem); const dst: [*]u8 = @ptrCast(locked); @memcpy(dst[0..wrapped.len], wrapped); dst[wrapped.len] = 0; } - if (SetClipboardData(cf_html, mem) == null) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.SetClipboardData(cf_html, mem) == null) { + return lastError(); } } // CF_UNICODETEXT fallback. { const bytes = (plain_w.len + 1) * @sizeOf(u16); - const mem = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes) orelse - return windows.unexpectedError(windows.kernel32.GetLastError()); - errdefer _ = GlobalFree(mem); - const locked = GlobalLock(mem) orelse - return windows.unexpectedError(windows.kernel32.GetLastError()); + const mem = sys.GlobalAlloc(c.GMEM_MOVEABLE | c.GMEM_ZEROINIT, bytes) orelse + return lastError(); + errdefer _ = sys.GlobalFree(mem); + const locked = sys.GlobalLock(mem) orelse + return lastError(); { - defer _ = GlobalUnlock(mem); + defer _ = sys.GlobalUnlock(mem); const dst: [*]u16 = @ptrCast(@alignCast(locked)); @memcpy(dst[0 .. plain_w.len + 1], plain_w[0 .. plain_w.len + 1]); } - if (SetClipboardData(CF_UNICODETEXT, mem) == null) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (sys.SetClipboardData(c.CF_UNICODETEXT, mem) == null) { + return lastError(); } } } @@ -27884,12 +23912,12 @@ pub const Surface = struct { fn destroyGL(self: *Surface) void { if (self.hglrc != null) { self.clearGLContextCurrent(); - _ = wglDeleteContext(self.hglrc); + _ = sys.wglDeleteContext(self.hglrc); self.hglrc = null; } if (self.hdc != null) { - if (self.hwnd) |hwnd| _ = ReleaseDC(hwnd, self.hdc); + if (self.hwnd) |hwnd| _ = sys.ReleaseDC(hwnd, self.hdc); self.hdc = null; } @@ -27897,6 +23925,238 @@ pub const Surface = struct { } }; +/// Byte-zeroed storage used only where no valid headless initializer or +/// declared default exists. The returned value is NOT a valid instance; +/// reading it before a real value is written is illegal behavior. Palette +/// counts guard their unused slots, and `core_initialized = false` guards the +/// inert `CoreSurface` field. +fn uninitializedInertStorage(comptime T: type) T { + var value: T = undefined; + @memset(std.mem.asBytes(&value), 0); + return value; +} + +/// Headless CoreApp/App/Host/Surface fixture for Win32 action and +/// structural-history tests. Caller-owned storage keeps the familiar local +/// names used by assertions while this fixture owns initialized resources and +/// their teardown. Guarded CoreSurface and unused palette slots are inert +/// storage, not valid live instances. +const TestSession = struct { + const max_hosts = 4; + const max_surfaces = 8; + + const HostOptions = struct { + storage: *Host, + id: u32 = 1, + register: bool = false, + active_tab: usize = 0, + next_tab_id: u32 = 1, + }; + + const SurfaceOptions = struct { + storage: *Surface, + host: ?*Host = null, + register: bool = false, + window_visible: bool = true, + host_active: bool = true, + window_focused: bool = false, + undo_allocator: ?Allocator = null, + free_launch_profile_key_on_deinit: bool = false, + }; + + const TabOptions = struct { + host: *Host, + surface: *Surface, + id: u32, + }; + + const Options = struct { + core_app: *CoreApp, + app: *App, + hosts: []const HostOptions = &.{}, + surfaces: []const SurfaceOptions = &.{}, + tabs: []const TabOptions = &.{}, + focused: bool = true, + shell_runtime_initialized: bool = false, + }; + + const SurfaceRecord = struct { + surface: *Surface, + undo_initialized: bool = true, + free_launch_profile_key: bool = false, + }; + + allocator: Allocator = std.testing.allocator, + core_app: ?*CoreApp = null, + app: ?*App = null, + core_app_initialized: bool = false, + app_initialized: bool = false, + hosts: [max_hosts]?*Host = .{null} ** max_hosts, + host_count: usize = 0, + surfaces: [max_surfaces]?SurfaceRecord = .{null} ** max_surfaces, + surface_count: usize = 0, + + fn init(self: *TestSession, options: Options) !void { + self.* = .{}; + if (options.hosts.len > max_hosts or options.surfaces.len > max_surfaces) { + return error.TestSessionCapacityExceeded; + } + + self.core_app = options.core_app; + try options.core_app.init(self.allocator); + self.core_app_initialized = true; + errdefer self.deinit(); + options.core_app.focused = options.focused; + + const app_config = try configpkg.Config.default(self.allocator); + options.app.* = .{ + .core_app = options.core_app, + .config = app_config, + .hinstance = sys.GetModuleHandleW(null), + // These initializers only establish inert model state; they do not + // create windows, initialize COM, or start compositor devices. + .settings_window = win32_settings.SettingsWindow.init(.{ + .ctx = @ptrCast(options.app), + .alloc = self.allocator, + .hinstance = sys.GetModuleHandleW(null), + .ownerWindow = &settingsOwnerWindowThunk, + .chromeBg = &settingsChromeBgThunk, + .textPrimary = &settingsTextPrimaryThunk, + .themeColors = &settingsThemeColorsThunk, + .highContrast = &settingsHighContrastThunk, + .openInEditor = &settingsOpenInEditorThunk, + .currentConfig = &settingsCurrentConfigThunk, + .saveAndReload = &settingsSaveAndReloadThunk, + .configRevision = &settingsConfigRevisionThunk, + .previewField = &settingsPreviewFieldThunk, + .notifyConflict = &settingsNotifyConflictThunk, + .notifySuccess = &settingsNotifySuccessThunk, + .customUiaProvidersEnabled = &settingsCustomUiaProvidersEnabledThunk, + .deferUiaDisconnect = &settingsDeferUiaDisconnectThunk, + .onClosed = &settingsOnClosedThunk, + }), + .link_hover_tracker = win32_link_preview.HoverTracker.init(), + .shell_compositor_driver = win32_compositor_native.NativeDriver.init(self.allocator), + .shell_compositor = win32_compositor.Backend.init(.{}, null), + .shell_runtime = win32_shell.runtime.Runtime.init(self.allocator), + .shell_runtime_initialized = options.shell_runtime_initialized, + }; + self.app = options.app; + self.app_initialized = true; + + for (options.hosts) |host_options| { + const host = host_options.storage; + host.* = .{ + .app = options.app, + .id = host_options.id, + .active_tab = host_options.active_tab, + .next_tab_id = host_options.next_tab_id, + // Item, tagged-payload, and ranked elements have no declared + // defaults. Zero counts/catalog guard these invalid slots from + // reads until production code overwrites them. Label bytes do + // have a valid zero value, so their array uses valid elements. + .palette_catalog_items = uninitializedInertStorage([palette_catalog_capacity]PaletteItem), + .palette_catalog_payloads = uninitializedInertStorage([palette_catalog_capacity]PalettePayload), + .palette_catalog_labels = @splat(@splat(0)), + .palette_list_ranked = uninitializedInertStorage([win32_palette.max_ranked]PaletteRanked), + }; + self.hosts[self.host_count] = host; + self.host_count += 1; + if (host_options.register) try options.app.hosts.append(self.allocator, host); + } + + for (options.surfaces) |surface_options| { + const surface = surface_options.storage; + const undo_allocator = surface_options.undo_allocator orelse self.allocator; + surface.* = .{ + .app = options.app, + .host = surface_options.host, + .host_id = if (surface_options.host) |host| host.id else 0, + .window_visible = surface_options.window_visible, + .host_active = surface_options.host_active, + .window_focused = surface_options.window_focused, + // No valid headless CoreSurface initializer exists. This + // byte-zeroed field is NOT a valid CoreSurface; + // core_initialized=false guards it, and reading it is illegal + // behavior while that guard remains false. + .core_surface = uninitializedInertStorage(CoreSurface), + .undo_stack = win32_undo.UndoStack.init(undo_allocator), + .search_bar = win32_search_bar.SearchBar.init(self.allocator), + .drop_target = win32_surface_drop_target.DropTarget.init( + self.allocator, + @ptrCast(surface), + &surfaceDropPayloadCallback, + ), + }; + self.surfaces[self.surface_count] = .{ + .surface = surface, + .free_launch_profile_key = surface_options.free_launch_profile_key_on_deinit, + }; + self.surface_count += 1; + if (surface_options.register) try options.app.windows.append(self.allocator, surface); + } + + for (options.tabs) |tab_options| { + try tab_options.host.tabs.append( + self.allocator, + try Tab.init(self.allocator, tab_options.id, tab_options.surface), + ); + } + } + + fn deinitSurfaceUndo(self: *TestSession, surface: *Surface) void { + for (self.surfaces[0..self.surface_count]) |*maybe_record| { + if (maybe_record.*) |*record| { + if (record.surface != surface or !record.undo_initialized) continue; + record.surface.undo_stack.deinit(); + record.undo_initialized = false; + return; + } + } + } + + fn deinit(self: *TestSession) void { + var surface_index = self.surface_count; + while (surface_index > 0) { + surface_index -= 1; + if (self.surfaces[surface_index]) |*record| { + if (record.free_launch_profile_key) { + if (record.surface.launch_profile_key) |value| self.allocator.free(value); + record.surface.launch_profile_key = null; + } + if (record.undo_initialized) record.surface.undo_stack.deinit(); + record.surface.search_bar.deinit(); + record.surface.drop_target.deinit(); + } + } + + var host_index = self.host_count; + while (host_index > 0) { + host_index -= 1; + const host = self.hosts[host_index] orelse continue; + host.setBanner(.none, null) catch {}; + host.clearStructuralHistory(.normal); + host.structural_undo_entries.deinit(self.allocator); + host.structural_redo_entries.deinit(self.allocator); + while (host.tabs.items.len > 0) { + var tab = host.tabs.orderedRemove(0); + tab.deinit(); + } + host.tabs.deinit(self.allocator); + } + + if (self.app_initialized) { + const app = self.app.?; + app.shell_runtime.deinit(); + app.config.deinit(); + app.hosts.deinit(self.allocator); + app.windows.deinit(self.allocator); + } + if (self.core_app_initialized) self.core_app.?.deinit(); + self.* = .{}; + } +}; + fn pushUndoAndRedoBranch( stack: *win32_undo.UndoStack, alloc: Allocator, @@ -27933,41 +24193,21 @@ test "win32 undo prune expires local and structural histories" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.windows = .empty; - app.hosts = .empty; - defer { - app.windows.deinit(std.testing.allocator); - app.hosts.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - - var host: Host = .{ + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - host.tabs.deinit(std.testing.allocator); - } - - try app.windows.append(std.testing.allocator, &surface_a); - try app.hosts.append(std.testing.allocator, &host); + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .register = true }, + .{ .storage = &surface_b }, + }, + }); + defer session.deinit(); try surface_a.undo_stack.push(.{ .kind = .clear_screen, @@ -28021,46 +24261,24 @@ test "win32 undo prune expires local and structural histories" { try std.testing.expectEqual(@as(usize, 1), host.structural_undo_entries.items.len); try std.testing.expectEqual(@as(u64, 220), host.structural_undo_entries.items[0].timestamp_ms); try std.testing.expectEqual(@as(usize, 1), host.structural_redo_entries.items.len); - try std.testing.expectEqual(@as(u64, 230), host.structural_redo_entries.items[0].timestamp_ms); - try std.testing.expectEqual(@as(?u64, 200), app.oldestUndoTimestamp()); -} - -test "win32 undo redo actions prune expired empty-host structural history before selection" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - - var app: App = .{ - .core_app = &core_app, - .config = try configpkg.Config.default(std.testing.allocator), - .hinstance = GetModuleHandleW(null), - .hosts = .empty, - .windows = .empty, - }; - defer { - app.config.deinit(); - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - app.config.@"undo-timeout" = .{ .duration = 0 }; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.setBanner(.none, null) catch {}; - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - host.tabs.deinit(std.testing.allocator); - } + try std.testing.expectEqual(@as(u64, 230), host.structural_redo_entries.items[0].timestamp_ms); + try std.testing.expectEqual(@as(?u64, 200), app.oldestUndoTimestamp()); +} - try app.hosts.append(std.testing.allocator, &host); +test "win32 undo redo actions prune expired empty-host structural history before selection" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + var core_app: CoreApp = undefined; + var app: App = undefined; + var host: Host = undefined; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + }); + defer session.deinit(); + app.config.@"undo-timeout" = .{ .duration = 0 }; try host.structural_undo_entries.append(std.testing.allocator, .{ .kind = .close_tab, .timestamp_ms = 0, @@ -28094,32 +24312,21 @@ test "win32 local undo capture invalidates host structural redo only" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.host = &host; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.host = &host; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + }, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 1); try host.structural_redo_entries.append(std.testing.allocator, .{ @@ -28142,32 +24349,21 @@ test "win32 snapshotless local action invalidates local redo and host structural if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.host = &host; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.host = &host; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + }, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 11); try host.structural_redo_entries.append(std.testing.allocator, .{ @@ -28190,32 +24386,21 @@ test "win32 unsupported structural action invalidates local redo and host struct if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.host = &host; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.host = &host; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + }, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 40); try host.structural_undo_entries.append(std.testing.allocator, .{ @@ -28247,49 +24432,29 @@ test "win32 unsupported tab structural action invalidates affected tab redo and if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var surface_c: Surface = undefined; - surface_c.app = &app; - surface_c.host = &host; - surface_c.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_c.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + .{ .storage = &surface_c, .host = &host }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 50); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 51); try pushUndoAndRedoBranch(&surface_c.undo_stack, std.testing.allocator, 52); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -28338,37 +24503,24 @@ test "win32 unsupported tab structural action invalidates affected tab redo and test "win32 local undo push failure preserves host structural redo" { if (builtin.os.tag != .windows) return error.SkipZigTest; + var arena: [64]u8 = .{0} ** 64; + var fixed: std.heap.FixedBufferAllocator = .init(&arena); var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - host.tabs.deinit(std.testing.allocator); - } - - var arena: [64]u8 = undefined; - var fixed: std.heap.FixedBufferAllocator = .init(&arena); - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.host = &host; - surface_a.undo_stack = win32_undo.UndoStack.init(fixed.allocator()); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.host = &host; - + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .undo_allocator = fixed.allocator() }, + .{ .storage = &surface_b, .host = &host }, + }, + }); + defer session.deinit(); try host.structural_redo_entries.append(std.testing.allocator, .{ .kind = .split_create, .timestamp_ms = 77, @@ -28400,38 +24552,29 @@ test "win32 structural redo invalidation clears affected tab redo only" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - }; - defer { - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var surface_c: Surface = undefined; - surface_c.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_c.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a }, + .{ .storage = &surface_b }, + .{ .storage = &surface_c }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 10); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 20); try pushUndoAndRedoBranch(&surface_c.undo_stack, std.testing.allocator, 30); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -28463,80 +24606,27 @@ test "win32 new_tab action inserts after the active tab by default, clears curre if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - try core_app.init(std.testing.allocator); - defer core_app.deinit(); - - var app: App = .{ - .core_app = &core_app, - .config = try configpkg.Config.default(std.testing.allocator), - .hinstance = GetModuleHandleW(null), - }; - defer { - app.config.deinit(); - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var app: App = undefined; + var host: Host = undefined; var source: Surface = undefined; - source.app = &app; - source.host = &host; - source.host_id = host.id; - source.hwnd = null; - source.core_initialized = false; - source.window_visible = true; - source.host_active = true; - source.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer source.undo_stack.deinit(); - var sibling: Surface = undefined; - sibling.app = &app; - sibling.host = &host; - sibling.host_id = host.id; - sibling.hwnd = null; - sibling.core_initialized = false; - sibling.window_visible = true; - sibling.host_active = true; - sibling.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer sibling.undo_stack.deinit(); - var other: Surface = undefined; - other.app = &app; - other.host = &host; - other.host_id = host.id; - other.hwnd = null; - other.core_initialized = false; - other.window_visible = true; - other.host_active = true; - other.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer other.undo_stack.deinit(); - var created: Surface = undefined; - created.app = &app; - created.host = &host; - created.host_id = host.id; - created.hwnd = null; - created.core_initialized = false; - created.window_visible = false; - created.host_active = false; - created.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer created.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &source)); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true, .next_tab_id = 3 }}, + .surfaces = &.{ + .{ .storage = &source, .host = &host, .register = true }, + .{ .storage = &sibling, .host = &host, .register = true }, + .{ .storage = &other, .host = &host, .register = true }, + .{ .storage = &created, .host = &host, .window_visible = false, .host_active = false }, + }, + .tabs = &.{.{ .host = &host, .surface = &source, .id = 1 }}, + }); + defer session.deinit(); + const inserted = try SplitTreeSurface.init(std.testing.allocator, &sibling); defer { var cleanup = inserted; @@ -28553,13 +24643,6 @@ test "win32 new_tab action inserts after the active tab by default, clears curre host.tabs.items[0].tree = next_tree; host.tabs.items[0].focused = host.tabs.items[0].findHandle(&source) orelse host.tabs.items[0].focused; try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &other)); - host.active_tab = 0; - host.next_tab_id = 3; - - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &source); - try app.windows.append(std.testing.allocator, &sibling); - try app.windows.append(std.testing.allocator, &other); try pushUndoAndRedoBranch(&source.undo_stack, std.testing.allocator, 120); try pushUndoAndRedoBranch(&sibling.undo_stack, std.testing.allocator, 121); @@ -28650,77 +24733,28 @@ test "win32 new_tab action appends when window-new-tab-position is end" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - try core_app.init(std.testing.allocator); - defer core_app.deinit(); - - var app: App = .{ - .core_app = &core_app, - .config = try configpkg.Config.default(std.testing.allocator), - .hinstance = GetModuleHandleW(null), - }; - defer { - app.config.deinit(); - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - app.config.@"window-new-tab-position" = .end; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var app: App = undefined; + var host: Host = undefined; var source: Surface = undefined; - source.app = &app; - source.host = &host; - source.host_id = host.id; - source.hwnd = null; - source.core_initialized = false; - source.window_visible = true; - source.host_active = true; - source.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer source.undo_stack.deinit(); - var other: Surface = undefined; - other.app = &app; - other.host = &host; - other.host_id = host.id; - other.hwnd = null; - other.core_initialized = false; - other.window_visible = true; - other.host_active = true; - other.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer other.undo_stack.deinit(); - var created: Surface = undefined; - created.app = &app; - created.host = &host; - created.host_id = host.id; - created.hwnd = null; - created.core_initialized = false; - created.window_visible = false; - created.host_active = false; - created.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer created.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &source)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &other)); - host.active_tab = 0; - host.next_tab_id = 3; - - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &source); - try app.windows.append(std.testing.allocator, &other); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true, .next_tab_id = 3 }}, + .surfaces = &.{ + .{ .storage = &source, .host = &host, .register = true }, + .{ .storage = &other, .host = &host, .register = true }, + .{ .storage = &created, .host = &host, .window_visible = false, .host_active = false }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &source, .id = 1 }, + .{ .host = &host, .surface = &other, .id = 2 }, + }, + }); + defer session.deinit(); + app.config.@"window-new-tab-position" = .end; const Hook = struct { var host_ref: *Host = undefined; @@ -28769,83 +24803,34 @@ test "win32 profile tab open appends when window-new-tab-position is end" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - try core_app.init(std.testing.allocator); - defer core_app.deinit(); - - var app: App = .{ - .core_app = &core_app, - .config = try configpkg.Config.default(std.testing.allocator), - .hinstance = GetModuleHandleW(null), - }; - defer { - app.config.deinit(); - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - app.config.@"window-new-tab-position" = .end; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var app: App = undefined; + var host: Host = undefined; var source: Surface = undefined; - source.app = &app; - source.host = &host; - source.host_id = host.id; - source.hwnd = null; - source.core_initialized = false; - source.window_visible = true; - source.host_active = true; - source.launch_profile_key = null; - source.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer source.undo_stack.deinit(); - var other: Surface = undefined; - other.app = &app; - other.host = &host; - other.host_id = host.id; - other.hwnd = null; - other.core_initialized = false; - other.window_visible = true; - other.host_active = true; - other.launch_profile_key = null; - other.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer other.undo_stack.deinit(); - var created: Surface = undefined; - created.app = &app; - created.host = &host; - created.host_id = host.id; - created.hwnd = null; - created.core_initialized = false; - created.window_visible = false; - created.host_active = false; - created.launch_profile_key = null; - created.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer { - if (created.launch_profile_key) |value| std.testing.allocator.free(value); - created.undo_stack.deinit(); - } - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &source)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &other)); - host.active_tab = 0; - host.next_tab_id = 3; - - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &source); - try app.windows.append(std.testing.allocator, &other); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true, .next_tab_id = 3 }}, + .surfaces = &.{ + .{ .storage = &source, .host = &host, .register = true }, + .{ .storage = &other, .host = &host, .register = true }, + .{ + .storage = &created, + .host = &host, + .window_visible = false, + .host_active = false, + .free_launch_profile_key_on_deinit = true, + }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &source, .id = 1 }, + .{ .host = &host, .surface = &other, .id = 2 }, + }, + }); + defer session.deinit(); + app.config.@"window-new-tab-position" = .end; const Hook = struct { var host_ref: *Host = undefined; @@ -28898,92 +24883,44 @@ test "win32 profile tab open appends when window-new-tab-position is end" { try std.testing.expectEqual(@as(u32, 2), host.tabs.items[1].id); try std.testing.expectEqual(@as(u32, 3), host.tabs.items[2].id); try std.testing.expect(host.tabs.items[2].focusedSurface() == &created); - try std.testing.expectEqualStrings("pwsh", created.launch_profile_key.?); -} - -test "win32 profile tab open inserts after active tab when window-new-tab-position is current" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var core_app: CoreApp = undefined; - try core_app.init(std.testing.allocator); - defer core_app.deinit(); - - var app: App = .{ - .core_app = &core_app, - .config = try configpkg.Config.default(std.testing.allocator), - .hinstance = GetModuleHandleW(null), - }; - defer { - app.config.deinit(); - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - // default is .current — explicitly set for clarity - app.config.@"window-new-tab-position" = .current; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } + try std.testing.expectEqualStrings("pwsh", created.launch_profile_key.?); +} - var source: Surface = undefined; - source.app = &app; - source.host = &host; - source.host_id = host.id; - source.hwnd = null; - source.core_initialized = false; - source.window_visible = true; - source.host_active = true; - source.launch_profile_key = null; - source.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer source.undo_stack.deinit(); +test "win32 profile tab open inserts after active tab when window-new-tab-position is current" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + var core_app: CoreApp = undefined; + var app: App = undefined; + var host: Host = undefined; + var source: Surface = undefined; var other: Surface = undefined; - other.app = &app; - other.host = &host; - other.host_id = host.id; - other.hwnd = null; - other.core_initialized = false; - other.window_visible = true; - other.host_active = true; - other.launch_profile_key = null; - other.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer other.undo_stack.deinit(); - var created: Surface = undefined; - created.app = &app; - created.host = &host; - created.host_id = host.id; - created.hwnd = null; - created.core_initialized = false; - created.window_visible = false; - created.host_active = false; - created.launch_profile_key = null; - created.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer { - if (created.launch_profile_key) |value| std.testing.allocator.free(value); - created.undo_stack.deinit(); - } + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true, .next_tab_id = 3 }}, + .surfaces = &.{ + .{ .storage = &source, .host = &host, .register = true }, + .{ .storage = &other, .host = &host, .register = true }, + .{ + .storage = &created, + .host = &host, + .window_visible = false, + .host_active = false, + .free_launch_profile_key_on_deinit = true, + }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &source, .id = 1 }, + .{ .host = &host, .surface = &other, .id = 2 }, + }, + }); + defer session.deinit(); + // default is .current — explicitly set for clarity + app.config.@"window-new-tab-position" = .current; // active_tab = 0; new tab should be inserted at index 1 (between tab1 and tab2) - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &source)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &other)); - host.active_tab = 0; - host.next_tab_id = 3; - - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &source); - try app.windows.append(std.testing.allocator, &other); const Hook = struct { var host_ref: *Host = undefined; @@ -29045,63 +24982,25 @@ test "win32 moveTab invalidates current tab redo and structural history before r if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var surface_c: Surface = undefined; - surface_c.app = &app; - surface_c.host = &host; - surface_c.hwnd = null; - surface_c.core_initialized = false; - surface_c.window_visible = true; - surface_c.host_active = true; - surface_c.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_c.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host }, + .{ .storage = &surface_c, .host = &host, .register = true }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -29118,9 +25017,6 @@ test "win32 moveTab invalidates current tab redo and structural history before r host.tabs.items[0].tree = next_tree; host.tabs.items[0].focused = host.tabs.items[0].findHandle(&surface_a) orelse host.tabs.items[0].focused; try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &surface_c)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); - try app.windows.append(std.testing.allocator, &surface_c); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 30); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 31); try pushUndoAndRedoBranch(&surface_c.undo_stack, std.testing.allocator, 32); @@ -29162,63 +25058,25 @@ test "win32 closeTab other clears current tab redo and structural history before if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var surface_c: Surface = undefined; - surface_c.app = &app; - surface_c.host = &host; - surface_c.hwnd = null; - surface_c.core_initialized = false; - surface_c.window_visible = true; - surface_c.host_active = true; - surface_c.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_c.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host }, + .{ .storage = &surface_c, .host = &host, .register = true }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -29235,9 +25093,6 @@ test "win32 closeTab other clears current tab redo and structural history before host.tabs.items[0].tree = next_tree; host.tabs.items[0].focused = host.tabs.items[0].findHandle(&surface_a) orelse host.tabs.items[0].focused; try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &surface_c)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); - try app.windows.append(std.testing.allocator, &surface_c); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 40); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 41); @@ -29277,74 +25132,30 @@ test "win32 closeTab right clears current tab redo and structural history before if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_left: Surface = undefined; - surface_left.app = &app; - surface_left.host = &host; - surface_left.hwnd = null; - surface_left.core_initialized = false; - surface_left.window_visible = true; - surface_left.host_active = true; - surface_left.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_left.undo_stack.deinit(); - var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var surface_right: Surface = undefined; - surface_right.app = &app; - surface_right.host = &host; - surface_right.hwnd = null; - surface_right.core_initialized = false; - surface_right.window_visible = true; - surface_right.host_active = true; - surface_right.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_right.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_left)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &surface_a)); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true, .active_tab = 1 }}, + .surfaces = &.{ + .{ .storage = &surface_left, .host = &host, .register = true }, + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host }, + .{ .storage = &surface_right, .host = &host, .register = true }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &surface_left, .id = 1 }, + .{ .host = &host, .surface = &surface_a, .id = 2 }, + }, + }); + defer session.deinit(); + const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -29361,12 +25172,6 @@ test "win32 closeTab right clears current tab redo and structural history before host.tabs.items[1].tree = next_tree; host.tabs.items[1].focused = host.tabs.items[1].findHandle(&surface_a) orelse host.tabs.items[1].focused; try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 3, &surface_right)); - host.active_tab = 1; - - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_left); - try app.windows.append(std.testing.allocator, &surface_a); - try app.windows.append(std.testing.allocator, &surface_right); try pushUndoAndRedoBranch(&surface_left.undo_stack, std.testing.allocator, 50); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 51); @@ -29409,53 +25214,23 @@ test "win32 closeTab other no-op preserves redo and structural history" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -29471,8 +25246,6 @@ test "win32 closeTab other no-op preserves redo and structural history" { host.tabs.items[0].tree.deinit(); host.tabs.items[0].tree = next_tree; host.tabs.items[0].focused = host.tabs.items[0].findHandle(&surface_a) orelse host.tabs.items[0].focused; - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 60); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 61); @@ -29508,64 +25281,28 @@ test "win32 closeTab right no-op preserves redo and structural history" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_left: Surface = undefined; - surface_left.app = &app; - surface_left.host = &host; - surface_left.hwnd = null; - surface_left.core_initialized = false; - surface_left.window_visible = true; - surface_left.host_active = true; - surface_left.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_left.undo_stack.deinit(); - var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true, .active_tab = 1 }}, + .surfaces = &.{ + .{ .storage = &surface_left, .host = &host, .register = true }, + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &surface_left, .id = 1 }, + .{ .host = &host, .surface = &surface_a, .id = 2 }, + }, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_left)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -29581,10 +25318,6 @@ test "win32 closeTab right no-op preserves redo and structural history" { host.tabs.items[1].tree.deinit(); host.tabs.items[1].tree = next_tree; host.tabs.items[1].focused = host.tabs.items[1].findHandle(&surface_a) orelse host.tabs.items[1].focused; - host.active_tab = 1; - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_left); - try app.windows.append(std.testing.allocator, &surface_a); try pushUndoAndRedoBranch(&surface_left.undo_stack, std.testing.allocator, 70); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 71); @@ -29623,47 +25356,18 @@ test "win32 closeTab this on last tab detaches tab for undo" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.setBanner(.none, null) catch {}; - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface: Surface = undefined; - surface.app = &app; - surface.host = &host; - surface.host_id = host.id; - surface.hwnd = null; - surface.core_initialized = false; - surface.window_visible = true; - surface.host_active = true; - surface.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{.{ .storage = &surface, .host = &host, .register = true }}, + .tabs = &.{.{ .host = &host, .surface = &surface, .id = 1 }}, + }); + defer session.deinit(); try std.testing.expect(try app.closeTab(.app, .this)); try std.testing.expectEqual(@as(usize, 0), host.tabs.items.len); @@ -29678,50 +25382,21 @@ test "win32 closeTab this on last tab detaches tab for undo" { } test "win32 prune preserves empty host while structural undo remains" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.setBanner(.none, null) catch {}; - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } + if (builtin.os.tag != .windows) return error.SkipZigTest; + var core_app: CoreApp = undefined; + var app: App = undefined; + var host: Host = undefined; var surface: Surface = undefined; - surface.app = &app; - surface.host = &host; - surface.host_id = host.id; - surface.hwnd = null; - surface.core_initialized = false; - surface.window_visible = true; - surface.host_active = true; - surface.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{.{ .storage = &surface, .host = &host, .register = true }}, + .tabs = &.{.{ .host = &host, .surface = &surface, .id = 1 }}, + }); + defer session.deinit(); const entry = host.detachTabForUndo(0) orelse unreachable; try host.structural_undo_entries.append(std.testing.allocator, entry); @@ -29739,52 +25414,18 @@ test "win32 app undo redo replays last tab close without active surface" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - - var app: App = .{ + var app: App = undefined; + var host: Host = undefined; + var surface: Surface = undefined; + var session: TestSession = .{}; + try session.init(.{ .core_app = &core_app, - .config = try configpkg.Config.default(std.testing.allocator), - .hinstance = GetModuleHandleW(null), - .hosts = .empty, - .windows = .empty, - }; - defer { - app.config.deinit(); - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.setBanner(.none, null) catch {}; - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - - var surface: Surface = undefined; - surface.app = &app; - surface.host = &host; - surface.host_id = host.id; - surface.hwnd = null; - surface.core_initialized = false; - surface.window_visible = true; - surface.window_focused = false; - surface.host_active = true; - surface.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface); + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{.{ .storage = &surface, .host = &host, .register = true }}, + .tabs = &.{.{ .host = &host, .surface = &surface, .id = 1 }}, + }); + defer session.deinit(); try std.testing.expect(try app.closeTab(.app, .this)); try std.testing.expectEqual(@as(usize, 0), host.tabs.items.len); @@ -29812,85 +25453,31 @@ test "win32 app undo redo uses focused host" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - - var app: App = .{ - .core_app = &core_app, - .config = try configpkg.Config.default(std.testing.allocator), - .hinstance = GetModuleHandleW(null), - .hosts = .empty, - .windows = .empty, - }; - defer { - app.config.deinit(); - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host_a: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host_a.setBanner(.none, null) catch {}; - host_a.clearStructuralHistory(.normal); - host_a.structural_undo_entries.deinit(std.testing.allocator); - host_a.structural_redo_entries.deinit(std.testing.allocator); - for (host_a.tabs.items) |*tab| tab.deinit(); - host_a.tabs.deinit(std.testing.allocator); - } - - var host_b: Host = .{ - .app = &app, - .id = 2, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host_b.setBanner(.none, null) catch {}; - host_b.clearStructuralHistory(.normal); - host_b.structural_undo_entries.deinit(std.testing.allocator); - host_b.structural_redo_entries.deinit(std.testing.allocator); - for (host_b.tabs.items) |*tab| tab.deinit(); - host_b.tabs.deinit(std.testing.allocator); - } - + var app: App = undefined; + var host_a: Host = undefined; + var host_b: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host_a; - surface_a.host_id = host_a.id; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.window_focused = false; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host_b; - surface_b.host_id = host_b.id; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.window_focused = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - - try host_a.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 11, &surface_a)); - try host_b.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 22, &surface_b)); - try app.hosts.append(std.testing.allocator, &host_a); - try app.hosts.append(std.testing.allocator, &host_b); - try app.windows.append(std.testing.allocator, &surface_a); - try app.windows.append(std.testing.allocator, &surface_b); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{ + .{ .storage = &host_a, .id = 1, .register = true }, + .{ .storage = &host_b, .id = 2, .register = true }, + }, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host_a, .register = true }, + .{ .storage = &surface_b, .host = &host_b, .register = true, .window_focused = true }, + }, + .tabs = &.{ + .{ .host = &host_a, .surface = &surface_a, .id = 11 }, + .{ .host = &host_b, .surface = &surface_b, .id = 22 }, + }, + }); + defer session.deinit(); - const now = GetTickCount64(); + const now = sys.GetTickCount64(); try host_a.structural_undo_entries.append(std.testing.allocator, .{ .kind = .close_tab, .timestamp_ms = now, @@ -29942,41 +25529,17 @@ test "win32 structural undo OOM rolls close_tab entry back to undo" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface: Surface = undefined; - surface.app = &app; - surface.host = &host; - surface.hwnd = null; - surface.core_initialized = false; - surface.window_visible = false; - surface.host_active = false; - surface.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{.{ .storage = &surface, .host = &host, .window_visible = false, .host_active = false }}, + }); + defer session.deinit(); try host.structural_redo_entries.ensureTotalCapacity(std.testing.allocator, 1); try host.structural_undo_entries.append(std.testing.allocator, .{ @@ -30006,55 +25569,22 @@ test "win32 structural redo OOM rolls split_create entry back to redo" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var source: Surface = undefined; - source.app = &app; - source.host = &host; - source.hwnd = null; - source.core_initialized = false; - source.window_visible = true; - source.host_active = true; - source.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer source.undo_stack.deinit(); - var created: Surface = undefined; - created.app = &app; - created.host = &host; - created.hwnd = null; - created.core_initialized = false; - created.window_visible = false; - created.host_active = false; - created.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer created.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &source)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &source); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &source, .host = &host, .register = true }, + .{ .storage = &created, .host = &host, .window_visible = false, .host_active = false }, + }, + .tabs = &.{.{ .host = &host, .surface = &source, .id = 1 }}, + }); + defer session.deinit(); try host.structural_undo_entries.ensureTotalCapacity(std.testing.allocator, 1); try host.structural_redo_entries.append(std.testing.allocator, .{ .kind = .split_create, @@ -30084,62 +25614,25 @@ test "win32 closeTab this restores tab when structural undo push fails" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .active_tab = 1, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.host_id = host.id; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.window_focused = false; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.host_id = host.id; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.window_focused = false; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &surface_b)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); - try app.windows.append(std.testing.allocator, &surface_b); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true, .active_tab = 1 }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host, .register = true }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &surface_a, .id = 1 }, + .{ .host = &host, .surface = &surface_b, .id = 2 }, + }, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 77); var backing: [0]u8 = .{}; @@ -30162,46 +25655,21 @@ test "win32 closeTab this on last tab restores tab when structural undo push fai if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface: Surface = undefined; - surface.app = &app; - surface.host = &host; - surface.host_id = host.id; - surface.hwnd = null; - surface.core_initialized = false; - surface.window_visible = true; - surface.host_active = true; - surface.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{.{ .storage = &surface, .host = &host, .register = true }}, + .tabs = &.{.{ .host = &host, .surface = &surface, .id = 1 }}, + }); + defer session.deinit(); + // The former partial App left undo-timeout uninitialized. This test is + // about reserve OOM preserving redo, so make pruning explicitly inert. + app.config.@"undo-timeout" = .{ .duration = std.math.maxInt(u64) }; try host.structural_redo_entries.append(std.testing.allocator, .{ .kind = .split_create, @@ -30232,53 +25700,23 @@ test "win32 resizeSplit fallback OOM preserves redo and structural history" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -30294,8 +25732,6 @@ test "win32 resizeSplit fallback OOM preserves redo and structural history" { host.tabs.items[0].tree.deinit(); host.tabs.items[0].tree = next_tree; host.tabs.items[0].focused = host.tabs.items[0].findHandle(&surface_a) orelse host.tabs.items[0].focused; - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 80); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 81); @@ -30318,7 +25754,7 @@ test "win32 resizeSplit fallback OOM preserves redo and structural history" { } }, }); - var tiny: [1]u8 = undefined; + var tiny: [1]u8 = .{0}; var fixed: std.heap.FixedBufferAllocator = .init(&tiny); const original_alloc = core_app.alloc; core_app.alloc = fixed.allocator(); @@ -30340,53 +25776,23 @@ test "win32 equalizeSplits fallback OOM preserves redo and structural history" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -30402,8 +25808,6 @@ test "win32 equalizeSplits fallback OOM preserves redo and structural history" { host.tabs.items[0].tree.deinit(); host.tabs.items[0].tree = next_tree; host.tabs.items[0].focused = host.tabs.items[0].findHandle(&surface_a) orelse host.tabs.items[0].focused; - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 90); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 91); @@ -30426,7 +25830,7 @@ test "win32 equalizeSplits fallback OOM preserves redo and structural history" { } }, }); - var tiny: [1]u8 = undefined; + var tiny: [1]u8 = .{0}; var fixed: std.heap.FixedBufferAllocator = .init(&tiny); const original_alloc = core_app.alloc; core_app.alloc = fixed.allocator(); @@ -30448,49 +25852,22 @@ test "win32 resizeSplit fallback no-op preserves redo and structural history" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var detached_surface: Surface = undefined; - detached_surface.app = &app; - detached_surface.host = &host; - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &detached_surface, .host = &host }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 100); try host.structural_undo_entries.append(std.testing.allocator, .{ @@ -30523,49 +25900,22 @@ test "win32 equalizeSplits fallback no-op preserves redo and structural history" if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var detached_surface: Surface = undefined; - detached_surface.app = &app; - detached_surface.host = &host; - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &detached_surface, .host = &host }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 110); try host.structural_undo_entries.append(std.testing.allocator, .{ @@ -30598,61 +25948,25 @@ test "win32 toggle_split_zoom invalidates structural redo" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var detached_surface: Surface = undefined; - detached_surface.app = &app; - detached_surface.host = &host; - detached_surface.hwnd = null; - detached_surface.core_initialized = false; - detached_surface.window_visible = false; - detached_surface.host_active = false; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host }, + .{ .storage = &detached_surface, .host = &host, .window_visible = false, .host_active = false }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -30668,8 +25982,6 @@ test "win32 toggle_split_zoom invalidates structural redo" { host.tabs.items[0].tree.deinit(); host.tabs.items[0].tree = next_tree; host.tabs.items[0].focused = host.tabs.items[0].findHandle(&surface_a) orelse host.tabs.items[0].focused; - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 10); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 11); @@ -30696,48 +26008,25 @@ test "win32 swapTabs invalidates structural history before drag reorder" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &surface_b)); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &surface_a, .id = 1 }, + .{ .host = &host, .surface = &surface_b, .id = 2 }, + }, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 30); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 40); @@ -30778,46 +26067,25 @@ test "win32 close_surface on single-surface tab invalidates structural history" if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &surface_b)); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &surface_a, .id = 1 }, + .{ .host = &host, .surface = &surface_b, .id = 2 }, + }, + }); + defer session.deinit(); try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 12); try host.structural_undo_entries.append(std.testing.allocator, .{ @@ -30851,55 +26119,25 @@ test "win32 close_surface on split pane clears structural history before shell c if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var detached_surface: Surface = undefined; - detached_surface.app = &app; - detached_surface.host = &host; - detached_surface.hwnd = null; - detached_surface.core_initialized = false; - detached_surface.window_visible = false; - detached_surface.host_active = false; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + .{ .storage = &detached_surface, .host = &host, .window_visible = false, .host_active = false }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -30950,65 +26188,25 @@ test "win32 windowDestroyed backup clears affected tab redo and structural redo if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.running = false; - app.shell_runtime_initialized = false; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - surface_a.pending_close_tree = null; - surface_a.pending_shell_close = null; - defer surface_a.undo_stack.deinit(); - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var detached_surface: Surface = undefined; - detached_surface.app = &app; - detached_surface.host = &host; - detached_surface.hwnd = null; - detached_surface.core_initialized = false; - detached_surface.window_visible = false; - detached_surface.host_active = false; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host, .register = true }, + .{ .storage = &detached_surface, .host = &host, .window_visible = false, .host_active = false }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -31026,10 +26224,6 @@ test "win32 windowDestroyed backup clears affected tab redo and structural redo try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 30); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 31); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); - try app.windows.append(std.testing.allocator, &surface_b); - try host.structural_undo_entries.append(std.testing.allocator, .{ .kind = .close_tab, .timestamp_ms = 10, @@ -31062,67 +26256,28 @@ test "win32 windowDestroyed backup clears affected tab redo and structural redo } test "win32 windowDestroyed backup skips closing surface redo after undo teardown" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - - var app: App = undefined; - app.core_app = &core_app; - app.running = false; - app.shell_runtime_initialized = false; - app.hosts = .empty; - app.windows = .empty; - defer { - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .structural_undo_entries = .empty, - .structural_redo_entries = .empty, - }; - defer { - host.clearStructuralHistory(.normal); - host.structural_undo_entries.deinit(std.testing.allocator); - host.structural_redo_entries.deinit(std.testing.allocator); - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - - var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; - surface_a.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - surface_a.pending_close_tree = null; - surface_a.pending_shell_close = null; + if (builtin.os.tag != .windows) return error.SkipZigTest; + var core_app: CoreApp = undefined; + var app: App = undefined; + var host: Host = undefined; + var surface_a: Surface = undefined; var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - surface_b.undo_stack = win32_undo.UndoStack.init(std.testing.allocator); - defer surface_b.undo_stack.deinit(); - var detached_surface: Surface = undefined; - detached_surface.app = &app; - detached_surface.host = &host; - detached_surface.hwnd = null; - detached_surface.core_initialized = false; - detached_surface.window_visible = false; - detached_surface.host_active = false; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host, .register = true }, + .{ .storage = &surface_b, .host = &host, .register = true }, + .{ .storage = &detached_surface, .host = &host, .window_visible = false, .host_active = false }, + }, + .tabs = &.{.{ .host = &host, .surface = &surface_a, .id = 1 }}, + }); + defer session.deinit(); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); const inserted = try SplitTreeSurface.init(std.testing.allocator, &surface_b); defer { var cleanup = inserted; @@ -31140,10 +26295,6 @@ test "win32 windowDestroyed backup skips closing surface redo after undo teardow try pushUndoAndRedoBranch(&surface_a.undo_stack, std.testing.allocator, 40); try pushUndoAndRedoBranch(&surface_b.undo_stack, std.testing.allocator, 41); - try app.hosts.append(std.testing.allocator, &host); - try app.windows.append(std.testing.allocator, &surface_a); - try app.windows.append(std.testing.allocator, &surface_b); - try host.structural_redo_entries.append(std.testing.allocator, .{ .kind = .split_create, .timestamp_ms = 11, @@ -31154,8 +26305,7 @@ test "win32 windowDestroyed backup skips closing surface redo after undo teardow } }, }); - surface_a.undo_stack.deinit(); - surface_a.undo_stack = undefined; + session.deinitSurfaceUndo(&surface_a); app.windowDestroyed(&surface_a); @@ -31214,39 +26364,29 @@ test "win32 terminal undo snapshot restores terminal state" { test "win32 close_tab structural undo restores and redoes a detached tab" { if (builtin.os.tag != .windows) return error.SkipZigTest; - var surface_a: Surface = undefined; - var surface_b: Surface = undefined; - surface_a.hwnd = null; - surface_b.hwnd = null; - surface_a.core_initialized = false; - surface_b.core_initialized = false; - surface_a.window_visible = true; - surface_b.window_visible = true; - surface_a.host_active = true; - surface_b.host_active = true; - var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ + var host: Host = undefined; + var surface_a: Surface = undefined; + var surface_b: Surface = undefined; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, .app = &app, - .id = 1, - .tabs = .empty, - .active_tab = 1, - }; - defer { - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface_a)); - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 2, &surface_b)); + .hosts = &.{.{ .storage = &host, .active_tab = 1 }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + }, + .tabs = &.{ + .{ .host = &host, .surface = &surface_a, .id = 1 }, + .{ .host = &host, .surface = &surface_b, .id = 2 }, + }, + }); + defer session.deinit(); var entry = host.detachTabForUndo(1).?; - defer entry.dispose(&host, .host_destroy) catch {}; + defer entry.dispose(&host, .host_destroy) catch {}; // Best-effort test teardown. try std.testing.expectEqual(@as(usize, 1), host.tabs.items.len); try std.testing.expectEqual(@as(usize, 0), host.active_tab); @@ -31269,37 +26409,22 @@ test "win32 close_tab structural undo restores and redoes a detached tab" { test "win32 close_tab structural undo detaches the last remaining tab" { if (builtin.os.tag != .windows) return error.SkipZigTest; - var surface: Surface = undefined; - surface.hwnd = null; - surface.core_initialized = false; - surface.window_visible = true; - surface.host_active = true; - var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ + var host: Host = undefined; + var surface: Surface = undefined; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, .app = &app, - .id = 1, - .tabs = .empty, - .active_tab = 0, - }; - defer { - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - - surface.app = &app; - surface.host = &host; - surface.host_id = host.id; - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface)); + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{.{ .storage = &surface, .host = &host }}, + .tabs = &.{.{ .host = &host, .surface = &surface, .id = 1 }}, + }); + defer session.deinit(); var entry = host.detachTabForUndo(0).?; - defer entry.dispose(&host, .host_destroy) catch {}; + defer entry.dispose(&host, .host_destroy) catch {}; // Best-effort test teardown. try std.testing.expectEqual(@as(usize, 0), host.tabs.items.len); try std.testing.expectEqual(@as(usize, 0), host.active_tab); @@ -31312,35 +26437,21 @@ test "win32 close_tab structural redo can detach the last restored tab" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - - var host: Host = .{ - .app = &app, - .id = 1, - .tabs = .empty, - .active_tab = 0, - }; - defer { - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - + var host: Host = undefined; var surface: Surface = undefined; - surface.app = &app; - surface.host = &host; - surface.host_id = host.id; - surface.hwnd = null; - surface.core_initialized = false; - surface.window_visible = true; - surface.host_active = true; - - try host.tabs.append(std.testing.allocator, try Tab.init(std.testing.allocator, 1, &surface)); + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, + .app = &app, + .hosts = &.{.{ .storage = &host }}, + .surfaces = &.{.{ .storage = &surface, .host = &host }}, + .tabs = &.{.{ .host = &host, .surface = &surface, .id = 1 }}, + }); + defer session.deinit(); var entry = host.detachTabForUndo(0).?; - defer entry.dispose(&host, .host_destroy) catch {}; + defer entry.dispose(&host, .host_destroy) catch {}; // Best-effort test teardown. const close_tab = &entry.payload.close_tab; try std.testing.expect(try host.restoreClosedTabEntry(close_tab)); @@ -31359,54 +26470,30 @@ test "win32 split_create structural undo removes and redoes the split" { if (builtin.os.tag != .windows) return error.SkipZigTest; var core_app: CoreApp = undefined; - core_app.alloc = std.testing.allocator; - var app: App = undefined; - app.core_app = &core_app; - app.hosts = .empty; - app.windows = .empty; - app.shell_runtime = win32_shell.runtime.Runtime.init(std.testing.allocator); - app.shell_runtime_initialized = true; - defer { - app.shell_runtime.deinit(); - app.hosts.deinit(std.testing.allocator); - app.windows.deinit(std.testing.allocator); - } - - var host: Host = .{ + var host: Host = undefined; + var surface_a: Surface = undefined; + var surface_b: Surface = undefined; + var session: TestSession = .{}; + try session.init(.{ + .core_app = &core_app, .app = &app, - .id = 1, - .tabs = .empty, - .active_tab = 0, - }; + .hosts = &.{.{ .storage = &host, .register = true }}, + .surfaces = &.{ + .{ .storage = &surface_a, .host = &host }, + .{ .storage = &surface_b, .host = &host }, + }, + .shell_runtime_initialized = true, + }); + defer session.deinit(); + var shell_window = try app.shell_runtime.prepare(.create_window); defer shell_window.deinit(); try shell_window.commit(&app.shell_runtime); host.shell_id = shell_window.created.window; - try app.hosts.append(std.testing.allocator, &host); - defer { - for (host.tabs.items) |*tab| tab.deinit(); - host.tabs.deinit(std.testing.allocator); - } - - var surface_a: Surface = undefined; - surface_a.app = &app; - surface_a.host = &host; - surface_a.hwnd = null; - surface_a.core_initialized = false; - surface_a.window_visible = true; - surface_a.host_active = true; surface_a.shell_id = shell_window.created.pane; surface_a.shell_committed = true; - var surface_b: Surface = undefined; - surface_b.app = &app; - surface_b.host = &host; - surface_b.hwnd = null; - surface_b.core_initialized = false; - surface_b.window_visible = true; - surface_b.host_active = true; - var initial_tab = try Tab.init(std.testing.allocator, 1, &surface_a); initial_tab.shell_id = shell_window.created.tab; try host.tabs.append(std.testing.allocator, initial_tab); @@ -31486,268 +26573,6 @@ test "win32 runtime can initialize config" { try std.testing.expect(@intFromPtr(app.hinstance) != 0); } -test "win32 keyFromVirtualKey maps core keys" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(input.Key.key_a, keyFromVirtualKey(VK_A, 0)); - try std.testing.expectEqual(input.Key.enter, keyFromVirtualKey(VK_RETURN, 0)); - try std.testing.expectEqual(input.Key.numpad_enter, keyFromVirtualKey(VK_RETURN, KF_EXTENDED)); - try std.testing.expectEqual(input.Key.arrow_left, keyFromVirtualKey(VK_LEFT, 0)); - try std.testing.expectEqual(input.Key.f12, keyFromVirtualKey(VK_F1 + 11, 0)); - try std.testing.expectEqual(input.Key.quote, keyFromVirtualKey(VK_OEM_7, 0)); -} - -test "win32 shouldDeferTextToCharMessage only defers plain text keys" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(UINT, 0x0004), TO_UNICODE_NO_STATE_CHANGE); - try std.testing.expectEqual(@as(usize, 1), utf16CodeUnitCount('a')); - try std.testing.expectEqual(@as(usize, 2), utf16CodeUnitCount(0x1F642)); - try std.testing.expect(shouldDeferTextToCharMessage( - .press, - .key_a, - .{}, - .{ .len = 1, .unshifted_codepoint = 'a', .deferred_utf16_units = 1 }, - )); - try std.testing.expect(shouldDeferTextToCharMessage( - .repeat, - .space, - .{}, - .{ .len = 1, .unshifted_codepoint = ' ', .deferred_utf16_units = 1 }, - )); - try std.testing.expect(shouldDeferTextToCharMessage( - .press, - .quote, - .{}, - .{ .unshifted_codepoint = '\'', .deferred_utf16_units = 1 }, - )); - try std.testing.expect(!shouldDeferTextToCharMessage( - .press, - .digit_2, - .{ .ctrl = true, .alt = true }, - .{ .unshifted_codepoint = '2', .deferred_utf16_units = 1 }, - )); - try std.testing.expect(shouldDeferTextToCharMessage( - .press, - .equal, - .{ .ctrl = true, .alt = true, .sides = .{ .alt = .right } }, - .{ .len = 1, .unshifted_codepoint = '=', .deferred_utf16_units = 1 }, - )); - try std.testing.expect(!shouldDeferTextToCharMessage( - .press, - .equal, - .{ .alt = true, .sides = .{ .alt = .right } }, - .{ .len = 1, .unshifted_codepoint = '=', .deferred_utf16_units = 1 }, - )); - try std.testing.expect(!shouldDeferTextToCharMessage( - .press, - .equal, - .{ - .ctrl = true, - .alt = true, - .sides = .{ .ctrl = .right, .alt = .right }, - }, - .{ .len = 1, .unshifted_codepoint = '=', .deferred_utf16_units = 1 }, - )); - try std.testing.expect(!shouldDeferTextToCharMessage( - .press, - .enter, - .{}, - .{ .unshifted_codepoint = 0x0D }, - )); -} - -test "win32 deferred char authorization respects key handling effect" { - try std.testing.expect(shouldAuthorizeDeferredCharMessage(.ignored)); - try std.testing.expect(!shouldAuthorizeDeferredCharMessage(.consumed)); - try std.testing.expect(!shouldAuthorizeDeferredCharMessage(.closed)); -} - -test "win32 VK_PACKET key down authorizes one unit without direct text or modifiers" { - const message = keyEventFromWin32Message(WM_KEYDOWN, VK_PACKET, 0).?; - try std.testing.expectEqual(input.Action.press, message.event.action); - try std.testing.expectEqual(input.Key.unidentified, message.event.key); - try std.testing.expectEqualStrings("", message.event.utf8); - try std.testing.expect(message.event.composing); - try std.testing.expectEqual(@as(u21, 0), message.event.unshifted_codepoint); - try std.testing.expect(std.meta.eql(input.Mods{}, message.event.mods)); - try std.testing.expect(std.meta.eql(input.Mods{}, message.event.consumed_mods)); - try std.testing.expectEqual(@as(usize, 1), message.deferred_utf16_units); -} - -test "win32 VK_PACKET key up authorizes no units or text" { - const message = keyEventFromWin32Message(WM_KEYUP, VK_PACKET, 0).?; - try std.testing.expectEqual(input.Action.release, message.event.action); - try std.testing.expectEqual(input.Key.unidentified, message.event.key); - try std.testing.expectEqualStrings("", message.event.utf8); - try std.testing.expect(!message.event.composing); - try std.testing.expectEqual(@as(u21, 0), message.event.unshifted_codepoint); - try std.testing.expect(std.meta.eql(input.Mods{}, message.event.mods)); - try std.testing.expect(std.meta.eql(input.Mods{}, message.event.consumed_mods)); - try std.testing.expectEqual(@as(usize, 0), message.deferred_utf16_units); -} - -test "win32 authorized char commit preserves packet control characters" { - var utf8_buf: [8]u8 = undefined; - const fixtures = [_]struct { codepoint: u21, expected: []const u8 }{ - .{ .codepoint = '\r', .expected = "\r" }, - .{ .codepoint = '\n', .expected = "\n" }, - .{ .codepoint = '\t', .expected = "\t" }, - .{ .codepoint = 0x08, .expected = "\x08" }, - .{ .codepoint = 0x1B, .expected = "\x1B" }, - }; - for (fixtures) |fixture| { - const event = charCommitEvent(fixture.codepoint, 0, &utf8_buf).?; - try std.testing.expectEqual(input.Key.unidentified, event.key); - try std.testing.expectEqual(fixture.codepoint, event.unshifted_codepoint); - try std.testing.expectEqualStrings(fixture.expected, event.utf8); - } -} - -test "win32 deferred char authorization preserves pending units across non-text events" { - var state: DeferredCharState = .{}; - state.authorize(1); - // Release and unrelated non-text key messages authorize zero units. - state.authorize(0); - try std.testing.expectEqual(@as(usize, 1), state.pending_units); - try std.testing.expectEqual(@as(?u21, 'a'), state.consumeCodeUnit('a', false)); - try std.testing.expectEqual(@as(usize, 0), state.pending_units); -} - -test "win32 deferred char dead key and composition consume exact units" { - var state: DeferredCharState = .{}; - state.authorize(1); - state.consumeDeadChar(); - try std.testing.expectEqual(@as(usize, 0), state.pending_units); - - state.authorize(1); - try std.testing.expectEqual(@as(?u21, 0x00E9), state.consumeCodeUnit(0x00E9, false)); - try std.testing.expectEqual(@as(usize, 0), state.pending_units); -} - -test "win32 deferred char authorization blocks unsolicited and IME text" { - var state: DeferredCharState = .{}; - try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit('a', false)); - - state.authorize(1); - try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit('a', true)); - try std.testing.expectEqual(@as(usize, 0), state.pending_units); - try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit('a', false)); -} - -test "win32 deferred char two surrogate keydowns consume two code units" { - var state: DeferredCharState = .{}; - state.authorize(1); - state.authorize(1); - try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit(0xD83D, false)); - try std.testing.expectEqual(@as(usize, 1), state.pending_units); - try std.testing.expectEqual(@as(?u21, 0x1F642), state.consumeCodeUnit(0xDE42, false)); - try std.testing.expectEqual(@as(usize, 0), state.pending_units); -} - -test "win32 deferred char supplementary expectation authorizes both units" { - var state: DeferredCharState = .{}; - state.authorize(2); - try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit(0xD83D, false)); - try std.testing.expectEqual(@as(usize, 1), state.pending_units); - try std.testing.expectEqual(@as(?u21, 0x1F642), state.consumeCodeUnit(0xDE42, false)); - try std.testing.expectEqual(@as(usize, 0), state.pending_units); -} - -test "win32 deferred char commits 256 delayed BMP authorizations" { - var state: DeferredCharState = .{}; - for (0..256) |_| state.authorize(1); - try std.testing.expectEqual(@as(usize, 256), state.pending_units); - - for (0..256) |_| { - try std.testing.expectEqual(@as(?u21, 'a'), state.consumeCodeUnit('a', false)); - } - try std.testing.expectEqual(@as(usize, 0), state.pending_units); -} - -test "win32 deferred char malformed surrogate clears authorization state" { - var state: DeferredCharState = .{}; - state.authorize(3); - try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit(0xD83D, false)); - try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit('a', false)); - try std.testing.expectEqual(@as(usize, 0), state.pending_units); - try std.testing.expectEqual(@as(?u16, null), state.high_surrogate); -} - -test "win32 deferred char authorization saturates only at usize maximum" { - var state: DeferredCharState = .{ - .pending_units = std.math.maxInt(usize) - 1, - }; - state.authorize(2); - try std.testing.expectEqual(std.math.maxInt(usize), state.pending_units); -} - -test "win32 hotkeySpecForTrigger maps physical key triggers" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const spec = hotkeySpecForTrigger(.{ - .key = .{ .physical = .key_a }, - .mods = .{ .ctrl = true, .shift = true }, - }).?; - - try std.testing.expectEqual(@as(UINT, MOD_CONTROL | MOD_SHIFT), spec.modifiers); - try std.testing.expectEqual(@as(UINT, VK_A), spec.vk); -} - -test "win32 hotkeySpecForTrigger maps unicode triggers with implicit shift" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const spec = hotkeySpecForTrigger(.{ - .key = .{ .unicode = '+' }, - .mods = .{ .alt = true }, - }).?; - - try std.testing.expectEqual(@as(UINT, MOD_ALT | MOD_SHIFT), spec.modifiers); - try std.testing.expectEqual(@as(UINT, VK_OEM_PLUS), spec.vk); -} - -test "win32 hotkeySpecForTrigger rejects unsupported catch-all triggers" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(hotkeySpecForTrigger(.{ - .key = .catch_all, - .mods = .{ .ctrl = true }, - }) == null); -} - -test "win32 hotkeySpecEql detects duplicate resolved triggers" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const physical = hotkeySpecForTrigger(.{ - .key = .{ .physical = .backquote }, - .mods = .{ .ctrl = true }, - }).?; - const unicode = hotkeySpecForTrigger(.{ - .key = .{ .unicode = '`' }, - .mods = .{ .ctrl = true }, - }).?; - const shifted = hotkeySpecForTrigger(.{ - .key = .{ .unicode = '~' }, - .mods = .{ .ctrl = true }, - }).?; - - try std.testing.expect(hotkeySpecEql(physical, unicode)); - try std.testing.expect(!hotkeySpecEql(physical, shifted)); -} - -test "win32 hotkeyRegistrationFailureReason names conflicts" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqualStrings( - "already registered by another app or another noctty instance", - hotkeyRegistrationFailureReason(.HOTKEY_ALREADY_REGISTERED), - ); - try std.testing.expectEqualStrings( - "access denied; hotkey may be reserved, occupied by an elevated app, or blocked by policy", - hotkeyRegistrationFailureReason(.ACCESS_DENIED), - ); -} - test "win32 quitTimerDelayMs clamps to at least one second" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -31784,16 +26609,16 @@ test "win32 systemBackdropTypeForBuild gates unsupported builds" { config.@"background-opacity" = 0.85; config.@"background-blur" = .true; - try std.testing.expect(!supportsDwmSystemBackdropAttribute(OS_BUILD_WIN10_22H2)); - try std.testing.expect(!supportsDwmSystemBackdropAttribute(OS_BUILD_WIN11_21H2)); - try std.testing.expect(supportsDwmSystemBackdropAttribute(OS_BUILD_WIN11_22H2)); + try std.testing.expect(!supportsDwmSystemBackdropAttribute(c.OS_BUILD_WIN10_22H2)); + try std.testing.expect(!supportsDwmSystemBackdropAttribute(c.OS_BUILD_WIN11_21H2)); + try std.testing.expect(supportsDwmSystemBackdropAttribute(c.OS_BUILD_WIN11_22H2)); - try std.testing.expectEqual(DWMSBT_NONE, systemBackdropTypeForBuild(&config, OS_BUILD_WIN10_22H2)); - try std.testing.expectEqual(DWMSBT_NONE, systemBackdropTypeForBuild(&config, OS_BUILD_WIN11_21H2)); - try std.testing.expectEqual(DWMSBT_TABBEDWINDOW, systemBackdropTypeForBuild(&config, OS_BUILD_WIN11_22H2)); + try std.testing.expectEqual(c.DWMSBT_NONE, systemBackdropTypeForBuild(&config, c.OS_BUILD_WIN10_22H2)); + try std.testing.expectEqual(c.DWMSBT_NONE, systemBackdropTypeForBuild(&config, c.OS_BUILD_WIN11_21H2)); + try std.testing.expectEqual(c.DWMSBT_TABBEDWINDOW, systemBackdropTypeForBuild(&config, c.OS_BUILD_WIN11_22H2)); config.@"background-blur" = .false; - try std.testing.expectEqual(DWMSBT_NONE, systemBackdropTypeForBuild(&config, OS_BUILD_WIN11_22H2)); + try std.testing.expectEqual(c.DWMSBT_NONE, systemBackdropTypeForBuild(&config, c.OS_BUILD_WIN11_22H2)); } test "win32 configuredHostWindowPosition requires both coordinates" { @@ -31822,151 +26647,53 @@ test "win32 titlebar colors honor ghostty overrides" { config.@"window-theme" = .ghostty; config.@"window-titlebar-background" = .{ .r = 1, .g = 2, .b = 3 }; - config.@"window-titlebar-foreground" = .{ .r = 4, .g = 5, .b = 6 }; - - try std.testing.expectEqual(rgb(1, 2, 3), titlebarCaptionColor(&theme, &config)); - try std.testing.expectEqual(rgb(4, 5, 6), titlebarTextColor(&theme, &config)); -} - -test "win32 cursorPosFromLParam decodes signed coordinates" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const y: u16 = @bitCast(@as(i16, -5)); - const x: u16 = @bitCast(@as(i16, 12)); - const encoded = (@as(usize, y) << 16) | @as(usize, x); - const pos = cursorPosFromLParam(@bitCast(@as(isize, @intCast(encoded)))); - try std.testing.expectEqual(@as(f32, 12), pos.x); - try std.testing.expectEqual(@as(f32, -5), pos.y); -} - -test "win32 IME windows use scaled terminal caret geometry" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const forms = imeWindowForms( - .{ .x = 12, .y = 34, .width = 0, .height = 16 }, - .{ .x = 1.5, .y = 2 }, - ); - - try std.testing.expectEqual(CFS_POINT, forms.composition.dwStyle); - try std.testing.expectEqual(@as(i32, 18), forms.composition.ptCurrentPos.x); - try std.testing.expectEqual(@as(i32, 68), forms.composition.ptCurrentPos.y); - - try std.testing.expectEqual(@as(u32, 0), forms.candidate.dwIndex); - try std.testing.expectEqual(CFS_EXCLUDE, forms.candidate.dwStyle); - try std.testing.expectEqual(forms.composition.ptCurrentPos.x, forms.candidate.ptCurrentPos.x); - try std.testing.expectEqual(forms.composition.ptCurrentPos.y, forms.candidate.ptCurrentPos.y); - try std.testing.expectEqual(@as(i32, 18), forms.candidate.rcArea.left); - try std.testing.expectEqual(@as(i32, 36), forms.candidate.rcArea.top); - try std.testing.expectEqual(@as(i32, 19), forms.candidate.rcArea.right); - try std.testing.expectEqual(@as(i32, 68), forms.candidate.rcArea.bottom); -} - -test "win32 mouseButtonFromMessage maps standard buttons" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(input.MouseButton.left, mouseButtonFromMessage(WM_LBUTTONDOWN).?); - try std.testing.expectEqual(input.MouseButton.right, mouseButtonFromMessage(WM_RBUTTONDOWN).?); - try std.testing.expectEqual(input.MouseButton.middle, mouseButtonFromMessage(WM_MBUTTONDOWN).?); - try std.testing.expect(mouseButtonFromMessage(WM_XBUTTONDOWN) == null); - try std.testing.expect(mouseButtonFromMessage(WM_XBUTTONUP) == null); -} - -test "win32 XButton wParam decoding maps forward and back buttons" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - // XBUTTON1 in HIWORD of wParam - const wp1 = (@as(usize, XBUTTON1) << 16) | @as(usize, MK_XBUTTON1); - try std.testing.expectEqual(XBUTTON1, highWord(wp1)); - - // XBUTTON2 in HIWORD of wParam - const wp2 = (@as(usize, XBUTTON2) << 16) | @as(usize, MK_XBUTTON2); - try std.testing.expectEqual(XBUTTON2, highWord(wp2)); -} - -test "win32 normalizeWheelDelta maps discrete wheel steps to pixel deltas" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const event = normalizeWheelDelta(.{ - .settings = .{ .lines = 3, .chars = 5 }, - .cell_size = .{ .width = 8, .height = 16 }, - .viewport = .{ .width = 800, .height = 600 }, - }, .vertical, 120); - - try std.testing.expectApproxEqAbs(48.0, event.yoff, 0.0001); - try std.testing.expectEqual(@as(f64, 0), event.xoff); - try std.testing.expect(!event.mods.precision); - try std.testing.expect(event.mods.pixel_delta); -} - -test "win32 normalizeWheelDelta maps horizontal wheel steps to pixel deltas" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const event = normalizeWheelDelta(.{ - .settings = .{ .lines = 3, .chars = 5 }, - .cell_size = .{ .width = 8, .height = 16 }, - .viewport = .{ .width = 800, .height = 600 }, - }, .horizontal, 120); - - try std.testing.expectApproxEqAbs(40.0, event.xoff, 0.0001); - try std.testing.expectEqual(@as(f64, 0), event.yoff); - try std.testing.expect(!event.mods.precision); - try std.testing.expect(event.mods.pixel_delta); -} - -test "win32 normalizeWheelDelta scales high-resolution input proportionally" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const event = normalizeWheelDelta(.{ - .settings = .{ .lines = 3, .chars = 3 }, - .cell_size = .{ .width = 8, .height = 16 }, - .viewport = .{ .width = 800, .height = 600 }, - }, .vertical, 40); + config.@"window-titlebar-foreground" = .{ .r = 4, .g = 5, .b = 6 }; - try std.testing.expectApproxEqAbs(16.0 / 3.0, event.yoff, 0.0001); - try std.testing.expect(event.mods.precision); - try std.testing.expect(event.mods.pixel_delta); + try std.testing.expectEqual(rgb(1, 2, 3), titlebarCaptionColor(&theme, &config)); + try std.testing.expectEqual(rgb(4, 5, 6), titlebarTextColor(&theme, &config)); } -test "win32 normalizeWheelDelta honors page scroll settings" { +test "win32 cursorPosFromLParam decodes signed coordinates" { if (builtin.os.tag != .windows) return error.SkipZigTest; - const event = normalizeWheelDelta(.{ - .settings = .{ .lines = WHEEL_PAGESCROLL, .chars = 3 }, - .cell_size = .{ .width = 8, .height = 16 }, - .viewport = .{ .width = 800, .height = 600 }, - }, .vertical, 120); - - try std.testing.expectApproxEqAbs(584.0, event.yoff, 0.0001); - try std.testing.expect(!event.mods.precision); - try std.testing.expect(event.mods.pixel_delta); + const y: u16 = @bitCast(@as(i16, -5)); + const x: u16 = @bitCast(@as(i16, 12)); + const encoded = (@as(usize, y) << 16) | @as(usize, x); + const pos = cursorPosFromLParam(@bitCast(@as(isize, @intCast(encoded)))); + try std.testing.expectEqual(@as(f32, 12), pos.x); + try std.testing.expectEqual(@as(f32, -5), pos.y); } -test "win32 normalizeWheelDelta ignores page scroll settings for high-resolution input" { +test "win32 IME windows use scaled terminal caret geometry" { if (builtin.os.tag != .windows) return error.SkipZigTest; - const event = normalizeWheelDelta(.{ - .settings = .{ .lines = WHEEL_PAGESCROLL, .chars = 3 }, - .cell_size = .{ .width = 8, .height = 16 }, - .viewport = .{ .width = 800, .height = 600 }, - }, .vertical, 40); + const forms = imeWindowForms( + .{ .x = 12, .y = 34, .width = 0, .height = 16 }, + .{ .x = 1.5, .y = 2 }, + ); + + try std.testing.expectEqual(c.CFS_POINT, forms.composition.dwStyle); + try std.testing.expectEqual(@as(i32, 18), forms.composition.ptCurrentPos.x); + try std.testing.expectEqual(@as(i32, 68), forms.composition.ptCurrentPos.y); - try std.testing.expectApproxEqAbs(16.0 / 3.0, event.yoff, 0.0001); - try std.testing.expect(event.mods.precision); - try std.testing.expect(event.mods.pixel_delta); + try std.testing.expectEqual(@as(u32, 0), forms.candidate.dwIndex); + try std.testing.expectEqual(c.CFS_EXCLUDE, forms.candidate.dwStyle); + try std.testing.expectEqual(forms.composition.ptCurrentPos.x, forms.candidate.ptCurrentPos.x); + try std.testing.expectEqual(forms.composition.ptCurrentPos.y, forms.candidate.ptCurrentPos.y); + try std.testing.expectEqual(@as(i32, 18), forms.candidate.rcArea.left); + try std.testing.expectEqual(@as(i32, 36), forms.candidate.rcArea.top); + try std.testing.expectEqual(@as(i32, 19), forms.candidate.rcArea.right); + try std.testing.expectEqual(@as(i32, 68), forms.candidate.rcArea.bottom); } -test "win32 normalizeWheelDelta ignores disabled notch settings for high-resolution input" { +test "win32 mouseButtonFromMessage maps standard buttons" { if (builtin.os.tag != .windows) return error.SkipZigTest; - const event = normalizeWheelDelta(.{ - .settings = .{ .lines = 0, .chars = 0 }, - .cell_size = .{ .width = 8, .height = 16 }, - .viewport = .{ .width = 800, .height = 600 }, - }, .vertical, 40); - - try std.testing.expectApproxEqAbs(16.0 / 3.0, event.yoff, 0.0001); - try std.testing.expect(event.mods.precision); - try std.testing.expect(event.mods.pixel_delta); + try std.testing.expectEqual(input.MouseButton.left, mouseButtonFromMessage(c.WM_LBUTTONDOWN).?); + try std.testing.expectEqual(input.MouseButton.right, mouseButtonFromMessage(c.WM_RBUTTONDOWN).?); + try std.testing.expectEqual(input.MouseButton.middle, mouseButtonFromMessage(c.WM_MBUTTONDOWN).?); + try std.testing.expect(mouseButtonFromMessage(c.WM_XBUTTONDOWN) == null); + try std.testing.expect(mouseButtonFromMessage(c.WM_XBUTTONUP) == null); } test "win32 renderer repaint request coalesces until paint completes" { @@ -32003,71 +26730,12 @@ test "win32 host composition redraw avoids terminal child HWNDs" { if (builtin.os.tag != .windows) return error.SkipZigTest; const flags = hostCompositionRedrawFlags(); - try std.testing.expect((flags & RDW_INVALIDATE) != 0); - try std.testing.expect((flags & RDW_ERASE) != 0); - try std.testing.expect((flags & RDW_FRAME) != 0); - try std.testing.expect((flags & RDW_UPDATENOW) != 0); - try std.testing.expect((flags & RDW_NOCHILDREN) != 0); - try std.testing.expect((flags & RDW_ALLCHILDREN) == 0); -} - -test "win32-opengl-startup-failure-message-explains-error-126" { - var buf: [4096]u8 = undefined; - const message = try formatOpenGLStartupFailureMessage(&buf, error.Unexpected, .{ - .step = .create_context, - .win32_error = ERROR_MOD_NOT_FOUND, - .zig_error_name = "Unexpected", - }); - - try std.testing.expect(std.mem.indexOf(u8, message, "Win32 error: 126 (ERROR_MOD_NOT_FOUND)") != null); - try std.testing.expect(std.mem.indexOf(u8, message, "AMD+NVIDIA hybrid GPU") != null); - try std.testing.expect(std.mem.indexOf(u8, message, "DirectX or ANGLE fallback") != null); - try std.testing.expect(std.mem.indexOf(u8, message, "https://github.com/amanthanvi/noctty/issues/64") != null); -} - -test "win32-opengl-startup-failure-message-explains-version-floor" { - var buf: [4096]u8 = undefined; - const message = try formatOpenGLStartupFailureMessage(&buf, error.OpenGLOutdated, .{ - .step = .version_check, - .zig_error_name = "OpenGLOutdated", - }); - - try std.testing.expect(std.mem.indexOf(u8, message, "OpenGL 4.3 through WGL") != null); - try std.testing.expect(std.mem.indexOf(u8, message, "required OpenGL 4.3 feature level") != null); - try std.testing.expect(std.mem.indexOf(u8, message, "Win32 error: not reported") != null); -} - -test "win32-opengl-startup-failure-recording-is-startup-scoped" { - clearOpenGLStartupFailure(); - recordOpenGLStartupError(.make_current, error.Unexpected); - try std.testing.expect(currentOpenGLStartupFailure() == null); - - beginOpenGLStartupDiagnostics(); - try std.testing.expect(openGLStartupDiagnosticsActive()); - - clearOpenGLStartupFailure(); - try std.testing.expect(!openGLStartupDiagnosticsActive()); - try std.testing.expect(currentOpenGLStartupFailure() == null); -} - -test "win32-opengl-startup-failure-preserves-win32-loader-cause" { - clearOpenGLStartupFailure(); - beginOpenGLStartupDiagnostics(); - - try std.testing.expect(recordOpenGLStartupFailure(.{ - .step = .load_opengl32, - .win32_error = ERROR_MOD_NOT_FOUND, - })); - try std.testing.expect(!recordOpenGLStartupFailure(.{ - .step = .load_functions, - .zig_error_name = "OpenGLFunctionLoadFailed", - })); - - const failure = currentOpenGLStartupFailure().?; - try std.testing.expectEqual(OpenGLStartupStep.load_opengl32, failure.step); - try std.testing.expectEqual(@as(?DWORD, ERROR_MOD_NOT_FOUND), failure.win32_error); - - clearOpenGLStartupFailure(); + try std.testing.expect((flags & c.RDW_INVALIDATE) != 0); + try std.testing.expect((flags & c.RDW_ERASE) != 0); + try std.testing.expect((flags & c.RDW_FRAME) != 0); + try std.testing.expect((flags & c.RDW_UPDATENOW) != 0); + try std.testing.expect((flags & c.RDW_NOCHILDREN) != 0); + try std.testing.expect((flags & c.RDW_ALLCHILDREN) == 0); } test "win32 requestRepaint preserves renderer request during live resize" { @@ -32145,12 +26813,12 @@ test "win32 flushDeferredVisibleSurfaceRepaints schedules deferred visible surfa test "win32 getSize prefers live hwnd client rect over cached size" { if (builtin.os.tag != .windows) return error.SkipZigTest; - const hinstance = GetModuleHandleW(null); - const hwnd = CreateWindowExW( + const hinstance = sys.GetModuleHandleW(null); + const hwnd = sys.CreateWindowExW( 0, prompt_label_class, std.unicode.utf8ToUtf16LeStringLiteral(""), - WS_POPUP | WS_VISIBLE, + c.WS_POPUP | c.WS_VISIBLE, 0, 0, 320, @@ -32159,8 +26827,8 @@ test "win32 getSize prefers live hwnd client rect over cached size" { null, hinstance, null, - ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); - defer _ = DestroyWindow(hwnd); + ) orelse return lastError(); + defer _ = sys.DestroyWindow(hwnd); var app: App = undefined; var surface: Surface = undefined; @@ -32236,19 +26904,19 @@ test "win32 startupHostWindowStyle respects decoration and fullscreen frame stat if (builtin.os.tag != .windows) return error.SkipZigTest; try std.testing.expectEqual( - effectiveHostWindowStyle(true, false, true) & ~@as(u32, WS_VISIBLE), + effectiveHostWindowStyle(true, false, true) & ~@as(u32, c.WS_VISIBLE), startupHostWindowStyle(true, false), ); try std.testing.expectEqual( - effectiveHostWindowStyle(false, false, true) & ~@as(u32, WS_VISIBLE), + effectiveHostWindowStyle(false, false, true) & ~@as(u32, c.WS_VISIBLE), startupHostWindowStyle(false, false), ); try std.testing.expectEqual( - effectiveHostWindowStyle(false, true, true) & ~@as(u32, WS_VISIBLE), + effectiveHostWindowStyle(false, true, true) & ~@as(u32, c.WS_VISIBLE), startupHostWindowStyle(false, true), ); - try std.testing.expect((startupHostWindowStyle(true, false) & WS_CLIPCHILDREN) != 0); - try std.testing.expect((startupHostWindowStyle(true, false) & WS_VISIBLE) == 0); + try std.testing.expect((startupHostWindowStyle(true, false) & c.WS_CLIPCHILDREN) != 0); + try std.testing.expect((startupHostWindowStyle(true, false) & c.WS_VISIBLE) == 0); } test "win32 initialDecorationsVisible follows startup source" { @@ -32314,8 +26982,8 @@ test "win32 surfaceWindowStyle clips sibling repaints" { if (builtin.os.tag != .windows) return error.SkipZigTest; const style = surfaceWindowStyle(); - try std.testing.expect((style & WS_CLIPSIBLINGS) != 0); - try std.testing.expect((style & WS_CHILD) != 0); + try std.testing.expect((style & c.WS_CLIPSIBLINGS) != 0); + try std.testing.expect((style & c.WS_CHILD) != 0); } test "win32 shouldUseIntegratedTitlebar gates on Win11 build floor" { @@ -32324,17 +26992,17 @@ test "win32 shouldUseIntegratedTitlebar gates on Win11 build floor" { try std.testing.expect(!shouldUseIntegratedTitlebar(0, .auto)); try std.testing.expect(!shouldUseIntegratedTitlebar(21_999, .auto)); try std.testing.expect(shouldUseIntegratedTitlebar(22_000, .auto)); - try std.testing.expect(shouldUseIntegratedTitlebar(OS_BUILD_WIN11_22H2, .always)); - try std.testing.expect(!shouldUseIntegratedTitlebar(OS_BUILD_WIN11_22H2, .never)); + try std.testing.expect(shouldUseIntegratedTitlebar(c.OS_BUILD_WIN11_22H2, .always)); + try std.testing.expect(!shouldUseIntegratedTitlebar(c.OS_BUILD_WIN11_22H2, .never)); } test "win32 effectiveHostWindowStyle preserves clipchildren for hosted surfaces" { if (builtin.os.tag != .windows) return error.SkipZigTest; - try std.testing.expect((effectiveHostWindowStyle(true, false, true) & WS_CLIPCHILDREN) != 0); - try std.testing.expect((effectiveHostWindowStyle(false, false, true) & WS_CLIPCHILDREN) != 0); - try std.testing.expect((effectiveHostWindowStyle(true, true, true) & WS_CLIPCHILDREN) != 0); - try std.testing.expect((effectiveHostWindowStyle(true, false, false) & WS_CLIPCHILDREN) == 0); + try std.testing.expect((effectiveHostWindowStyle(true, false, true) & c.WS_CLIPCHILDREN) != 0); + try std.testing.expect((effectiveHostWindowStyle(false, false, true) & c.WS_CLIPCHILDREN) != 0); + try std.testing.expect((effectiveHostWindowStyle(true, true, true) & c.WS_CLIPCHILDREN) != 0); + try std.testing.expect((effectiveHostWindowStyle(true, false, false) & c.WS_CLIPCHILDREN) == 0); } test "win32 sharesHostWindowState only for same-host clones" { @@ -32508,9 +27176,9 @@ test "win32 shared host topmost and opacity equality track separate window state test "win32 hostPresentShowCommand skips redundant maximize for visible hosts" { if (builtin.os.tag != .windows) return error.SkipZigTest; - try std.testing.expectEqual(@as(?i32, SW_SHOW), hostPresentShowCommand(false, false, false)); - try std.testing.expectEqual(@as(?i32, SW_MAXIMIZE), hostPresentShowCommand(false, false, true)); - try std.testing.expectEqual(@as(?i32, SW_RESTORE), hostPresentShowCommand(true, true, false)); + try std.testing.expectEqual(@as(?i32, c.SW_SHOW), hostPresentShowCommand(false, false, false)); + try std.testing.expectEqual(@as(?i32, c.SW_MAXIMIZE), hostPresentShowCommand(false, false, true)); + try std.testing.expectEqual(@as(?i32, c.SW_RESTORE), hostPresentShowCommand(true, true, false)); try std.testing.expectEqual(@as(?i32, null), hostPresentShowCommand(true, false, true)); } @@ -32697,7 +27365,7 @@ test "win32 IPC silent client read is bounded" { const pipe_name_utf8 = try std.fmt.allocPrintSentinel( std.testing.allocator, "\\\\.\\pipe\\noctty-ipc-timeout-{d}", - .{GetTickCount64()}, + .{sys.GetTickCount64()}, 0, ); defer std.testing.allocator.free(pipe_name_utf8); @@ -32707,10 +27375,10 @@ test "win32 IPC silent client read is bounded" { ); defer std.testing.allocator.free(pipe_name); - const server = CreateNamedPipeW( + const server = sys.CreateNamedPipeW( pipe_name.ptr, - PIPE_ACCESS_DUPLEX, - windows.PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | win32_ipc.pipe_nowait, + c.PIPE_ACCESS_DUPLEX, + windows.PIPE_TYPE_BYTE | c.PIPE_READMODE_BYTE | win32_ipc.pipe_nowait, 1, 1024, 1024, @@ -32720,7 +27388,7 @@ test "win32 IPC silent client read is bounded" { try std.testing.expect(server != windows.INVALID_HANDLE_VALUE); defer _ = windows.CloseHandle(server); - try std.testing.expectEqual(@as(BOOL, 0), ConnectNamedPipe(server, null)); + try std.testing.expectEqual(@as(BOOL, 0), sys.ConnectNamedPipe(server, null)); try std.testing.expectEqual(windows.Win32Error.PIPE_LISTENING, windows.kernel32.GetLastError()); const client = windows.kernel32.CreateFileW( @@ -32735,7 +27403,7 @@ test "win32 IPC silent client read is bounded" { try std.testing.expect(client != windows.INVALID_HANDLE_VALUE); defer _ = windows.CloseHandle(client); - const connected = ConnectNamedPipe(server, null); + const connected = sys.ConnectNamedPipe(server, null); if (connected == 0) { try std.testing.expectEqual(windows.Win32Error.PIPE_CONNECTED, windows.kernel32.GetLastError()); } @@ -33223,106 +27891,6 @@ test "win32 non-WSL split cwd keeps startup cwd candidate" { )); } -test "win32 profileIndexByKey finds launch profile key" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var profiles = [_]windows_shell.Profile{ - .{ - .kind = .cmd, - .key = "cmd.exe", - .label = "Command Prompt", - .command = .{ .shell = "cmd.exe" }, - }, - .{ - .kind = .wsl_distro, - .key = "wsl:Ubuntu", - .label = "Ubuntu", - .command = .{ .shell = "wsl.exe" }, - }, - }; - - try std.testing.expectEqual(@as(?usize, 1), profileIndexByKey(&profiles, "wsl:Ubuntu")); - try std.testing.expectEqual(@as(?usize, null), profileIndexByKey(&profiles, "missing")); -} - -test "win32 buildWindowTitle appends active status segments" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const title = try buildWindowTitle(std.testing.allocator, "pwsh", .{ - .pwd = "/Users/amant", - .scrollbar = .{ - .total = 200, - .offset = 50, - .len = 40, - }, - .readonly = true, - .secure_input = true, - .key_sequence_active = true, - .key_table_name = "resize", - .search = .{ - .active = true, - .needle = "foo", - .total = 4, - .selected = 2, - }, - .progress = "progress:35%", - }); - defer std.testing.allocator.free(title); - - try std.testing.expectEqualStrings( - "pwsh | readonly | sensitive | keys | table:resize | cwd:/Users/amant | find:foo (2/4) | progress:35%", - title, - ); -} - -test "win32 buildWindowTitle uses default title when base is null" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const title = try buildWindowTitle(std.testing.allocator, null, .{}); - defer std.testing.allocator.free(title); - - try std.testing.expectEqualStrings("noctty", title); -} - -test "win32 resolveWindowBaseTitle prefers tab then surface override" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqualStrings( - "tab", - resolveWindowBaseTitle("terminal", "surface", "tab").?, - ); - try std.testing.expectEqualStrings( - "surface", - resolveWindowBaseTitle("terminal", "surface", null).?, - ); - try std.testing.expectEqualStrings( - "terminal", - resolveWindowBaseTitle("terminal", null, null).?, - ); -} - -test "win32 effectiveBackgroundOpacity respects opaque override" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(f64, 0.4), effectiveBackgroundOpacity(0.4, false)); - try std.testing.expectEqual(@as(f64, 1.0), effectiveBackgroundOpacity(0.4, true)); - try std.testing.expectEqual(@as(u8, 128), alphaByteForOpacity(0.5)); - try std.testing.expectEqual(@as(u8, 1), hiddenScrollbarAlphaByte()); -} - -test "win32 resizeSplitFallbackDelta maps directions to window deltas" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqualDeep( - ResizeSplitFallbackDelta{ .width = -24, .height = 0 }, - resizeSplitFallbackDelta(.{ .amount = 24, .direction = .left }), - ); - try std.testing.expectEqualDeep( - ResizeSplitFallbackDelta{ .width = 0, .height = 12 }, - resizeSplitFallbackDelta(.{ .amount = 12, .direction = .down }), - ); -} - test "win32 splitDirectionFromAction preserves requested split direction" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -33399,47 +27967,6 @@ test "win32 session save skips quick terminal tabs" { try std.testing.expectEqual(@as(usize, 0), mixed_state.windows[0].selected_tab); } -fn sessionStatePolicyAllows(safe_mode: bool, policy: configpkg.Config.WindowSaveState) bool { - return !safe_mode and policy != .never; -} - -const SessionRestoreTransaction = struct { - app: ?*App = null, - host: ?*Host = null, - committed: bool = false, - - fn noteSurface(self: *SessionRestoreTransaction, surface: *Surface) void { - if (self.host == null) { - self.app = surface.app; - self.host = surface.host; - } - } - - fn commit(self: *SessionRestoreTransaction) void { - self.committed = true; - } - - fn rollback(self: *SessionRestoreTransaction) void { - if (self.committed) return; - const app = self.app orelse return; - const host = self.host orelse return; - app.rollbackSessionRestoreHost(host); - } -}; - -fn sessionRestorePolicyAllows( - safe_mode: bool, - policy: configpkg.Config.WindowSaveState, - initial_window: bool, - has_initial_command: bool, - startup_profile_picker: bool, -) bool { - return sessionStatePolicyAllows(safe_mode, policy) and - initial_window and - !has_initial_command and - !startup_profile_picker; -} - test "win32 safe mode never mutates saved session state" { try std.testing.expect(!sessionStatePolicyAllows(true, .default)); try std.testing.expect(!sessionStatePolicyAllows(true, .always)); @@ -33558,256 +28085,30 @@ test "win32 session restore rebuilds saved split tree shape" { try std.testing.expectEqual(@as(SplitTreeSurface.Node.Handle, @enumFromInt(1)), tree.nodes[0].split.left); try std.testing.expectEqual(@as(SplitTreeSurface.Node.Handle, @enumFromInt(2)), tree.nodes[0].split.right); try std.testing.expectEqual(&surface_a, tree.nodes[1].leaf); - try std.testing.expectEqual(SplitTreeSurface.Split.Layout.vertical, tree.nodes[2].split.layout); - try std.testing.expectEqual(@as(f16, 0.75), tree.nodes[2].split.ratio); - try std.testing.expectEqual(@as(SplitTreeSurface.Node.Handle, @enumFromInt(3)), tree.nodes[2].split.left); - try std.testing.expectEqual(@as(SplitTreeSurface.Node.Handle, @enumFromInt(4)), tree.nodes[2].split.right); - try std.testing.expectEqual(&surface_b, tree.nodes[3].leaf); - try std.testing.expectEqual(&surface_c, tree.nodes[4].leaf); -} - -test "win32 session restore rejects split tree with missing pane surface" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const layout_nodes = [_]win32_session_state.Node{ - .{ .pane = .{ .cwd = "C:\\left" } }, - }; - const node_surfaces = [_]?*Surface{null}; - - try std.testing.expectError( - error.InvalidTreeShape, - App.buildRestoredSessionSplitTree( - std.testing.allocator, - .{ .root = 0, .nodes = &layout_nodes }, - &node_surfaces, - ), - ); -} - -test "win32 nextInspectorVisible follows requested mode" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(nextInspectorVisible(false, .toggle)); - try std.testing.expect(!nextInspectorVisible(true, .toggle)); - try std.testing.expect(nextInspectorVisible(false, .show)); - try std.testing.expect(!nextInspectorVisible(true, .hide)); -} - -test "win32 tab inspector toggle hides any active pane inspector" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(nextTabInspectorVisible(false, .toggle)); - try std.testing.expect(!nextTabInspectorVisible(true, .toggle)); - try std.testing.expect(nextTabInspectorVisible(true, .show)); - try std.testing.expect(!nextTabInspectorVisible(false, .hide)); -} - -test "win32 primarySurfaceIndex prefers active tab of first host" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const entries = [_]SurfaceOrderEntry{ - .{ .host_id = 11, .host_active = false }, - .{ .host_id = 22, .host_active = true }, - .{ .host_id = 11, .host_active = true }, - }; - try std.testing.expectEqual(@as(?usize, 2), primarySurfaceIndex(&entries)); - - const fallback = [_]SurfaceOrderEntry{ - .{ .host_id = 11, .host_active = false }, - .{ .host_id = 22, .host_active = true }, - }; - try std.testing.expectEqual(@as(?usize, 0), primarySurfaceIndex(&fallback)); -} - -test "win32 buildHostAwareBaseTitle prefixes host tab position" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const titled = try buildHostAwareBaseTitle(std.testing.allocator, "pwsh", .{ - .index = 1, - .total = 3, - }); - defer std.testing.allocator.free(titled); - try std.testing.expectEqualStrings("[2/3] pwsh", titled); - - const single = try buildHostAwareBaseTitle(std.testing.allocator, "pwsh", .{ - .index = 0, - .total = 1, - }); - defer std.testing.allocator.free(single); - try std.testing.expectEqualStrings("pwsh", single); -} - -test "win32 buildTabButtonLabel marks active tab and pane count" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const title = try buildTabButtonLabel(std.testing.allocator, "pwsh", 1, true, 3, 24, true); - defer std.testing.allocator.free(title); - try std.testing.expectEqualStrings("* 2: pwsh (3)", title); -} - -test "win32 buildTabButtonLabel omits pane count for single pane tabs" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const title = try buildTabButtonLabel(std.testing.allocator, "pwsh", 0, false, 1, 24, false); - defer std.testing.allocator.free(title); - try std.testing.expectEqualStrings("1: pwsh", title); -} - -test "win32 buildTabButtonLabel compacts long titles" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const title = try buildTabButtonLabel(std.testing.allocator, "this-is-a-very-long-terminal-title", 0, false, 1, 24, false); - defer std.testing.allocator.free(title); - try std.testing.expectEqualStrings("1: this-is-a-very-long-t...", title); -} - -test "win32 buildTabButtonLabel drops pane count when tabs are narrow" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const title = try buildTabButtonLabel(std.testing.allocator, "logs-and-output-pane", 1, false, 3, 9, false); - defer std.testing.allocator.free(title); - try std.testing.expectEqualStrings("2: logs-a...", title); -} - -test "win32 hostTabLabelMaxLen shrinks with narrow tab widths" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(usize, 24), hostTabLabelMaxLen(260)); - try std.testing.expectEqual(@as(usize, 9), hostTabLabelMaxLen(98)); - try std.testing.expectEqual(@as(usize, 6), hostTabLabelMaxLen(40)); - try std.testing.expect(shouldShowPaneCount(180, 3)); - try std.testing.expect(!shouldShowPaneCount(120, 3)); -} - -test "win32 visibleTabRange windows tabs around the active tab" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqualDeep(VisibleTabRange{ .start = 0, .count = 3 }, visibleTabRange(6, 0, 324)); - try std.testing.expectEqualDeep(VisibleTabRange{ .start = 2, .count = 3 }, visibleTabRange(6, 3, 324)); - try std.testing.expectEqualDeep(VisibleTabRange{ .start = 3, .count = 3 }, visibleTabRange(6, 5, 324)); - try std.testing.expectEqualDeep(VisibleTabRange{ .start = 0, .count = 2 }, visibleTabRange(2, 1, 500)); -} - -test "win32 buildTabOverviewBannerText lists active tabs and pane counts" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const banner = try buildTabOverviewBannerText(std.testing.allocator, &.{ - .{ .title = "pwsh", .pane_count = 1, .active = true }, - .{ .title = "logs-and-output-pane", .pane_count = 3, .active = false }, - }); - defer std.testing.allocator.free(banner); - try std.testing.expectEqualStrings("Tabs: *1:pwsh | 2:logs-and-output... (3)", banner); -} - -test "win32 buildSearchButtonLabel reflects active search state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const active = try buildSearchButtonLabel(std.testing.allocator, true, 8, 2); - defer std.testing.allocator.free(active); - try std.testing.expectEqualStrings("[F] 2/8", active); - - const passive = try buildSearchButtonLabel(std.testing.allocator, false, 5, null); - defer std.testing.allocator.free(passive); - try std.testing.expectEqualStrings("Find 5", passive); - - const idle = try buildSearchButtonLabel(std.testing.allocator, false, null, null); - defer std.testing.allocator.free(idle); - try std.testing.expectEqualStrings("Find", idle); -} - -test "win32 buildProfilesButtonLabel reflects selected cached profile" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profiles = [_]windows_shell.Profile{ - .{ - .kind = .pwsh, - .key = "pwsh.exe", - .label = "PowerShell", - .command = .{ .direct = &.{"pwsh.exe"} }, - }, - .{ - .kind = .git_bash, - .key = "git-bash", - .label = "Git Bash", - .command = .{ .direct = &.{"bash.exe"} }, - }, - }; - - const active = try buildProfilesButtonLabel(std.testing.allocator, true, &profiles, 1, 1); - defer std.testing.allocator.free(active); - try std.testing.expectEqualStrings("Pick Git Bash", active); - - const idle = try buildProfilesButtonLabel(std.testing.allocator, false, &profiles, 0, null); - defer std.testing.allocator.free(idle); - try std.testing.expectEqualStrings("Launch Power...", idle); -} - -test "win32 profilesButtonKeyAction maps focused launcher keys" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(ProfilesButtonKeyAction.open, profilesButtonKeyAction(VK_RETURN).?); - try std.testing.expectEqual(ProfilesButtonKeyAction.toggle, profilesButtonKeyAction(VK_SPACE).?); - try std.testing.expectEqual(ProfilesButtonKeyAction.previous, profilesButtonKeyAction(VK_LEFT).?); - try std.testing.expectEqual(ProfilesButtonKeyAction.next, profilesButtonKeyAction(VK_DOWN).?); - try std.testing.expectEqual(ProfilesButtonKeyAction.first, profilesButtonKeyAction(VK_HOME).?); - try std.testing.expectEqual(ProfilesButtonKeyAction.last, profilesButtonKeyAction(VK_END).?); -} - -test "win32 profileShortcutIndexFromKey supports top row and numpad digits" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(?usize, 0), profileShortcutIndexFromKey('1')); - try std.testing.expectEqual(@as(?usize, 4), profileShortcutIndexFromKey('5')); - try std.testing.expectEqual(@as(?usize, 8), profileShortcutIndexFromKey('9')); - try std.testing.expectEqual(@as(?usize, 0), profileShortcutIndexFromKey(0x61)); - try std.testing.expectEqual(@as(?usize, 8), profileShortcutIndexFromKey(0x69)); - try std.testing.expectEqual(@as(?usize, null), profileShortcutIndexFromKey('0')); - try std.testing.expectEqual(@as(?usize, null), profileShortcutIndexFromKey('A')); -} - -test "win32 quickSlotShortcutProfileIndex maps visible launcher slots" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(?usize, 1), quickSlotShortcutProfileIndex(5, 0, '1', true)); - try std.testing.expectEqual(@as(?usize, 2), quickSlotShortcutProfileIndex(5, 0, '2', true)); - try std.testing.expectEqual(@as(?usize, 3), quickSlotShortcutProfileIndex(5, 0, '3', true)); - try std.testing.expectEqual(@as(?usize, 0), quickSlotShortcutProfileIndex(5, 3, '1', true)); - try std.testing.expectEqual(@as(?usize, 1), quickSlotShortcutProfileIndex(5, 3, '2', true)); - try std.testing.expectEqual(@as(?usize, null), quickSlotShortcutProfileIndex(5, 0, '4', true)); - try std.testing.expectEqual(@as(?usize, null), quickSlotShortcutProfileIndex(5, 0, '1', false)); -} - -test "win32 quickSlotPinOrdinalFromKey maps visible pin slots" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(?usize, 0), quickSlotPinOrdinalFromKey('1', true, true)); - try std.testing.expectEqual(@as(?usize, 2), quickSlotPinOrdinalFromKey('3', true, true)); - try std.testing.expectEqual(@as(?usize, null), quickSlotPinOrdinalFromKey('4', true, true)); - try std.testing.expectEqual(@as(?usize, null), quickSlotPinOrdinalFromKey('1', true, false)); - try std.testing.expectEqual(@as(?usize, null), quickSlotPinOrdinalFromKey('1', false, true)); + try std.testing.expectEqual(SplitTreeSurface.Split.Layout.vertical, tree.nodes[2].split.layout); + try std.testing.expectEqual(@as(f16, 0.75), tree.nodes[2].split.ratio); + try std.testing.expectEqual(@as(SplitTreeSurface.Node.Handle, @enumFromInt(3)), tree.nodes[2].split.left); + try std.testing.expectEqual(@as(SplitTreeSurface.Node.Handle, @enumFromInt(4)), tree.nodes[2].split.right); + try std.testing.expectEqual(&surface_b, tree.nodes[3].leaf); + try std.testing.expectEqual(&surface_c, tree.nodes[4].leaf); } -test "win32 clearQuickSlotPinsRequested detects clear shortcut" { +test "win32 session restore rejects split tree with missing pane surface" { if (builtin.os.tag != .windows) return error.SkipZigTest; - try std.testing.expect(clearQuickSlotPinsRequested(VK_0, true, true)); - try std.testing.expect(clearQuickSlotPinsRequested(VK_NUMPAD0, true, true)); - try std.testing.expect(!clearQuickSlotPinsRequested('1', true, true)); - try std.testing.expect(!clearQuickSlotPinsRequested(VK_0, true, false)); - try std.testing.expect(!clearQuickSlotPinsRequested(VK_0, false, true)); -} - -test "win32 quickSlotFocusKeyAction maps painted quick slot focus keys" { - if (builtin.os.tag != .windows) return error.SkipZigTest; + const layout_nodes = [_]win32_session_state.Node{ + .{ .pane = .{ .cwd = "C:\\left" } }, + }; + const node_surfaces = [_]?*Surface{null}; - try std.testing.expectEqual(QuickSlotFocusKeyAction.previous, quickSlotFocusKeyAction(VK_LEFT).?); - try std.testing.expectEqual(QuickSlotFocusKeyAction.previous, quickSlotFocusKeyAction(VK_UP).?); - try std.testing.expectEqual(QuickSlotFocusKeyAction.next, quickSlotFocusKeyAction(VK_RIGHT).?); - try std.testing.expectEqual(QuickSlotFocusKeyAction.next, quickSlotFocusKeyAction(VK_DOWN).?); - try std.testing.expectEqual(QuickSlotFocusKeyAction.first, quickSlotFocusKeyAction(VK_HOME).?); - try std.testing.expectEqual(QuickSlotFocusKeyAction.last, quickSlotFocusKeyAction(VK_END).?); - try std.testing.expectEqual(QuickSlotFocusKeyAction.open, quickSlotFocusKeyAction(VK_RETURN).?); - try std.testing.expect(quickSlotFocusKeyAction(VK_SPACE) == null); + try std.testing.expectError( + error.InvalidTreeShape, + App.buildRestoredSessionSplitTree( + std.testing.allocator, + .{ .root = 0, .nodes = &layout_nodes }, + &node_surfaces, + ), + ); } test "win32 taskbar progress mapping preserves state and percent" { @@ -33860,23 +28161,6 @@ test "win32 taskbar progress mapping covers set error and indeterminate" { try std.testing.expect(busy.value == null); } -test "win32 progress status and taskbar mapping clamp percent to shell range" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const report: terminal.osc.Command.ProgressReport = .{ - .state = .set, - .progress = 255, - }; - - const mapped = win32_taskbar_progress.mapProgressReport(report); - try std.testing.expectEqual(@as(u64, 100), mapped.value.?.completed); - try std.testing.expectEqual(@as(u64, 100), mapped.value.?.total); - - const status = try formatProgressStatus(std.testing.allocator, report); - defer if (status) |owned| std.testing.allocator.free(owned); - try std.testing.expectEqualStrings("progress:100%", status.?); -} - test "win32 activeSurfaceForHost isolates taskbar progress by host and tab" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -34203,27 +28487,15 @@ test "win32 cycleProfileOpenTarget wraps launcher target order" { try std.testing.expectEqual(ProfileOpenTarget.split, cycleProfileOpenTarget(.tab, true)); } -test "win32 launchTargetButtonLabel reflects selected launcher slot" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const pane = try launchTargetButtonLabel(std.testing.allocator, .split, 2, 2); - defer std.testing.allocator.free(pane); - try std.testing.expectEqualStrings("*3 Pane", pane); - - const tab = try launchTargetButtonLabel(std.testing.allocator, .tab, null, null); - defer std.testing.allocator.free(tab); - try std.testing.expectEqualStrings("Tab", tab); -} - test "win32 launchTargetButtonKeyAction maps focused target keys" { if (builtin.os.tag != .windows) return error.SkipZigTest; - try std.testing.expectEqual(LaunchTargetButtonKeyAction.previous, launchTargetButtonKeyAction(VK_LEFT).?); - try std.testing.expectEqual(LaunchTargetButtonKeyAction.previous, launchTargetButtonKeyAction(VK_UP).?); - try std.testing.expectEqual(LaunchTargetButtonKeyAction.next, launchTargetButtonKeyAction(VK_RIGHT).?); - try std.testing.expectEqual(LaunchTargetButtonKeyAction.next, launchTargetButtonKeyAction(VK_SPACE).?); - try std.testing.expectEqual(LaunchTargetButtonKeyAction.first, launchTargetButtonKeyAction(VK_HOME).?); - try std.testing.expectEqual(LaunchTargetButtonKeyAction.last, launchTargetButtonKeyAction(VK_END).?); + try std.testing.expectEqual(LaunchTargetButtonKeyAction.previous, launchTargetButtonKeyAction(c.VK_LEFT).?); + try std.testing.expectEqual(LaunchTargetButtonKeyAction.previous, launchTargetButtonKeyAction(c.VK_UP).?); + try std.testing.expectEqual(LaunchTargetButtonKeyAction.next, launchTargetButtonKeyAction(c.VK_RIGHT).?); + try std.testing.expectEqual(LaunchTargetButtonKeyAction.next, launchTargetButtonKeyAction(c.VK_SPACE).?); + try std.testing.expectEqual(LaunchTargetButtonKeyAction.first, launchTargetButtonKeyAction(c.VK_HOME).?); + try std.testing.expectEqual(LaunchTargetButtonKeyAction.last, launchTargetButtonKeyAction(c.VK_END).?); } test "win32 parseProfileOpenTarget accepts terminal-style launch target names" { @@ -34237,306 +28509,6 @@ test "win32 parseProfileOpenTarget accepts terminal-style launch target names" { try std.testing.expect(parseProfileOpenTarget("definitely_not_real") == null); } -test "win32 launchTargetButtonLabel reflects Windows-style target wording" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const tab = try launchTargetButtonLabel(std.testing.allocator, .tab, null, null); - defer std.testing.allocator.free(tab); - try std.testing.expectEqualStrings("Tab", tab); - - const win = try launchTargetButtonLabel(std.testing.allocator, .window, null, null); - defer std.testing.allocator.free(win); - try std.testing.expectEqualStrings("Win", win); - - const pane = try launchTargetButtonLabel(std.testing.allocator, .split, null, null); - defer std.testing.allocator.free(pane); - try std.testing.expectEqualStrings("Pane", pane); -} - -test "win32 preferredProfileIndex respects host key then app key then hint" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profiles = [_]windows_shell.Profile{ - .{ - .kind = .pwsh, - .key = "pwsh.exe", - .label = "PowerShell", - .command = .{ .direct = &.{"pwsh.exe"} }, - }, - .{ - .kind = .git_bash, - .key = "git-bash", - .label = "Git Bash", - .command = .{ .direct = &.{"bash.exe"} }, - }, - .{ - .kind = .cmd, - .key = "cmd.exe", - .label = "Command Prompt", - .command = .{ .direct = &.{"cmd.exe"} }, - }, - }; - - try std.testing.expectEqual(@as(?usize, 1), preferredProfileIndex(&profiles, "git-bash", null, "cmd", 0)); - try std.testing.expectEqual(@as(?usize, 2), preferredProfileIndex(&profiles, null, "cmd.exe", "pwsh", 0)); - try std.testing.expectEqual(@as(?usize, 0), preferredProfileIndex(&profiles, null, null, "power", 2)); - try std.testing.expectEqual(@as(?usize, null), preferredProfileIndex(&profiles, null, null, "definitely_not_real", 1)); -} - -test "win32 resolveProfileSelection supports index and prefix matching" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profiles = [_]windows_shell.Profile{ - .{ - .kind = .pwsh, - .key = "pwsh.exe", - .label = "PowerShell", - .command = .{ .direct = &.{"pwsh.exe"} }, - }, - .{ - .kind = .wsl_distro, - .key = "Ubuntu", - .label = "WSL: Ubuntu", - .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu", "~" } }, - }, - .{ - .kind = .wsl_distro, - .key = "Ubuntu-Preview", - .label = "WSL: Ubuntu Preview", - .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu-Preview", "~" } }, - }, - }; - - try std.testing.expectEqualDeep(ProfileSelection{ .exact = 0 }, resolveProfileSelection(&profiles, "", 0)); - try std.testing.expectEqualDeep(ProfileSelection{ .exact = 1 }, resolveProfileSelection(&profiles, "2", 0)); - try std.testing.expectEqualDeep(ProfileSelection{ .exact = 0 }, resolveProfileSelection(&profiles, "powers", 1)); - try std.testing.expectEqualDeep(ProfileSelection{ .ambiguous = 2 }, resolveProfileSelection(&profiles, "ubu", 0)); - try std.testing.expectEqualDeep(ProfileSelection.invalid, resolveProfileSelection(&profiles, "9", 0)); -} - -test "win32 buildProfileOverlayLabel and hint reflect selected profile" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profiles = [_]windows_shell.Profile{ - .{ - .kind = .pwsh, - .key = "pwsh.exe", - .label = "PowerShell", - .command = .{ .direct = &.{"pwsh.exe"} }, - }, - .{ - .kind = .cmd, - .key = "cmd.exe", - .label = "Command Prompt", - .command = .{ .direct = &.{"cmd.exe"} }, - }, - }; - - const label = try buildProfileOverlayLabel(std.testing.allocator, &profiles, "cmd", 0); - defer std.testing.allocator.free(label); - try std.testing.expectEqualStrings("Profile 2/2 CMD C>", label); - - const hint = try buildProfileHintText(std.testing.allocator, &profiles, "cmd", 0, .window, .{ "pwsh.exe", "cmd.exe", null }); - defer std.testing.allocator.free(hint); - try std.testing.expect(std.mem.indexOf(u8, hint, "CMD C>") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "Command Prompt") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "run cmd.exe") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "Pinned slot 2.") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "opens a new window") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "Alt+1-3 launches visible slots") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "Alt+Shift+1-3 pins the current profile") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "Alt+Shift+0 clears pinning") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "Quick picks: 1 PWSH >>") != null); - try std.testing.expect(std.mem.indexOf(u8, hint, "2 CMD C>") != null); -} - -test "win32 buildProfileDetailText reflects selected launcher state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profile: windows_shell.Profile = .{ - .kind = .pwsh, - .key = "pwsh.exe", - .label = "PowerShell", - .command = .{ .direct = &.{"pwsh.exe"} }, - }; - const profiles = [_]windows_shell.Profile{ - profile, - .{ - .kind = .git_bash, - .key = "git-bash", - .label = "Git Bash", - .command = .{ .direct = &.{"bash.exe"} }, - }, - .{ - .kind = .wsl_distro, - .key = "wsl:Ubuntu", - .label = "WSL: Ubuntu", - .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu" } }, - }, - }; - - const overlay = try buildProfileDetailText(std.testing.allocator, &profile, &profiles, true, .split, "git,pwsh,Ubuntu,cmd", .{ "pwsh.exe", "git-bash", null }); - defer std.testing.allocator.free(overlay); - try std.testing.expect(std.mem.indexOf(u8, overlay, "Selected profile: PWSH >> PowerShell") != null); - try std.testing.expect(std.mem.indexOf(u8, overlay, "Run pwsh.exe") != null); - try std.testing.expect(std.mem.indexOf(u8, overlay, "Pinned slot 1.") != null); - try std.testing.expect(std.mem.indexOf(u8, overlay, "opens a split") != null); - try std.testing.expect(std.mem.indexOf(u8, overlay, "Alt+1-3 launches visible slots") != null); - try std.testing.expect(std.mem.indexOf(u8, overlay, "Alt+Shift+1-3 pins the current profile") != null); - try std.testing.expect(std.mem.indexOf(u8, overlay, "Alt+Shift+0 clears pinning") != null); - try std.testing.expect(std.mem.indexOf(u8, overlay, "Quick picks: 1 PWSH >> PowerShell") != null); - try std.testing.expect(std.mem.indexOf(u8, overlay, "Order: git > pwsh > Ubuntu > cmd.") != null); - - const idle = try buildProfileDetailText(std.testing.allocator, &profile, &profiles, false, .window, "git,pwsh,Ubuntu,cmd", .{ "pwsh.exe", "git-bash", null }); - defer std.testing.allocator.free(idle); - try std.testing.expect(std.mem.indexOf(u8, idle, "Default profile: PWSH >> PowerShell") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "Run pwsh.exe") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "Pinned slot 1.") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "New hosts inherit this PowerShell profile with automatic shell integration.") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "opens a new window") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "Alt+1-3 launches visible slots") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "Alt+Shift+1-3 pins the current profile") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "Alt+Shift+0 clears pinning") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "Top slots: 1 PWSH >>") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "2 GIT $> Git Bash") != null); - try std.testing.expect(std.mem.indexOf(u8, idle, "Order: git > pwsh > Ubuntu > cmd.") != null); -} - -test "win32 buildProfileDetailText appends shell integration guidance when present" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profile: windows_shell.Profile = .{ - .kind = .cmd, - .key = "cmd.exe", - .label = "Command Prompt", - .command = .{ .direct = &.{"cmd.exe"} }, - }; - - const detail = try buildProfileDetailText( - std.testing.allocator, - &profile, - &.{profile}, - false, - .tab, - null, - .{ null, null, null }, - ); - defer std.testing.allocator.free(detail); - - try std.testing.expect(std.mem.indexOf( - u8, - detail, - "New hosts inherit this Command Prompt profile; shell integration unavailable.", - ) != null); - try std.testing.expect(std.mem.indexOf( - u8, - detail, - "Use PowerShell or WSL when prompt marking or cwd inheritance is required.", - ) != null); - try std.testing.expect(std.mem.indexOf(u8, detail, "+ opens a new tab") != null); -} - -test "win32 buildProfileCommandPreviewText compacts shell command preview" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profile: windows_shell.Profile = .{ - .kind = .wsl_distro, - .key = "Ubuntu", - .label = "WSL: Ubuntu", - .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu", "--cd", "~" } }, - }; - - const preview = try buildProfileCommandPreviewText(std.testing.allocator, &profile, 14); - defer std.testing.allocator.free(preview); - try std.testing.expectEqualStrings("wsl.exe -d ...", preview); -} - -test "win32 buildProfileOrderSummaryText compacts launcher order" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const summary = (try buildProfileOrderSummaryText( - std.testing.allocator, - "git,pwsh,Ubuntu,cmd,powershell", - 4, - )).?; - defer std.testing.allocator.free(summary); - try std.testing.expectEqualStrings("git > pwsh > Ubuntu > cmd > ...", summary); -} - -test "win32 buildProfileQuickPickText reflects ordered launcher profiles" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profiles = [_]windows_shell.Profile{ - .{ - .kind = .git_bash, - .key = "git-bash", - .label = "Git Bash", - .command = .{ .direct = &.{"bash.exe"} }, - }, - .{ - .kind = .pwsh, - .key = "pwsh.exe", - .label = "PowerShell", - .command = .{ .direct = &.{"pwsh.exe"} }, - }, - .{ - .kind = .wsl_distro, - .key = "wsl:Ubuntu", - .label = "WSL: Ubuntu", - .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu" } }, - }, - .{ - .kind = .cmd, - .key = "cmd.exe", - .label = "Command Prompt", - .command = .{ .direct = &.{"cmd.exe"} }, - }, - .{ - .kind = .powershell, - .key = "powershell.exe", - .label = "Windows PowerShell", - .command = .{ .direct = &.{"powershell.exe"} }, - }, - }; - - const quick = (try buildProfileQuickPickText(std.testing.allocator, &profiles, 4, 10)).?; - defer std.testing.allocator.free(quick); - try std.testing.expectEqualStrings( - "1 GIT $> Git Bash | 2 PWSH >> PowerShell | 3 WSL <> WSL: Ub... | 4 CMD C> Command... | ...", - quick, - ); -} - -test "win32 buildProfileStatusBadgeText reflects selected profile kind" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profile: windows_shell.Profile = .{ - .kind = .git_bash, - .key = "git-bash", - .label = "Git Bash", - .command = .{ .direct = &.{"bash.exe"} }, - }; - - const badge = try buildProfileStatusBadgeText(std.testing.allocator, &profile, 0, 0); - defer std.testing.allocator.free(badge); - try std.testing.expectEqualStrings("Git Bash", badge); -} - -test "win32 profileStatusBadgeTextLen matches built text" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profile: windows_shell.Profile = .{ - .kind = .git_bash, - .key = "git-bash", - .label = "Git Bash", - .command = .{ .direct = &.{"bash.exe"} }, - }; - - const badge = try buildProfileStatusBadgeText(std.testing.allocator, &profile, 0, 0); - defer std.testing.allocator.free(badge); - try std.testing.expectEqual(badge.len, profileStatusBadgeTextLen(&profile, 0, 0)); -} - test "win32 windowNewTabInsertIndex respects current and end positions" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -34720,40 +28692,6 @@ test "win32 prepareActiveTabVisibility hides inactive tab surfaces first" { try std.testing.expect(!surface_c.host_active); } -test "win32 buildProfileQuickSlotChipText reflects ordered quick slot badge" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profile: windows_shell.Profile = .{ - .kind = .wsl_distro, - .key = "wsl:Ubuntu", - .label = "WSL: Ubuntu", - .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu" } }, - }; - - const chip = try buildProfileQuickSlotChipText(std.testing.allocator, &profile, 2, 2); - defer std.testing.allocator.free(chip); - try std.testing.expectEqualStrings("*3 WSL", chip); -} - -test "win32 profileQuickSlotChipTextLen matches built text" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const profile: windows_shell.Profile = .{ - .kind = .wsl_distro, - .key = "wsl:Ubuntu", - .label = "WSL: Ubuntu", - .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu" } }, - }; - - const pinned = try buildProfileQuickSlotChipText(std.testing.allocator, &profile, 2, 2); - defer std.testing.allocator.free(pinned); - try std.testing.expectEqual(pinned.len, profileQuickSlotChipTextLen(&profile, 2, 2)); - - const unpinned = try buildProfileQuickSlotChipText(std.testing.allocator, &profile, 1, null); - defer std.testing.allocator.free(unpinned); - try std.testing.expectEqual(unpinned.len, profileQuickSlotChipTextLen(&profile, 1, null)); -} - test "win32 quickSlotChipColors follow profile hover accent" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -34766,99 +28704,26 @@ test "win32 quickSlotChipColors follow profile hover accent" { const hovered = quickSlotChipColors(.git_bash, true, true); try std.testing.expectEqual(rgb(58, 48, 37), hovered.bg); try std.testing.expectEqual(rgb(236, 182, 118), hovered.border); - try std.testing.expectEqual(rgb(248, 202, 134), hovered.fg); - - // Light mode - const idle_light = quickSlotChipColors(.git_bash, false, false); - try std.testing.expectEqual(rgb(252, 244, 228), idle_light.bg); - try std.testing.expectEqual(rgb(168, 120, 24), idle_light.border); - try std.testing.expectEqual(rgb(120, 80, 4), idle_light.fg); -} - -test "win32 pinnedChipMarkerColor follows profile accent" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - // Dark mode - try std.testing.expectEqual(rgb(248, 202, 134), pinnedChipMarkerColor(.git_bash, true, false)); - try std.testing.expectEqual(rgb(255, 224, 178), pinnedChipMarkerColor(.git_bash, true, true)); - try std.testing.expectEqual(rgb(136, 216, 242), pinnedChipMarkerColor(.pwsh, true, false)); - - // Light mode - try std.testing.expectEqual(rgb(140, 96, 8), pinnedChipMarkerColor(.git_bash, false, false)); - try std.testing.expectEqual(rgb(120, 80, 4), pinnedChipMarkerColor(.git_bash, false, true)); -} - -test "win32 launcherChipRightInset reserves badge and target space" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(i32, 5), launcherChipRightInset(false, false)); - try std.testing.expectEqual(@as(i32, 12), launcherChipRightInset(false, true)); - try std.testing.expectEqual(@as(i32, 16), launcherChipRightInset(true, false)); - try std.testing.expectEqual(@as(i32, 16), launcherChipRightInset(true, true)); -} - -test "win32 targetButtonLabelRightInset reserves target badge space" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(i32, 0), targetButtonLabelRightInset(null)); - try std.testing.expectEqual(@as(i32, 12), targetButtonLabelRightInset(.tab)); - try std.testing.expectEqual(@as(i32, 12), targetButtonLabelRightInset(.window)); - try std.testing.expectEqual(@as(i32, 12), targetButtonLabelRightInset(.split)); -} - -test "win32 buttonLabelRightInset reserves slot and target badge space" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(i32, 0), buttonLabelRightInset(null, null)); - try std.testing.expectEqual(@as(i32, 12), buttonLabelRightInset(null, .tab)); - try std.testing.expectEqual(@as(i32, 16), buttonLabelRightInset(0, null)); - try std.testing.expectEqual(@as(i32, 16), buttonLabelRightInset(0, .split)); - try std.testing.expectEqual(@as(i32, 12), buttonLabelRightInset(9, .window)); -} - -test "win32 shouldPaintQuickSlotTargetMarker follows active chip state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!shouldPaintQuickSlotTargetMarker(false, false)); - try std.testing.expect(shouldPaintQuickSlotTargetMarker(true, false)); - try std.testing.expect(shouldPaintQuickSlotTargetMarker(false, true)); -} - -test "win32 profileOpenTargetMarkerColor reflects launcher target" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(rgb(132, 172, 238), profileOpenTargetMarkerColor(.tab)); - try std.testing.expectEqual(rgb(236, 182, 118), profileOpenTargetMarkerColor(.window)); - try std.testing.expectEqual(rgb(126, 204, 148), profileOpenTargetMarkerColor(.split)); -} - -test "win32 profileOpenTargetBadgeGlyph reflects launcher target" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(u8, 'T'), profileOpenTargetBadgeGlyph(.tab)); - try std.testing.expectEqual(@as(u8, 'W'), profileOpenTargetBadgeGlyph(.window)); - try std.testing.expectEqual(@as(u8, 'S'), profileOpenTargetBadgeGlyph(.split)); -} - -test "win32 pinnedSlotBadgeDigit reflects visible quick slot ordinals" { - if (builtin.os.tag != .windows) return error.SkipZigTest; + try std.testing.expectEqual(rgb(248, 202, 134), hovered.fg); - try std.testing.expectEqual(@as(?u8, '1'), pinnedSlotBadgeDigit(0)); - try std.testing.expectEqual(@as(?u8, '3'), pinnedSlotBadgeDigit(2)); - try std.testing.expectEqual(@as(?u8, null), pinnedSlotBadgeDigit(null)); - try std.testing.expectEqual(@as(?u8, null), pinnedSlotBadgeDigit(9)); + // Light mode + const idle_light = quickSlotChipColors(.git_bash, false, false); + try std.testing.expectEqual(rgb(252, 244, 228), idle_light.bg); + try std.testing.expectEqual(rgb(168, 120, 24), idle_light.border); + try std.testing.expectEqual(rgb(120, 80, 4), idle_light.fg); } -test "win32 quickSlotProfileIndex skips the selected profile and preserves order" { +test "win32 pinnedChipMarkerColor follows profile accent" { if (builtin.os.tag != .windows) return error.SkipZigTest; - try std.testing.expectEqual(@as(?usize, 1), quickSlotProfileIndex(5, 0, 0, 3)); - try std.testing.expectEqual(@as(?usize, 2), quickSlotProfileIndex(5, 0, 1, 3)); - try std.testing.expectEqual(@as(?usize, 3), quickSlotProfileIndex(5, 0, 2, 3)); - try std.testing.expectEqual(@as(?usize, 0), quickSlotProfileIndex(5, 3, 0, 3)); - try std.testing.expectEqual(@as(?usize, 1), quickSlotProfileIndex(5, 3, 1, 3)); - try std.testing.expectEqual(@as(?usize, 2), quickSlotProfileIndex(5, 3, 2, 3)); - try std.testing.expectEqual(@as(?usize, null), quickSlotProfileIndex(1, 0, 0, 3)); + // Dark mode + try std.testing.expectEqual(rgb(248, 202, 134), pinnedChipMarkerColor(.git_bash, true, false)); + try std.testing.expectEqual(rgb(255, 224, 178), pinnedChipMarkerColor(.git_bash, true, true)); + try std.testing.expectEqual(rgb(136, 216, 242), pinnedChipMarkerColor(.pwsh, true, false)); + + // Light mode + try std.testing.expectEqual(rgb(140, 96, 8), pinnedChipMarkerColor(.git_bash, false, false)); + try std.testing.expectEqual(rgb(120, 80, 4), pinnedChipMarkerColor(.git_bash, false, true)); } test "win32 nextQuickSlotFocus cycles visible painted quick slots" { @@ -34911,14 +28776,6 @@ test "win32 applyQuickSlotPreferenceOrder promotes pinned launcher profiles" { try std.testing.expectEqualStrings("wsl:Ubuntu", profiles[3].key); } -test "win32 findLauncherQuickSlotOrdinal finds runtime-pinned slots" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(?usize, 0), findLauncherQuickSlotOrdinal(.{ "git-bash", "cmd.exe", null }, "git-bash")); - try std.testing.expectEqual(@as(?usize, 1), findLauncherQuickSlotOrdinal(.{ "git-bash", "cmd.exe", null }, "CMD.EXE")); - try std.testing.expectEqual(@as(?usize, null), findLauncherQuickSlotOrdinal(.{ "git-bash", "cmd.exe", null }, "pwsh.exe")); -} - test "win32 launcherQuickSlotAssignmentNeedsChange skips no-op pin replays" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -34963,56 +28820,6 @@ test "win32 selectedProfileSelectionNeedsChange skips identical selection state" )); } -test "win32 buildProfileChromeBadgeText adds profile glyph treatment" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const pwsh = try buildProfileChromeBadgeText(std.testing.allocator, .pwsh); - defer std.testing.allocator.free(pwsh); - try std.testing.expectEqualStrings("PWSH >>", pwsh); - - const wsl = try buildProfileChromeBadgeText(std.testing.allocator, .wsl_distro); - defer std.testing.allocator.free(wsl); - try std.testing.expectEqualStrings("WSL <>", wsl); -} - -test "win32 profileKindDetail exposes shell integration posture" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const pwsh = profileKindDetail(.pwsh); - try std.testing.expectEqualStrings( - "PowerShell profile with automatic shell integration", - pwsh.summary, - ); - try std.testing.expectEqual(@as(?[]const u8, null), pwsh.next_step); - - const wsl = profileKindDetail(.wsl_distro); - try std.testing.expectEqualStrings( - "WSL distro profile; shell integration depends on the Linux shell", - wsl.summary, - ); - try std.testing.expectEqualStrings( - "Enable shell integration inside the selected WSL shell startup.", - wsl.next_step.?, - ); - - const git_bash = profileKindDetail(.git_bash); - try std.testing.expectEqualStrings( - "Git Bash profile with automatic shell integration", - git_bash.summary, - ); - try std.testing.expectEqual(@as(?[]const u8, null), git_bash.next_step); - - const cmd = profileKindDetail(.cmd); - try std.testing.expectEqualStrings( - "Command Prompt profile; shell integration unavailable", - cmd.summary, - ); - try std.testing.expectEqualStrings( - "Use PowerShell or WSL when prompt marking or cwd inheritance is required.", - cmd.next_step.?, - ); -} - test "win32 startupProfilePickerEnabled parses launcher env values" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -35025,94 +28832,6 @@ test "win32 startupProfilePickerEnabled parses launcher env values" { try std.testing.expect(!startupProfilePickerEnabled("no")); } -test "win32 buildSearchOverlayLabel reflects match counts" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const active = try buildSearchOverlayLabel(std.testing.allocator, 8, 2); - defer std.testing.allocator.free(active); - try std.testing.expectEqualStrings("Find 2/8", active); - - const passive = try buildSearchOverlayLabel(std.testing.allocator, 5, null); - defer std.testing.allocator.free(passive); - try std.testing.expectEqualStrings("Find 5", passive); - - const idle = try buildSearchOverlayLabel(std.testing.allocator, null, null); - defer std.testing.allocator.free(idle); - try std.testing.expectEqualStrings("Find", idle); -} - -test "win32 buildSearchBarResultsText reflects docked search states" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var bar = win32_search_bar.SearchBar.init(std.testing.allocator); - defer bar.deinit(); - - const idle = try buildSearchBarResultsText(std.testing.allocator, &bar); - defer std.testing.allocator.free(idle); - try std.testing.expectEqualStrings(search_results_idle, idle); - - try bar.setQuery("foo", 10); - const pending = try buildSearchBarResultsText(std.testing.allocator, &bar); - defer std.testing.allocator.free(pending); - try std.testing.expectEqualStrings(search_results_pending, pending); - - bar.searched = true; - bar.total = 0; - const none = try buildSearchBarResultsText(std.testing.allocator, &bar); - defer std.testing.allocator.free(none); - try std.testing.expectEqualStrings(search_results_none, none); - - bar.total = 12; - bar.selected = 3; - const active = try buildSearchBarResultsText(std.testing.allocator, &bar); - defer std.testing.allocator.free(active); - try std.testing.expectEqualStrings("3/12", active); -} - -test "win32 showSearchBarResults only shows status chip for non-empty queries" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var bar = win32_search_bar.SearchBar.init(std.testing.allocator); - defer bar.deinit(); - - try std.testing.expect(!showSearchBarResults(&bar)); - try bar.setQuery("search-open", 10); - try std.testing.expect(showSearchBarResults(&bar)); -} - -test "win32 searchBarNeedsRelayoutForQueryChange only trips on empty-state transitions" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!searchBarNeedsRelayoutForQueryChange(0, 0)); - try std.testing.expect(searchBarNeedsRelayoutForQueryChange(0, 1)); - try std.testing.expect(!searchBarNeedsRelayoutForQueryChange(4, 9)); - try std.testing.expect(searchBarNeedsRelayoutForQueryChange(3, 0)); -} - -test "win32 searchBarShouldInvalidateCoreSearchOnEdit invalidates active query edits" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!searchBarShouldInvalidateCoreSearchOnEdit(0, 0)); - try std.testing.expect(!searchBarShouldInvalidateCoreSearchOnEdit(0, 1)); - try std.testing.expect(searchBarShouldInvalidateCoreSearchOnEdit(5, 2)); - try std.testing.expect(searchBarShouldInvalidateCoreSearchOnEdit(5, 0)); -} - -test "win32 shouldAcceptCoreSearchUpdates rejects pending docked search state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var bar = win32_search_bar.SearchBar.init(std.testing.allocator); - defer bar.deinit(); - - try std.testing.expect(shouldAcceptCoreSearchUpdates(&bar)); - - try bar.setQuery("search-open", 10); - try std.testing.expect(!shouldAcceptCoreSearchUpdates(&bar)); - - bar.searched = true; - try std.testing.expect(shouldAcceptCoreSearchUpdates(&bar)); -} - test "win32 surfaceLayoutRuntimeSync only trips on visibility or pane rect changes" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -35508,22 +29227,6 @@ test "win32 surfaceFocusedActivationNeedsHostSync requires actual surface focus" try std.testing.expect(surfaceFocusedActivationNeedsHostSync(false, true, 2, 2, true, false)); } -test "win32 profileChromeNeedsFullTextInvalidation only trips for status-only profile chrome" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!profileChromeNeedsFullTextInvalidation(.profile, 0)); - try std.testing.expect(!profileChromeNeedsFullTextInvalidation(.profile, 24)); - try std.testing.expect(!profileChromeNeedsFullTextInvalidation(.none, 0)); - try std.testing.expect(profileChromeNeedsFullTextInvalidation(.none, 24)); -} - -test "win32 chromeTextNeedsFullInvalidation only trips when status bar is visible" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!chromeTextNeedsFullInvalidation(0)); - try std.testing.expect(chromeTextNeedsFullInvalidation(24)); -} - test "win32 optionalOwnedStringEquals handles present and null values" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -35576,33 +29279,6 @@ test "win32 scrollbarPaintKeyChanged only trips on visible scrollbar paint input try std.testing.expect(scrollbarPaintKeyChanged(base, .{ .rect = base.rect, .total = 2000, .len = 21, .offset = 1979, .dpi = 144, .hovered = false, .dragging = false, .marker_revision = 4 })); } -test "win32 searchBarResultsVisual marks no-match state with error colors" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - var bar = win32_search_bar.SearchBar.init(std.testing.allocator); - defer bar.deinit(); - - try bar.setQuery("zzzzz", 10); - bar.searched = true; - bar.total = 0; - - const theme = darkTheme(); - const visual = searchBarResultsVisual(&theme, &bar); - try std.testing.expectEqual(theme.error_fg, visual.border); - try std.testing.expectEqual(theme.error_fg, visual.fg); -} - -test "win32 searchBarButtonShowsLabel keeps docked search buttons icon-only" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!searchBarButtonShowsLabel(.prev, 80)); - try std.testing.expect(!searchBarButtonShowsLabel(.close, 80)); - try std.testing.expect(!searchBarButtonShowsLabel(.regex, 40)); - try std.testing.expect(!searchBarButtonShowsLabel(.regex, 64)); - try std.testing.expect(!searchBarButtonShowsLabel(.case_sensitive, 72)); - try std.testing.expect(!searchBarButtonShowsLabel(.whole_word, 80)); -} - test "win32 wmCommandChildHwnd handles null lParam" { try std.testing.expectEqual(@as(?HWND, null), wmCommandChildHwnd(0)); @@ -35610,197 +29286,6 @@ test "win32 wmCommandChildHwnd handles null lParam" { try std.testing.expectEqual(@as(usize, 0x1234), @intFromPtr(child)); } -test "win32 buildTabOverviewOverlayLabel reflects current host tab" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const multi = try buildTabOverviewOverlayLabel(std.testing.allocator, 1, 4); - defer std.testing.allocator.free(multi); - try std.testing.expectEqualStrings("Tab 2/4", multi); - - const single = try buildTabOverviewOverlayLabel(std.testing.allocator, 0, 1); - defer std.testing.allocator.free(single); - try std.testing.expectEqualStrings("Tab", single); -} - -test "win32 buildOverlayPaintLabelText reflects live overlay mode" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const command = try buildOverlayPaintLabelText( - std.testing.allocator, - .command_palette, - "toggle_", - null, - null, - .{}, - .{ .match_count = 4, .title = "Toggle fullscreen", .subtitle = "Fullscreen", .available = true }, - ); - defer std.testing.allocator.free(command); - try std.testing.expectEqualStrings("Command 4", command); - - const search = try buildOverlayPaintLabelText( - std.testing.allocator, - .search, - "", - 8, - 2, - .{}, - .{}, - ); - defer std.testing.allocator.free(search); - try std.testing.expectEqualStrings("Find 2/8", search); - - const title = try buildOverlayPaintLabelText( - std.testing.allocator, - .surface_title, - "", - null, - null, - .{}, - .{}, - ); - defer std.testing.allocator.free(title); - try std.testing.expectEqualStrings("Window title", title); -} - -test "win32 buildOverlayFeedbackText prefers inline banner state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const snap = PaletteSnapshot.fromDefaults(); - const empty_mru: []const []const u8 = &.{}; - - const info = try buildOverlayFeedbackText( - std.testing.allocator, - .info, - "Try: new_tab", - .command_palette, - "", - null, - null, - null, - .{}, - 1, - snap, - empty_mru, - .{}, - ); - defer std.testing.allocator.free(info); - try std.testing.expectEqualStrings("Info: Try: new_tab", info); - - const err = try buildOverlayFeedbackText( - std.testing.allocator, - .err, - "Unknown Ghostty action", - .command_palette, - "", - null, - null, - null, - .{}, - 1, - snap, - empty_mru, - .{}, - ); - defer std.testing.allocator.free(err); - try std.testing.expectEqualStrings("Error: Unknown Ghostty action", err); - - const fallback = try buildOverlayFeedbackText( - std.testing.allocator, - .none, - null, - .search, - "needle", - "needle", - 8, - 2, - .{}, - 1, - snap, - empty_mru, - .{}, - ); - defer std.testing.allocator.free(fallback); - try std.testing.expect(std.mem.indexOf(u8, fallback, "next match") != null); -} - -test "win32 nextTabOverviewSelection wraps and clamps" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(usize, 2), nextTabOverviewSelection(1, 4, false)); - try std.testing.expectEqual(@as(usize, 4), nextTabOverviewSelection(1, 4, true)); - try std.testing.expectEqual(@as(usize, 1), nextTabOverviewSelection(4, 4, false)); - try std.testing.expectEqual(@as(usize, 1), nextTabOverviewSelection(9, 4, false)); -} - -test "win32 tabDirectionFromWheelDelta maps wheel direction to tab navigation" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(apprt.action.GotoTab.previous, tabDirectionFromWheelDelta(120)); - try std.testing.expectEqual(apprt.action.GotoTab.next, tabDirectionFromWheelDelta(-120)); -} - -test "win32 searchDirectionFromWheelDelta maps wheel direction to search navigation" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(input.Binding.Action.NavigateSearch.next, searchDirectionFromWheelDelta(120)); - try std.testing.expectEqual(input.Binding.Action.NavigateSearch.previous, searchDirectionFromWheelDelta(-120)); -} - -test "win32 searchBarSearchedState helpers preserve pending searches on null callbacks" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!searchBarSearchedStateForTotal(null)); - try std.testing.expect(searchBarSearchedStateForTotal(0)); - - try std.testing.expect(!searchBarSearchedStateForSelected(null, null)); - try std.testing.expect(searchBarSearchedStateForSelected(null, 5)); - try std.testing.expect(searchBarSearchedStateForSelected(2, null)); -} - -test "win32 searchBarDisplayStateChanged only trips on visible results changes" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const same = win32_search_bar.SearchBar{ - .searched = true, - .total = 12, - .selected = 3, - .alloc = std.testing.allocator, - }; - try std.testing.expect(!searchBarDisplayStateChanged(&same, true, 12, 3)); - try std.testing.expect(searchBarDisplayStateChanged(&same, false, 12, 3)); - try std.testing.expect(searchBarDisplayStateChanged(&same, true, 11, 3)); - try std.testing.expect(searchBarDisplayStateChanged(&same, true, 12, 4)); -} - -test "win32 scrollStatusTextChanged only trips on indicator visibility or percent deltas" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const hidden_bottom: terminal.Scrollbar = .{ .total = 2000, .len = 21, .offset = 1979 }; - const hidden_short: terminal.Scrollbar = .{ .total = 21, .len = 21, .offset = 0 }; - const visible_50: terminal.Scrollbar = .{ .total = 2000, .len = 21, .offset = 1000 }; - const visible_50_neighbor: terminal.Scrollbar = .{ .total = 2000, .len = 21, .offset = 1001 }; - const visible_51: terminal.Scrollbar = .{ .total = 2000, .len = 21, .offset = 1020 }; - - try std.testing.expect(!scrollStatusTextChanged(hidden_short, hidden_bottom)); - try std.testing.expect(scrollStatusTextChanged(hidden_bottom, visible_50)); - try std.testing.expect(!scrollStatusTextChanged(visible_50, visible_50_neighbor)); - try std.testing.expect(scrollStatusTextChanged(visible_50, visible_51)); -} - -test "win32 commandPaletteDirectionFromWheelDelta maps wheel direction to completion direction" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(commandPaletteDirectionFromWheelDelta(120)); - try std.testing.expect(!commandPaletteDirectionFromWheelDelta(-120)); -} - -test "win32 profileDirectionFromWheelDelta maps wheel direction to profile navigation" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(profileDirectionFromWheelDelta(120)); - try std.testing.expect(!profileDirectionFromWheelDelta(-120)); -} - test "win32 overlayEditBorderColor reflects mode and focus" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -35967,157 +29452,19 @@ test "win32 profileChromeStripeColor tracks profile interaction state" { } test "win32 profileKind label and hint colors follow profile accent" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - // Dark mode - try std.testing.expectEqual(rgb(186, 232, 248), profileKindLabelColor(.pwsh, true)); - try std.testing.expectEqual(rgb(136, 216, 242), profileKindHintColor(.pwsh, true)); - try std.testing.expectEqual(rgb(255, 224, 178), profileKindLabelColor(.git_bash, true)); - try std.testing.expectEqual(rgb(248, 202, 134), profileKindHintColor(.git_bash, true)); - - // Light mode - try std.testing.expectEqual(rgb(8, 80, 108), profileKindLabelColor(.pwsh, false)); - try std.testing.expectEqual(rgb(12, 96, 126), profileKindHintColor(.pwsh, false)); - try std.testing.expectEqual(rgb(120, 80, 4), profileKindLabelColor(.git_bash, false)); - try std.testing.expectEqual(rgb(140, 96, 8), profileKindHintColor(.git_bash, false)); -} - -test "win32 tabButtonKeyAction maps focused-tab keys" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(TabButtonKeyAction.previous, tabButtonKeyAction(VK_LEFT, false).?); - try std.testing.expectEqual(TabButtonKeyAction.move_previous, tabButtonKeyAction(VK_LEFT, true).?); - try std.testing.expectEqual(TabButtonKeyAction.next, tabButtonKeyAction(VK_RIGHT, false).?); - try std.testing.expectEqual(TabButtonKeyAction.move_next, tabButtonKeyAction(VK_RIGHT, true).?); - try std.testing.expectEqual(TabButtonKeyAction.first, tabButtonKeyAction(VK_HOME, false).?); - try std.testing.expectEqual(TabButtonKeyAction.move_first, tabButtonKeyAction(VK_HOME, true).?); - try std.testing.expectEqual(TabButtonKeyAction.last, tabButtonKeyAction(VK_END, false).?); - try std.testing.expectEqual(TabButtonKeyAction.move_last, tabButtonKeyAction(VK_END, true).?); - try std.testing.expectEqual(TabButtonKeyAction.rename, tabButtonKeyAction(VK_F2, false).?); - try std.testing.expectEqual(TabButtonKeyAction.close, tabButtonKeyAction(VK_DELETE, false).?); - try std.testing.expectEqual(TabButtonKeyAction.overview, tabButtonKeyAction(VK_APPS, false).?); - try std.testing.expect(tabButtonKeyAction(VK_RETURN, false) == null); -} - -test "win32 moveTabAmountToEdge computes direct host reorder delta" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(isize, -2), moveTabAmountToEdge(4, 2, true)); - try std.testing.expectEqual(@as(isize, 1), moveTabAmountToEdge(4, 2, false)); - try std.testing.expectEqual(@as(isize, 0), moveTabAmountToEdge(4, 0, true)); - try std.testing.expectEqual(@as(isize, 0), moveTabAmountToEdge(4, 3, false)); -} - -test "win32 searchButtonKeyAction maps focused search button keys" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(SearchButtonKeyAction.next, searchButtonKeyAction(VK_F3, false).?); - try std.testing.expectEqual(SearchButtonKeyAction.previous, searchButtonKeyAction(VK_F3, true).?); - try std.testing.expectEqual(SearchButtonKeyAction.dismiss, searchButtonKeyAction(VK_ESCAPE, false).?); - try std.testing.expect(searchButtonKeyAction(VK_RETURN, false) == null); -} - -test "win32 docked search key actions preserve semantic next and previous navigation" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual( - input.Binding.Action.NavigateSearch.next, - dockedSearchCoreDirectionFromKeyAction(.next).?, - ); - try std.testing.expectEqual( - input.Binding.Action.NavigateSearch.previous, - dockedSearchCoreDirectionFromKeyAction(.previous).?, - ); - try std.testing.expect(dockedSearchCoreDirectionFromKeyAction(.dismiss) == null); -} - -test "win32 docked search arrow buttons follow visible up and down navigation" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual( - input.Binding.Action.NavigateSearch.next, - dockedSearchButtonDirection(SEARCH_PREV_ID).?, - ); - try std.testing.expectEqual( - input.Binding.Action.NavigateSearch.previous, - dockedSearchButtonDirection(SEARCH_NEXT_ID).?, - ); - try std.testing.expect(dockedSearchButtonDirection(0) == null); -} - -test "win32 docked search Enter and Shift+Enter follow the arrow-key contract" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual( - dockedSearchArrowDirection(VK_DOWN).?, - dockedSearchEnterDirection(false), - ); - try std.testing.expectEqual( - dockedSearchArrowDirection(VK_UP).?, - dockedSearchEnterDirection(true), - ); -} - -test "win32 docked search Up and Down keys follow the visible button contract" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual( - input.Binding.Action.NavigateSearch.next, - dockedSearchArrowDirection(VK_UP).?, - ); - try std.testing.expectEqual( - input.Binding.Action.NavigateSearch.previous, - dockedSearchArrowDirection(VK_DOWN).?, - ); - try std.testing.expect(dockedSearchArrowDirection(VK_LEFT) == null); -} - -test "win32 docked search selection preserves newest-first visible numbering" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(?usize, 0), searchSelectedRawFromCoreDisplay(1)); - try std.testing.expectEqual(@as(?usize, 7), searchSelectedRawFromCoreDisplay(8)); - try std.testing.expectEqual(@as(?usize, 1), searchSelectedDisplayFromRaw(0, 8)); - try std.testing.expectEqual(@as(?usize, 8), searchSelectedDisplayFromRaw(7, 8)); -} - -test "win32 docked search preview advances visible numbering with navigation" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(@as(?usize, 6), advanceSearchSelectedRaw(7, 8, .previous, true)); - try std.testing.expectEqual(@as(?usize, 0), advanceSearchSelectedRaw(7, 8, .next, true)); - try std.testing.expectEqual(@as(?usize, 7), searchSelectedDisplayFromRaw( - advanceSearchSelectedRaw(7, 8, .previous, true), - 8, - )); - try std.testing.expectEqual(@as(?usize, 1), searchSelectedDisplayFromRaw( - advanceSearchSelectedRaw(7, 8, .next, true), - 8, - )); -} - -test "win32 docked search sequential visible order increments while moving older and up" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const total: ?usize = 2500; - var raw: ?usize = null; - - try std.testing.expectEqual(@as(?usize, null), searchSelectedDisplayFromRaw(raw, total)); - - raw = searchSelectedRawFromCoreDisplay(1); - try std.testing.expectEqual(@as(?usize, 1), searchSelectedDisplayFromRaw(raw, total)); - - raw = advanceSearchSelectedRaw(raw, total, .next, true); - try std.testing.expectEqual(@as(?usize, 2), searchSelectedDisplayFromRaw(raw, total)); - - raw = advanceSearchSelectedRaw(raw, total, .next, true); - try std.testing.expectEqual(@as(?usize, 3), searchSelectedDisplayFromRaw(raw, total)); + if (builtin.os.tag != .windows) return error.SkipZigTest; - raw = advanceSearchSelectedRaw(raw, total, .previous, true); - try std.testing.expectEqual(@as(?usize, 2), searchSelectedDisplayFromRaw(raw, total)); + // Dark mode + try std.testing.expectEqual(rgb(186, 232, 248), profileKindLabelColor(.pwsh, true)); + try std.testing.expectEqual(rgb(136, 216, 242), profileKindHintColor(.pwsh, true)); + try std.testing.expectEqual(rgb(255, 224, 178), profileKindLabelColor(.git_bash, true)); + try std.testing.expectEqual(rgb(248, 202, 134), profileKindHintColor(.git_bash, true)); - raw = advanceSearchSelectedRaw(raw, total, .previous, true); - try std.testing.expectEqual(@as(?usize, 1), searchSelectedDisplayFromRaw(raw, total)); + // Light mode + try std.testing.expectEqual(rgb(8, 80, 108), profileKindLabelColor(.pwsh, false)); + try std.testing.expectEqual(rgb(12, 96, 126), profileKindHintColor(.pwsh, false)); + try std.testing.expectEqual(rgb(120, 80, 4), profileKindLabelColor(.git_bash, false)); + try std.testing.expectEqual(rgb(140, 96, 8), profileKindHintColor(.git_bash, false)); } test "win32 searchBarGroupRect ignores hidden placements" { @@ -36176,107 +29523,6 @@ test "win32 scrollbarProxyPointFromSurfaceCursor tracks the right-edge hover ban try std.testing.expect(!outside.over_band); } -test "win32 tabsButtonKeyAction maps focused tabs button keys" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(TabsButtonKeyAction.previous, tabsButtonKeyAction(VK_LEFT).?); - try std.testing.expectEqual(TabsButtonKeyAction.previous, tabsButtonKeyAction(VK_UP).?); - try std.testing.expectEqual(TabsButtonKeyAction.next, tabsButtonKeyAction(VK_RIGHT).?); - try std.testing.expectEqual(TabsButtonKeyAction.next, tabsButtonKeyAction(VK_DOWN).?); - try std.testing.expectEqual(TabsButtonKeyAction.rename, tabsButtonKeyAction(VK_F2).?); - try std.testing.expectEqual(TabsButtonKeyAction.overview, tabsButtonKeyAction(VK_APPS).?); - try std.testing.expect(tabsButtonKeyAction(VK_RETURN) == null); -} - -test "win32 commandButtonKeyAction maps focused command button keys" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(CommandButtonKeyAction.toggle, commandButtonKeyAction(VK_RETURN).?); - try std.testing.expectEqual(CommandButtonKeyAction.toggle, commandButtonKeyAction(VK_SPACE).?); - try std.testing.expectEqual(CommandButtonKeyAction.previous, commandButtonKeyAction(VK_UP).?); - try std.testing.expectEqual(CommandButtonKeyAction.next, commandButtonKeyAction(VK_DOWN).?); - try std.testing.expectEqual(CommandButtonKeyAction.dismiss, commandButtonKeyAction(VK_ESCAPE).?); - try std.testing.expect(commandButtonKeyAction(VK_F2) == null); -} - -test "win32 command palette toggle binding is recognized inside action chains" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const absent = [_]input.Binding.Action{ .{ .copy_to_clipboard = .mixed }, .paste_from_clipboard }; - const present = [_]input.Binding.Action{ .{ .copy_to_clipboard = .mixed }, .toggle_command_palette }; - try std.testing.expect(!bindingActionsToggleCommandPalette(&absent)); - try std.testing.expect(bindingActionsToggleCommandPalette(&present)); -} - -test "win32 buildInspectorBannerText reflects host inspector context" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const multi = try buildInspectorBannerText(std.testing.allocator, .{ .index = 1, .total = 4 }, 3, false); - defer std.testing.allocator.free(multi); - try std.testing.expectEqualStrings("Inspector active | tab 2/4 | panes 3 | toggle Inspect to return", multi); - - const zoomed = try buildInspectorBannerText(std.testing.allocator, .{ .index = 0, .total = 2 }, 2, true); - defer std.testing.allocator.free(zoomed); - try std.testing.expectEqualStrings("Inspector active | tab 1/2 | panes 2 | zoomed | toggle Inspect to return", zoomed); - - const single = try buildInspectorBannerText(std.testing.allocator, .{ .index = 0, .total = 1 }, 1, false); - defer std.testing.allocator.free(single); - try std.testing.expectEqualStrings("Inspector active | tab 1/1 | toggle Inspect to return", single); -} - -test "win32 buildHostBannerText prefixes info and error banners" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const plain = try buildHostBannerText(std.testing.allocator, .none, "ready"); - defer std.testing.allocator.free(plain); - try std.testing.expectEqualStrings("ready", plain); - - const info = try buildHostBannerText(std.testing.allocator, .info, "ready"); - defer std.testing.allocator.free(info); - try std.testing.expectEqualStrings("Info: ready", info); - - const err = try buildHostBannerText(std.testing.allocator, .err, "ready"); - defer std.testing.allocator.free(err); - try std.testing.expectEqualStrings("Error: ready", err); -} - -test "win32 overlayPaintCacheDirty ignores repaint-only dirtiness" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const cached = std.unicode.utf8ToUtf16LeStringLiteral("ok"); - - try std.testing.expect(!overlayPaintCacheDirty(false, .search, cached, cached, null)); - try std.testing.expect(!overlayPaintCacheDirty(false, .profile, cached, cached, cached)); - - try std.testing.expect(overlayPaintCacheDirty(true, .search, cached, cached, null)); - try std.testing.expect(overlayPaintCacheDirty(false, .search, null, cached, null)); - try std.testing.expect(overlayPaintCacheDirty(false, .profile, cached, cached, null)); -} - -test "win32 profileChromeVisible only trips for profile overlay or visible status bar" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(profileChromeVisible(.profile, 0)); - try std.testing.expect(profileChromeVisible(.none, 24)); - try std.testing.expect(!profileChromeVisible(.none, 0)); - try std.testing.expect(!profileChromeVisible(.search, 0)); -} - -test "win32 inspector chrome visibility only trips for banner or visible status bar" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(inspectorChromeVisible(.none, 0)); - try std.testing.expect(inspectorChromeVisible(.search, 24)); - try std.testing.expect(!inspectorChromeVisible(.search, 0)); -} - -test "win32 inspector visibility changes require host relayout" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(inspectorVisibilityChangeNeedsHostRelayout(true)); - try std.testing.expect(!inspectorVisibilityChangeNeedsHostRelayout(false)); -} - test "win32 surface focus state updates focused split handle" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -36287,79 +29533,6 @@ test "win32 surface focus state updates focused split handle" { try std.testing.expect(surfaceFocusStateChanged(0, root, 1, root)); } -test "win32 inspector panel visibility is tab scoped" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(inspectorPanelVisibleForState(.none, true)); - try std.testing.expect(!inspectorPanelVisibleForState(.none, false)); - try std.testing.expect(!inspectorPanelVisibleForState(.command_palette, true)); -} - -test "win32 overlay edit child rect preserves frame border" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const frame = RECT{ .left = 100, .top = 20, .right = 300, .bottom = 58 }; - const child = overlayEditChildRectFromFrame(frame, 8, 6); - try std.testing.expectEqual(@as(i32, 108), child.left); - try std.testing.expectEqual(@as(i32, 26), child.top); - try std.testing.expectEqual(@as(i32, 292), child.right); - try std.testing.expectEqual(@as(i32, 52), child.bottom); - try std.testing.expect(child.bottom < frame.bottom); -} - -test "win32 overlay edit frame offsets scale with DPI" { - const dpis = [_]u32{ 96, 192, 288 }; - for (dpis, 1..) |dpi, scale| { - const scale_i32: i32 = @intCast(scale); - const padding = 10 * scale_i32; - const label_w = 100 * scale_i32; - const cancel_w = 80 * scale_i32; - const row_h = 30 * scale_i32; - const overlay_y = 20 * scale_i32; - const frame = overlayEditFrameRect( - 800 * scale_i32, - overlay_y, - padding, - label_w, - cancel_w, - 0, - row_h, - dpi, - ); - try std.testing.expectEqual(overlay_y + 4 * scale_i32, frame.top); - try std.testing.expectEqual(frame.top + row_h, frame.bottom); - try std.testing.expectEqual( - 800 * scale_i32 - cancel_w - padding * 2 - 6 * scale_i32, - frame.right, - ); - } - - const narrow = overlayEditFrameRect(220, 20, 10, 100, 80, 0, 30, 96); - const close_left = 220 - 80 - 10; - try std.testing.expectEqual(@as(i32, 10), narrow.left); - try std.testing.expect(narrow.right <= close_left); - try std.testing.expect(narrow.right >= narrow.left); - - for ([_]i32{ 80, 100, 120, 140 }) |width| { - const visibility = overlayActionVisibilityForWidth(width, 10, 80, 80, false, 96); - const effective_cancel: i32 = if (visibility.cancel) 80 else 0; - const frame = overlayEditFrameRect(width, 20, 10, 100, effective_cancel, 0, 30, 96); - const child = overlayEditChildRectFromFrame(frame, 8, 6); - try std.testing.expect(child.left >= frame.left); - try std.testing.expect(child.right <= frame.right); - try std.testing.expect(child.left <= child.right); - if (visibility.cancel) try std.testing.expect(child.right <= width - 80 - 10); - } - for ([_]i32{ 1, 10, 20, 30 }) |width| { - const frame = overlayEditFrameRect(width, 20, 10, 100, 0, 0, 30, 96); - try std.testing.expect(frame.left >= 0); - try std.testing.expect(frame.right <= width); - try std.testing.expect(frame.right > frame.left); - } - try std.testing.expect(!overlayActionVisibilityForWidth(220, 10, 80, 80, true, 96).accept); - try std.testing.expect(overlayActionVisibilityForWidth(230, 10, 80, 80, true, 96).accept); -} - test "win32 confirm overlay keeps a bounded visible action at narrow widths" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -36401,320 +29574,16 @@ test "win32 confirm overlay keeps a bounded visible action at narrow widths" { try std.testing.expect(wide.cancel_x + wide.cancel_width <= 230); } -test "win32 GDI text length accepts empty sentinel slices" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const alloc = std.testing.allocator; - const empty = try std.unicode.utf8ToUtf16LeAllocZ(alloc, ""); - defer alloc.free(empty); - try std.testing.expectEqual(@as(i32, 0), utf16GdiTextLen(empty)); - - const confirm = try std.unicode.utf8ToUtf16LeAllocZ(alloc, "Confirm"); - defer alloc.free(confirm); - try std.testing.expectEqual(@as(i32, 7), utf16GdiTextLen(confirm)); -} - -test "win32 command palette hides duplicate accept button" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!overlayAcceptButtonVisible(.command_palette)); - try std.testing.expect(overlayAcceptButtonVisible(.profile)); - try std.testing.expect(overlayAcceptButtonVisible(.search)); - try std.testing.expect(overlayAcceptButtonVisible(.confirm)); - try std.testing.expect(!overlayEditFrameVisible(.confirm)); - try std.testing.expect(overlayEditFrameVisible(.command_palette)); - try std.testing.expect(overlayEditFrameVisible(.profile)); -} - -test "win32 transient overlay focus ring includes every visible control" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqual(OverlayFocusSlot.accept, nextOverlayFocusSlot(.profile, .edit, false)); - try std.testing.expectEqual(OverlayFocusSlot.cancel, nextOverlayFocusSlot(.profile, .accept, false)); - try std.testing.expectEqual(OverlayFocusSlot.edit, nextOverlayFocusSlot(.profile, .cancel, false)); - try std.testing.expectEqual(OverlayFocusSlot.cancel, nextOverlayFocusSlot(.profile, .edit, true)); - try std.testing.expectEqual(OverlayFocusSlot.edit, nextOverlayFocusSlot(.profile, .accept, true)); - try std.testing.expectEqual(OverlayFocusSlot.accept, nextOverlayFocusSlot(.profile, .cancel, true)); - - try std.testing.expectEqual(OverlayFocusSlot.cancel, nextOverlayFocusSlot(.command_palette, .edit, false)); - try std.testing.expectEqual(OverlayFocusSlot.edit, nextOverlayFocusSlot(.command_palette, .cancel, true)); - try std.testing.expectEqual(OverlayFocusSlot.cancel, nextOverlayFocusSlot(.confirm, .accept, false)); - try std.testing.expectEqual(OverlayFocusSlot.accept, nextOverlayFocusSlot(.confirm, .cancel, true)); - - try std.testing.expectEqual( - OverlayFocusSlot.cancel, - nextVisibleOverlayFocusSlot(.profile, .edit, false, true, false, true), - ); - try std.testing.expectEqual( - OverlayFocusSlot.cancel, - nextVisibleOverlayFocusSlot(.confirm, .accept, false, false, false, true), - ); - try std.testing.expectEqual( - OverlayFocusSlot.accept, - nextVisibleOverlayFocusSlot(.confirm, .cancel, true, false, true, true), - ); - try std.testing.expectEqual( - null, - nextVisibleOverlayFocusSlot(.command_palette, .edit, false, true, false, false), - ); - try std.testing.expectEqual( - null, - nextVisibleOverlayFocusSlot(.confirm, .cancel, false, false, false, true), - ); -} - test "win32 overlay buttons activate only for their exact click notification" { if (builtin.os.tag != .windows) return error.SkipZigTest; const accept: HWND = @ptrFromInt(0x10); const cancel: HWND = @ptrFromInt(0x20); - try std.testing.expect(expectedButtonClick(BN_CLICKED, accept, accept)); - try std.testing.expect(!expectedButtonClick(BN_SETFOCUS, accept, accept)); - try std.testing.expect(!expectedButtonClick(BN_KILLFOCUS, accept, accept)); - try std.testing.expect(!expectedButtonClick(BN_CLICKED, cancel, accept)); - try std.testing.expect(!expectedButtonClick(BN_CLICKED, null, accept)); -} - -test "win32 inspectorBannerStateChanged only trips on actual banner deltas" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(!inspectorBannerStateChanged(.none, .none, null, true)); - try std.testing.expect(inspectorBannerStateChanged(.none, .none, null, false)); - try std.testing.expect(!inspectorBannerStateChanged(.none, .info, host_banner_inspector_inactive, false)); - try std.testing.expect(inspectorBannerStateChanged(.none, .err, "error", false)); - try std.testing.expect(!inspectorBannerStateChanged(.search, .none, null, true)); -} - -test "win32 windowTitleSyncChanged only trips on actual title deltas" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expect(windowTitleSyncChanged(null, "noctty")); - try std.testing.expect(!windowTitleSyncChanged("noctty", "noctty")); - try std.testing.expect(windowTitleSyncChanged("noctty", "noctty - 2")); -} - -test "win32 buildInspectorPanelTitleText reflects host inspector context" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const multi = try buildInspectorPanelTitleText(std.testing.allocator, .{ .index = 1, .total = 4 }, 3, false); - defer std.testing.allocator.free(multi); - try std.testing.expect(std.mem.indexOf(u8, multi, "Inspector") != null); - try std.testing.expect(std.mem.indexOf(u8, multi, "tab 2/4") != null); - try std.testing.expect(std.mem.indexOf(u8, multi, "3 panes") != null); - - const zoomed = try buildInspectorPanelTitleText(std.testing.allocator, .{ .index = 0, .total = 2 }, 2, true); - defer std.testing.allocator.free(zoomed); - try std.testing.expect(std.mem.indexOf(u8, zoomed, "zoomed") != null); -} - -test "win32 buildInspectorPanelHintText reflects live inspector scope" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const single = try buildInspectorPanelHintText(std.testing.allocator, 1, false); - defer std.testing.allocator.free(single); - try std.testing.expect(std.mem.indexOf(u8, single, "this tab") != null); - - const multi = try buildInspectorPanelHintText(std.testing.allocator, 3, false); - defer std.testing.allocator.free(multi); - try std.testing.expect(std.mem.indexOf(u8, multi, "3 panes") != null); - - const zoomed = try buildInspectorPanelHintText(std.testing.allocator, 2, true); - defer std.testing.allocator.free(zoomed); - try std.testing.expect(std.mem.indexOf(u8, zoomed, "zoomed pane") != null); -} - -test "win32 buildInspectorDetailText reflects pane and zoom context" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const multi = try buildInspectorDetailText(std.testing.allocator, .{ .index = 1, .total = 4 }, 3, false); - defer std.testing.allocator.free(multi); - try std.testing.expect(std.mem.indexOf(u8, multi, "tab 2/4") != null); - try std.testing.expect(std.mem.indexOf(u8, multi, "3 panes") != null); - - const zoomed = try buildInspectorDetailText(std.testing.allocator, .{ .index = 0, .total = 2 }, 2, true); - defer std.testing.allocator.free(zoomed); - try std.testing.expect(std.mem.indexOf(u8, zoomed, "zoom") != null); -} - -test "win32 buildSearchDetailText reflects live search context" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const active = try buildSearchDetailText(std.testing.allocator, "needle", 8, 2); - defer std.testing.allocator.free(active); - try std.testing.expect(std.mem.indexOf(u8, active, "needle") != null); - try std.testing.expect(std.mem.indexOf(u8, active, "2 of 8") != null); - - const pending = try buildSearchDetailText(std.testing.allocator, "logs", null, null); - defer std.testing.allocator.free(pending); - try std.testing.expect(std.mem.indexOf(u8, pending, "refine") != null); -} - -test "win32 buildOverlayAcceptLabel reflects overlay action state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const palette_idle = try buildOverlayAcceptLabel(std.testing.allocator, .command_palette, "", null, null, null, .{}); - defer std.testing.allocator.free(palette_idle); - try std.testing.expectEqualStrings("Close", palette_idle); - - const palette_run = try buildOverlayAcceptLabel( - std.testing.allocator, - .command_palette, - "new_tab", - null, - null, - null, - .{ .match_count = 1, .title = "New tab", .subtitle = "Action", .available = true }, - ); - defer std.testing.allocator.free(palette_run); - try std.testing.expectEqualStrings("Activate", palette_run); - - const palette_matches = try buildOverlayAcceptLabel( - std.testing.allocator, - .command_palette, - "0x96f", - null, - null, - null, - .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme", .available = true }, - ); - defer std.testing.allocator.free(palette_matches); - try std.testing.expectEqualStrings("Activate", palette_matches); - - const palette_hidden = try buildOverlayAcceptLabel( - std.testing.allocator, - .command_palette, - "0x96f", - null, - null, - null, - .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme" }, - ); - defer std.testing.allocator.free(palette_hidden); - try std.testing.expectEqualStrings("Resize", palette_hidden); - - const search_next = try buildOverlayAcceptLabel(std.testing.allocator, .search, "needle", "needle", 8, 2, .{}); - defer std.testing.allocator.free(search_next); - try std.testing.expectEqualStrings("Next", search_next); - - const search_find = try buildOverlayAcceptLabel(std.testing.allocator, .search, "other", "needle", 8, 2, .{}); - defer std.testing.allocator.free(search_find); - try std.testing.expectEqualStrings("Find", search_find); - - const tab_go = try buildOverlayAcceptLabel(std.testing.allocator, .tab_overview, "2", null, null, null, .{}); - defer std.testing.allocator.free(tab_go); - try std.testing.expectEqualStrings("Go", tab_go); - - const title_apply = try buildOverlayAcceptLabel(std.testing.allocator, .surface_title, "logs", null, null, null, .{}); - defer std.testing.allocator.free(title_apply); - try std.testing.expectEqualStrings("Apply", title_apply); -} - -test "win32 buildOverlayHintText reflects live overlay guidance" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const snap = PaletteSnapshot.fromDefaults(); - const empty_mru: []const []const u8 = &.{}; - - const command_unique = try buildOverlayHintText( - std.testing.allocator, - .command_palette, - "reload_", - null, - null, - null, - .{}, - 1, - snap, - empty_mru, - ); - defer std.testing.allocator.free(command_unique); - try std.testing.expect(std.mem.indexOf(u8, command_unique, "reload_config") != null); - - const search_next = try buildOverlayHintText( - std.testing.allocator, - .search, - "needle", - "needle", - 8, - 2, - .{}, - 1, - snap, - empty_mru, - ); - defer std.testing.allocator.free(search_next); - try std.testing.expect(std.mem.indexOf(u8, search_next, "2/8") != null); - try std.testing.expect(std.mem.indexOf(u8, search_next, "next match") != null); - - const tab_invalid = try buildOverlayHintText( - std.testing.allocator, - .tab_overview, - "8", - null, - null, - null, - .{ .index = 1, .total = 4 }, - 2, - snap, - empty_mru, - ); - defer std.testing.allocator.free(tab_invalid); - try std.testing.expect(std.mem.indexOf(u8, tab_invalid, "out of range") != null); - - const tab_title = try buildOverlayHintText( - std.testing.allocator, - .tab_title, - "logs", - null, - null, - null, - .{ .index = 0, .total = 3 }, - 2, - snap, - empty_mru, - ); - defer std.testing.allocator.free(tab_title); - try std.testing.expect(std.mem.indexOf(u8, tab_title, "tab 1/3") != null); - try std.testing.expect(std.mem.indexOf(u8, tab_title, "2 panes") != null); -} - -test "win32 overlayCancelLabel reflects overlay mode" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - try std.testing.expectEqualStrings("Close", overlayCancelLabel(.search)); - try std.testing.expectEqualStrings("Cancel", overlayCancelLabel(.surface_title)); -} - -test "win32 buildCommandButtonLabel reflects live palette state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const active = try buildCommandButtonLabel(std.testing.allocator, true, "toggle_fullscreen"); - defer std.testing.allocator.free(active); - try std.testing.expectEqualStrings("Cmd toggle...", active); - - const armed = try buildCommandButtonLabel(std.testing.allocator, true, ""); - defer std.testing.allocator.free(armed); - try std.testing.expectEqualStrings("[Cmd]", armed); - - const idle = try buildCommandButtonLabel(std.testing.allocator, false, null); - defer std.testing.allocator.free(idle); - try std.testing.expectEqualStrings("Cmd", idle); -} - -test "win32 buildInspectorButtonLabel reflects inspector and pane state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const visible = try buildInspectorButtonLabel(std.testing.allocator, true, 3); - defer std.testing.allocator.free(visible); - try std.testing.expectEqualStrings("[Inspect 3]", visible); - - const multi = try buildInspectorButtonLabel(std.testing.allocator, false, 2); - defer std.testing.allocator.free(multi); - try std.testing.expectEqualStrings("Inspect 2", multi); - - const idle = try buildInspectorButtonLabel(std.testing.allocator, false, 1); - defer std.testing.allocator.free(idle); - try std.testing.expectEqualStrings("Inspect", idle); + try std.testing.expect(expectedButtonClick(c.BN_CLICKED, accept, accept)); + try std.testing.expect(!expectedButtonClick(c.BN_SETFOCUS, accept, accept)); + try std.testing.expect(!expectedButtonClick(c.BN_KILLFOCUS, accept, accept)); + try std.testing.expect(!expectedButtonClick(c.BN_CLICKED, cancel, accept)); + try std.testing.expect(!expectedButtonClick(c.BN_CLICKED, null, accept)); } test "win32 palette theme preview can restore original config clone" { @@ -36746,78 +29615,6 @@ test "win32 palette theme commit mutates pending without losing rollback source" try std.testing.expectEqualStrings("Committed Theme", pending.theme.?.light); } -test "win32 buildCommandPaletteOverlayLabel reflects palette state" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const idle = try buildCommandPaletteOverlayLabel(std.testing.allocator, "", .{}); - defer std.testing.allocator.free(idle); - try std.testing.expectEqualStrings("Command", idle); - - const matches = try buildCommandPaletteOverlayLabel( - std.testing.allocator, - "0x96f", - .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme", .available = true }, - ); - defer std.testing.allocator.free(matches); - try std.testing.expectEqualStrings("Command 1", matches); - - const invalid = try buildCommandPaletteOverlayLabel( - std.testing.allocator, - "definitely_not_real", - .{}, - ); - defer std.testing.allocator.free(invalid); - try std.testing.expectEqualStrings("Command ?", invalid); -} - -test "win32 command palette rich feedback never calls a theme an action miss" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const feedback = try buildCommandPaletteFeedbackText( - std.testing.allocator, - "0x96f", - &.{}, - .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme", .available = true }, - ); - defer std.testing.allocator.free(feedback); - try std.testing.expect(std.mem.indexOf(u8, feedback, "0x96f") != null); - try std.testing.expect(std.mem.indexOf(u8, feedback, "Bundled theme") != null); - try std.testing.expect(std.mem.indexOf(u8, feedback, "No matching action") == null); - - const hidden_feedback = try buildCommandPaletteFeedbackText( - std.testing.allocator, - "0x96f", - &.{}, - .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme" }, - ); - defer std.testing.allocator.free(hidden_feedback); - try std.testing.expect(std.mem.indexOf(u8, hidden_feedback, "window larger") != null); -} - -test "win32 commandPaletteCompletionCandidate resolves and cycles defaults" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const snap = PaletteSnapshot.fromDefaults(); - - try std.testing.expectEqualStrings( - "reload_config", - commandPaletteCompletionCandidate(snap, "reload_", "reload_", false).?, - ); - - // toggle_* has multiple matches in defaults; first-next/reverse should - // land on entries that all begin with "toggle_". - const first = commandPaletteCompletionCandidate(snap, "toggle_", "toggle_", false).?; - try std.testing.expect(std.mem.startsWith(u8, first, "toggle_")); - - const second = commandPaletteCompletionCandidate(snap, "toggle_", first, false).?; - try std.testing.expect(std.mem.startsWith(u8, second, "toggle_")); - try std.testing.expect(!std.mem.eql(u8, first, second)); - - const reverse = commandPaletteCompletionCandidate(snap, "toggle_", first, true).?; - try std.testing.expect(std.mem.startsWith(u8, reverse, "toggle_")); - try std.testing.expect(!std.mem.eql(u8, reverse, first)); -} - test "win32 rich palette completion cycles duplicate display text by stable id" { var items: [2]PaletteItem = undefined; var payloads: [2]PalettePayload = undefined; @@ -36848,65 +29645,6 @@ test "win32 rich palette completion cycles duplicate display text by stable id" try std.testing.expect(!first.id.eql(second.id)); } -test "win32 commandPaletteBannerText shows ready banner for valid action" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const snap = PaletteSnapshot.fromDefaults(); - const empty_mru: []const []const u8 = &.{}; - const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "new_tab", empty_mru)).?; - defer std.testing.allocator.free(banner); - try std.testing.expect(std.mem.startsWith(u8, banner, "Ready: new_tab")); - try std.testing.expect(std.mem.indexOf(u8, banner, "Open a new tab") != null); -} - -test "win32 commandPaletteBannerText suggests matching actions" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const snap = PaletteSnapshot.fromDefaults(); - const empty_mru: []const []const u8 = &.{}; - const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "new_", empty_mru)).?; - defer std.testing.allocator.free(banner); - // Top-5 match cap means a specific "new_split:*" may or may not - // appear, but the banner should list at least new_tab and at - // least one new_split variant. - try std.testing.expect(std.mem.indexOf(u8, banner, "new_tab") != null); - try std.testing.expect(std.mem.indexOf(u8, banner, "new_split") != null); -} - -test "win32 commandPaletteBannerText resolves unique prefix" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const snap = PaletteSnapshot.fromDefaults(); - const empty_mru: []const []const u8 = &.{}; - const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "reload_", empty_mru)).?; - defer std.testing.allocator.free(banner); - try std.testing.expect(std.mem.startsWith(u8, banner, "Ready: reload_config")); - try std.testing.expect(std.mem.indexOf(u8, banner, "Reload") != null); -} - -test "win32 commandPaletteBannerText uses fullscreen description" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const snap = PaletteSnapshot.fromDefaults(); - const empty_mru: []const []const u8 = &.{}; - const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "toggle_fullscreen", empty_mru)).?; - defer std.testing.allocator.free(banner); - try std.testing.expect(std.mem.startsWith(u8, banner, "Ready: toggle_fullscreen")); - try std.testing.expect(std.mem.indexOf(u8, banner, "fullscreen") != null); -} - -test "win32 commandPaletteBannerText suggests tab overview action" { - if (builtin.os.tag != .windows) return error.SkipZigTest; - - const snap = PaletteSnapshot.fromDefaults(); - const empty_mru: []const []const u8 = &.{}; - const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "toggle_tab", empty_mru)).?; - defer std.testing.allocator.free(banner); - try std.testing.expect(std.mem.indexOf(u8, banner, "toggle_tab_overview") != null); - // "tab overview" is part of the default description verbatim. - try std.testing.expect(std.mem.indexOf(u8, banner, "tab overview") != null); -} - test "win32 desiredTabIndex cycles and clamps" { if (builtin.os.tag != .windows) return error.SkipZigTest; @@ -36953,18 +29691,18 @@ test "win32 tab button mouse-up only closes when released in close zone" { test "win32 captionButtonSysCommand maps integrated titlebar buttons" { if (builtin.os.tag != .windows) return error.SkipZigTest; - try std.testing.expectEqual(@as(?WPARAM, SC_MINIMIZE), captionButtonSysCommand(HTMINBUTTON, false)); - try std.testing.expectEqual(@as(?WPARAM, SC_MINIMIZE), captionButtonSysCommand(HTMINBUTTON, true)); - try std.testing.expectEqual(@as(?WPARAM, SC_MAXIMIZE), captionButtonSysCommand(HTMAXBUTTON, false)); - try std.testing.expectEqual(@as(?WPARAM, SC_RESTORE), captionButtonSysCommand(HTMAXBUTTON, true)); + try std.testing.expectEqual(@as(?WPARAM, c.SC_MINIMIZE), captionButtonSysCommand(c.HTMINBUTTON, false)); + try std.testing.expectEqual(@as(?WPARAM, c.SC_MINIMIZE), captionButtonSysCommand(c.HTMINBUTTON, true)); + try std.testing.expectEqual(@as(?WPARAM, c.SC_MAXIMIZE), captionButtonSysCommand(c.HTMAXBUTTON, false)); + try std.testing.expectEqual(@as(?WPARAM, c.SC_RESTORE), captionButtonSysCommand(c.HTMAXBUTTON, true)); } test "win32 captionButtonSysCommand ignores non minimize/maximize hit tests" { if (builtin.os.tag != .windows) return error.SkipZigTest; - try std.testing.expectEqual(@as(?WPARAM, null), captionButtonSysCommand(HTCLIENT, false)); - try std.testing.expectEqual(@as(?WPARAM, null), captionButtonSysCommand(HTCAPTION, false)); - try std.testing.expectEqual(@as(?WPARAM, null), captionButtonSysCommand(HTCLOSE, false)); + try std.testing.expectEqual(@as(?WPARAM, null), captionButtonSysCommand(c.HTCLIENT, false)); + try std.testing.expectEqual(@as(?WPARAM, null), captionButtonSysCommand(c.HTCAPTION, false)); + try std.testing.expectEqual(@as(?WPARAM, null), captionButtonSysCommand(c.HTCLOSE, false)); } test "win32 titlebar glyph mapping uses Win11 glyph codepoints" { diff --git a/src/apprt/win32/chrome_layout.zig b/src/apprt/win32/chrome_layout.zig new file mode 100644 index 00000000000..05d0e915148 --- /dev/null +++ b/src/apprt/win32/chrome_layout.zig @@ -0,0 +1,259 @@ +//! Host-independent Win32 chrome and overlay rectangle math. +//! +//! This complements `win32_layout.zig`: that sibling owns normalized pane +//! topology, while this module owns native `RECT` calculations for chrome +//! controls and overlay action rows. + +const std = @import("std"); +const builtin = @import("builtin"); + +const win32_layout = @import("../win32_layout.zig"); +const win32_chrome_state = @import("../win32_chrome_state.zig"); +const win32_theme = @import("../win32_theme.zig"); +const labels = @import("labels.zig"); +const sys = @import("sys.zig"); + +const HostOverlayMode = win32_theme.HostOverlayMode; +const RECT = sys.RECT; +const overlay_right_gap_base: i32 = 6; +const overlay_min_edit_width_base: i32 = 24; + +const OverlayActionVisibility = struct { + accept: bool, + cancel: bool, +}; + +const OverlayActionLayout = struct { + accept_visible: bool, + cancel_visible: bool, + compact_cancel: bool, + accept_x: i32, + cancel_x: i32, + accept_width: i32, + cancel_width: i32, + accept_reservation_width: i32, +}; + +pub fn rectEquals(a: RECT, b: RECT) bool { + return a.left == b.left and + a.top == b.top and + a.right == b.right and + a.bottom == b.bottom; +} + +pub fn childRect(x: i32, y: i32, width: i32, height: i32) RECT { + return .{ + .left = x, + .top = y, + .right = x + width, + .bottom = y + height, + }; +} + +pub fn layoutRectToWin32(rect: win32_layout.Rect) RECT { + return .{ + .left = rect.left, + .top = rect.top, + .right = rect.right, + .bottom = rect.bottom, + }; +} + +pub fn centeredRect(rect: RECT, width: i32, height: i32) RECT { + const outer_w = rect.right - rect.left; + const outer_h = rect.bottom - rect.top; + const left = rect.left + @divTrunc(outer_w - width, 2); + const top = rect.top + @divTrunc(outer_h - height, 2); + return childRect(left, top, width, height); +} + +pub fn overlayEditFrameRect( + width: i32, + overlay_y: i32, + padding: i32, + label_w: i32, + cancel_w: i32, + accept_reservation_w: i32, + row_h: i32, + dpi: u32, +) RECT { + const top_offset = scaledBy(4, dpi); + const right_gap = scaledBy(overlay_right_gap_base, dpi); + const min_edit_width = scaledBy(overlay_min_edit_width_base, dpi); + const effective_label_w = overlayLabelReservation( + width, + padding, + label_w, + cancel_w, + accept_reservation_w, + dpi, + ); + const bounded_width = @max(0, width); + const raw_right = width - cancel_w - accept_reservation_w - (padding * 2) - right_gap; + const right = @min(bounded_width, @max(@min(bounded_width, min_edit_width), raw_right)); + const left = @min( + @max(0, padding + effective_label_w), + @max(0, right - min_edit_width), + ); + return .{ + .left = left, + .top = overlay_y + top_offset, + .right = right, + .bottom = overlay_y + top_offset + row_h, + }; +} + +pub fn overlayLabelReservation( + width: i32, + padding: i32, + desired_label_w: i32, + cancel_w: i32, + accept_reservation_w: i32, + dpi: u32, +) i32 { + const right_gap = scaledBy(overlay_right_gap_base, dpi); + const min_edit_width = scaledBy(overlay_min_edit_width_base, dpi); + const desired = @max(0, desired_label_w); + const available_before_actions = width - @max(0, cancel_w) - @max(0, accept_reservation_w) - + 2 * @max(0, padding) - right_gap; + return if (available_before_actions - @max(0, padding) >= desired + min_edit_width) desired else 0; +} + +fn overlayActionVisibilityForWidth( + width: i32, + padding: i32, + cancel_w: i32, + accept_w: i32, + accept_requested: bool, + dpi: u32, +) OverlayActionVisibility { + const bounded_padding = @max(0, padding); + const right_gap = scaledBy(overlay_right_gap_base, dpi); + const min_edit_width = scaledBy(overlay_min_edit_width_base, dpi); + const cancel = width >= @max(0, cancel_w) + 3 * bounded_padding + right_gap + min_edit_width; + const accept = cancel and accept_requested and + width >= @max(0, cancel_w) + @max(0, accept_w) + 4 * bounded_padding + right_gap + min_edit_width; + return .{ .accept = accept, .cancel = cancel }; +} + +pub fn overlayActionLayoutForWidth( + mode: HostOverlayMode, + width: i32, + padding: i32, + cancel_button_w: i32, + accept_button_w: i32, + dpi: u32, +) OverlayActionLayout { + const bounded_padding = @max(0, padding); + const visibility = overlayActionVisibilityForWidth( + width, + bounded_padding, + cancel_button_w, + accept_button_w, + labels.overlayAcceptButtonVisible(mode), + dpi, + ); + const compact_cancel = mode == .confirm and !visibility.cancel and width > 0; + const compact_inset = if (compact_cancel) + @min(bounded_padding, @divTrunc(width - 1, 2)) + else + bounded_padding; + const cancel_visible = visibility.cancel or compact_cancel; + const accept_width = if (visibility.accept) @max(0, accept_button_w) else 0; + const cancel_width = if (compact_cancel) + width - 2 * compact_inset + else if (cancel_visible) + @max(0, cancel_button_w) + else + 0; + return .{ + .accept_visible = visibility.accept, + .cancel_visible = cancel_visible, + .compact_cancel = compact_cancel, + .accept_x = @max(0, width - cancel_width - accept_width - 2 * bounded_padding), + .cancel_x = if (compact_cancel) compact_inset else width - cancel_width - bounded_padding, + .accept_width = accept_width, + .cancel_width = cancel_width, + .accept_reservation_width = if (visibility.accept) accept_width + bounded_padding else 0, + }; +} + +pub fn overlayEditChildRectFromFrame(frame: RECT, inset_x: i32, inset_y: i32) RECT { + const left = @min(frame.right, frame.left + @max(0, inset_x)); + const top = @min(frame.bottom, frame.top + @max(0, inset_y)); + return .{ + .left = left, + .top = top, + .right = @max(left, frame.right - @max(0, inset_x)), + .bottom = @max(top, frame.bottom - @max(0, inset_y)), + }; +} + +fn scaledBy(base: i32, dpi: u32) i32 { + return win32_chrome_state.scaled(base, dpi); +} + +test "win32 overlay edit child rect preserves frame border" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const frame = RECT{ .left = 100, .top = 20, .right = 300, .bottom = 58 }; + const child = overlayEditChildRectFromFrame(frame, 8, 6); + try std.testing.expectEqual(@as(i32, 108), child.left); + try std.testing.expectEqual(@as(i32, 26), child.top); + try std.testing.expectEqual(@as(i32, 292), child.right); + try std.testing.expectEqual(@as(i32, 52), child.bottom); + try std.testing.expect(child.bottom < frame.bottom); +} + +test "win32 overlay edit frame offsets scale with DPI" { + const dpis = [_]u32{ 96, 192, 288 }; + for (dpis, 1..) |dpi, scale| { + const scale_i32: i32 = @intCast(scale); + const padding = 10 * scale_i32; + const label_w = 100 * scale_i32; + const cancel_w = 80 * scale_i32; + const row_h = 30 * scale_i32; + const overlay_y = 20 * scale_i32; + const frame = overlayEditFrameRect( + 800 * scale_i32, + overlay_y, + padding, + label_w, + cancel_w, + 0, + row_h, + dpi, + ); + try std.testing.expectEqual(overlay_y + 4 * scale_i32, frame.top); + try std.testing.expectEqual(frame.top + row_h, frame.bottom); + try std.testing.expectEqual( + 800 * scale_i32 - cancel_w - padding * 2 - 6 * scale_i32, + frame.right, + ); + } + + const narrow = overlayEditFrameRect(220, 20, 10, 100, 80, 0, 30, 96); + const close_left = 220 - 80 - 10; + try std.testing.expectEqual(@as(i32, 10), narrow.left); + try std.testing.expect(narrow.right <= close_left); + try std.testing.expect(narrow.right >= narrow.left); + + for ([_]i32{ 80, 100, 120, 140 }) |width| { + const visibility = overlayActionVisibilityForWidth(width, 10, 80, 80, false, 96); + const effective_cancel: i32 = if (visibility.cancel) 80 else 0; + const frame = overlayEditFrameRect(width, 20, 10, 100, effective_cancel, 0, 30, 96); + const child = overlayEditChildRectFromFrame(frame, 8, 6); + try std.testing.expect(child.left >= frame.left); + try std.testing.expect(child.right <= frame.right); + try std.testing.expect(child.left <= child.right); + if (visibility.cancel) try std.testing.expect(child.right <= width - 80 - 10); + } + for ([_]i32{ 1, 10, 20, 30 }) |width| { + const frame = overlayEditFrameRect(width, 20, 10, 100, 0, 0, 30, 96); + try std.testing.expect(frame.left >= 0); + try std.testing.expect(frame.right <= width); + try std.testing.expect(frame.right > frame.left); + } + try std.testing.expect(!overlayActionVisibilityForWidth(220, 10, 80, 80, true, 96).accept); + try std.testing.expect(overlayActionVisibilityForWidth(230, 10, 80, 80, true, 96).accept); +} diff --git a/src/apprt/win32/consts.zig b/src/apprt/win32/consts.zig new file mode 100644 index 00000000000..1b52305a195 --- /dev/null +++ b/src/apprt/win32/consts.zig @@ -0,0 +1,398 @@ +//! Win32 API constants used by the Windows application runtime. + +const win32_types = @import("../win32_types.zig"); + +const INTRESOURCE = win32_types.INTRESOURCE; +const LPARAM = win32_types.LPARAM; +const WPARAM = win32_types.WPARAM; +const UINT = win32_types.UINT; +const UINT_PTR = win32_types.UINT_PTR; +const DWORD = win32_types.DWORD; +const WORD = win32_types.WORD; + +pub const CS_OWNDC = 0x0020; +pub const CW_USEDEFAULT = @as(i32, @bitCast(@as(u32, 0x80000000))); +pub const GWLP_USERDATA = -21; +pub const GWLP_WNDPROC = -4; +pub const GWL_STYLE = -16; +pub const GWL_EXSTYLE = -20; +pub const IDC_ARROW = @as(INTRESOURCE, @ptrFromInt(32512)); +pub const ID_ICON_GHOSTTY = 1; +pub const IMAGE_ICON = 1; +pub const LR_SHARED = 0x00008000; +pub const SM_CXICON = 11; +pub const SM_CYICON = 12; +pub const SM_CXSMICON = 49; +pub const SM_CYSMICON = 50; +pub const WM_SETICON = 0x0080; +pub const ICON_SMALL = 0; +pub const ICON_BIG = 1; +pub const HTNOWHERE = 0; +pub const HTCLIENT = 1; +pub const HTCAPTION = 2; +pub const HTSYSMENU = 3; +pub const HTMINBUTTON = 8; +pub const HTMAXBUTTON = 9; +pub const HTLEFT = 10; +pub const HTRIGHT = 11; +pub const HTTOP = 12; +pub const HTTOPLEFT = 13; +pub const HTTOPRIGHT = 14; +pub const HTBOTTOM = 15; +pub const HTBOTTOMLEFT = 16; +pub const HTBOTTOMRIGHT = 17; +pub const HTCLOSE = 20; + +pub const SW_SHOW = 5; +pub const SW_SHOWNOACTIVATE = 4; +pub const SW_RESTORE = 9; +pub const SW_MAXIMIZE = 3; +pub const SW_HIDE = 0; +const WM_APP = 0x8000; +pub const WM_ACTIVATE = 0x0006; +pub const WA_INACTIVE = 0; +pub const WM_COMMAND = 0x0111; +pub const WM_CAPTURECHANGED = 0x0215; +pub const WM_CLOSE = 0x0010; +pub const WM_DESTROY = 0x0002; +pub const WM_DRAWITEM = 0x002B; +pub const WM_ERASEBKGND = 0x0014; +pub const WM_GETMINMAXINFO = 0x0024; +pub const WM_CHAR = 0x0102; +pub const WM_DEADCHAR = 0x0103; +pub const WM_HOTKEY = 0x0312; +pub const WM_IME_SETCONTEXT = 0x0281; +pub const WM_IME_STARTCOMPOSITION = 0x010D; +pub const WM_IME_ENDCOMPOSITION = 0x010E; +pub const WM_IME_COMPOSITION = 0x010F; +pub const GCS_COMPSTR: u32 = 0x0008; +pub const GCS_RESULTSTR: u32 = 0x0800; +pub const CFS_POINT: u32 = 0x0002; +pub const CFS_EXCLUDE: u32 = 0x0080; +pub const ISC_SHOWUICOMPOSITIONWINDOW: LPARAM = 0x80000000; +pub const WM_KILLFOCUS = 0x0008; +pub const WM_KEYDOWN = 0x0100; +pub const WM_KEYUP = 0x0101; +pub const WM_LBUTTONDOWN = 0x0201; +pub const WM_LBUTTONUP = 0x0202; +pub const WM_LBUTTONDBLCLK = 0x0203; +pub const WM_MBUTTONDOWN = 0x0207; +pub const WM_MBUTTONUP = 0x0208; +pub const WM_MOUSEHWHEEL = 0x020E; +pub const WM_MOUSEMOVE = 0x0200; +pub const WM_MOUSEWHEEL = 0x020A; +pub const WM_MOUSELEAVE = 0x02A3; +pub const WM_POINTERHWHEEL = 0x024F; +pub const WM_POINTERWHEEL = 0x024E; +pub const WM_NCCREATE = 0x0081; +pub const WM_NCCALCSIZE = 0x0083; +pub const WM_NCHITTEST = 0x0084; +pub const WM_NCMOUSEMOVE = 0x00A0; +pub const WM_NCLBUTTONDOWN = 0x00A1; +pub const WM_NCLBUTTONUP = 0x00A2; +pub const WM_NCMOUSELEAVE = 0x02A2; +pub const WM_SYSCOMMAND = 0x0112; +pub const SC_CLOSE: WPARAM = 0xF060; +pub const SC_MINIMIZE: WPARAM = 0xF020; +pub const SC_MAXIMIZE: WPARAM = 0xF030; +pub const SC_RESTORE: WPARAM = 0xF120; +pub const WM_PAINT = 0x000F; +pub const WM_QUIT = 0x0012; +pub const WM_TIMER = 0x0113; +pub const WM_CTLCOLOREDIT = 0x0133; +pub const WM_CTLCOLORBTN = 0x0135; +pub const WM_CTLCOLORSTATIC = 0x0138; +pub const WM_GETOBJECT: UINT = 0x003D; +pub const WM_RBUTTONDOWN = 0x0204; +pub const WM_XBUTTONDOWN = 0x020B; +pub const WM_XBUTTONUP = 0x020C; +pub const XBUTTON1: WORD = 0x0001; +pub const XBUTTON2: WORD = 0x0002; +pub const WM_RBUTTONUP = 0x0205; +pub const WM_SETCURSOR = 0x0020; +pub const WM_SETFOCUS = 0x0007; +pub const WM_SETTINGCHANGE = 0x001A; +pub const WM_SIZE = 0x0005; +pub const WM_ENTERSIZEMOVE = 0x0231; +pub const WM_EXITSIZEMOVE = 0x0232; +pub const SIZE_MAXIMIZED: u32 = 2; +pub const SIZE_MINIMIZED: u32 = 1; +pub const SIZE_RESTORED: u32 = 0; +pub const WM_SYSKEYDOWN = 0x0104; +pub const WM_SYSKEYUP = 0x0105; +pub const WM_SYSDEADCHAR = 0x0107; +pub const WM_WINHOSTTY_WAKE = WM_APP + 1; +pub const WM_WINHOSTTY_UPDATE = WM_APP + 2; +pub const WM_WINHOSTTY_TOAST_ACTIVATION = WM_APP + 3; +pub const WM_WINHOSTTY_HOST_NEW_TAB = WM_APP + 4; +pub const WM_WINHOSTTY_UIA_DISCONNECT = WM_APP + 5; +pub const WM_WINHOSTTY_UIA_QUERY_REFRESH = WM_APP + 6; + +pub const PM_NOREMOVE: UINT = 0x0000; +pub const PM_REMOVE: UINT = 0x0001; +pub const WS_OVERLAPPED = 0x00000000; +pub const WS_CHILD = 0x40000000; +pub const WS_CLIPCHILDREN = 0x02000000; +pub const WS_CLIPSIBLINGS = 0x04000000; +pub const WS_CAPTION = 0x00C00000; +pub const WS_SYSMENU = 0x00080000; +pub const WS_THICKFRAME = 0x00040000; +pub const WS_MINIMIZEBOX = 0x00020000; +pub const WS_MAXIMIZEBOX = 0x00010000; +pub const WS_VISIBLE = 0x10000000; +pub const WS_TABSTOP = 0x00010000; + +pub const WS_POPUP = 0x80000000; +pub const WS_EX_LAYERED = 0x00080000; +pub const WS_EX_TRANSPARENT = 0x00000020; +pub const MOD_ALT = 0x0001; +pub const MOD_CONTROL = 0x0002; +pub const MOD_SHIFT = 0x0004; +pub const MOD_WIN = 0x0008; +pub const TO_UNICODE_NO_STATE_CHANGE: UINT = 0x0004; +pub const SWP_NOSIZE = 0x0001; +pub const SWP_NOMOVE = 0x0002; +pub const SWP_NOZORDER = 0x0004; +pub const SWP_NOACTIVATE = 0x0010; +pub const SWP_FRAMECHANGED = 0x0020; +pub const RDW_INVALIDATE: UINT = 0x0001; +pub const RDW_INTERNALPAINT: UINT = 0x0002; +pub const RDW_ERASE: UINT = 0x0004; +pub const RDW_NOCHILDREN: UINT = 0x0040; +pub const RDW_ALLCHILDREN: UINT = 0x0080; +pub const RDW_UPDATENOW: UINT = 0x0100; +pub const RDW_FRAME: UINT = 0x0400; +pub const MONITOR_DEFAULTTONEAREST = 0x00000002; +pub const MONITOR_DEFAULTTOPRIMARY = 0x00000001; +pub const MONITORINFOF_PRIMARY = 0x00000001; +pub const COLOR_WINDOW = 5; +pub const CF_UNICODETEXT = 13; + +pub const GMEM_MOVEABLE = 0x0002; +pub const GMEM_ZEROINIT = 0x0040; +pub const LWA_COLORKEY = 0x00000001; +pub const LWA_ALPHA = 0x00000002; +pub const TRANSPARENT = 1; +pub const OPAQUE = 2; +pub const MB_OK = 0x00000000; +pub const MB_ICONERROR = 0x00000010; +pub const MB_ICONINFORMATION = 0x00000040; +pub const MB_SETFOREGROUND = 0x00010000; +pub const MK_CONTROL = 0x0008; +pub const MK_LBUTTON = 0x0001; +pub const MK_MBUTTON = 0x0010; +pub const MK_RBUTTON = 0x0002; + +pub const MK_SHIFT = 0x0004; +pub const MK_XBUTTON1 = 0x0020; +pub const MK_XBUTTON2 = 0x0040; +pub const EN_CHANGE = 0x0300; +pub const BS_OWNERDRAW = 0x0000000B; +pub const SS_RIGHT = 0x00000002; +pub const SS_CENTERIMAGE = 0x00000200; +pub const SS_OWNERDRAW = 0x0000000D; +pub const ES_AUTOHSCROLL = 0x0080; +pub const BN_CLICKED = 0; +pub const BN_SETFOCUS = 6; +pub const BN_KILLFOCUS = 7; +pub const EM_SETSEL = 0x00B1; +pub const EM_GETSEL = 0x00B0; +pub const EM_CHARFROMPOS = 0x00D7; +pub const EM_SETMARGINS = 0x00D3; +pub const EM_SETCUEBANNER = 0x1501; +pub const EC_LEFTMARGIN: usize = 0x0001; +pub const EC_RIGHTMARGIN: usize = 0x0002; +pub const ODT_BUTTON = 4; +pub const ODT_STATIC = 5; +pub const ODS_SELECTED = 0x0001; +pub const ODS_DISABLED = 0x0004; +pub const ODS_FOCUS = 0x0010; +pub const TME_LEAVE = 0x00000002; +pub const TME_NONCLIENT = 0x00000010; +pub const DT_CENTER = 0x00000001; +pub const DT_VCENTER = 0x00000004; +pub const DT_SINGLELINE = 0x00000020; +pub const DT_NOPREFIX = 0x00000800; +pub const DT_END_ELLIPSIS = 0x00008000; +pub const DT_LEFT = 0x00000000; + +pub const WM_THEMECHANGED = 0x031A; +pub const WM_SYSCOLORCHANGE = 0x0015; +pub const WM_DWMCOLORIZATIONCOLORCHANGED: UINT = 0x0320; +pub const WM_DPICHANGED: UINT = 0x02E0; +pub const DWMSBT_NONE: u32 = 1; + +pub const DWMSBT_TABBEDWINDOW: u32 = 4; +pub const OS_BUILD_WIN10_22H2: u32 = 19045; +pub const OS_BUILD_WIN11_21H2: u32 = 22000; +pub const OS_BUILD_WIN11_22H2: u32 = 22621; +pub const DC_BRUSH: i32 = 18; +pub const DC_PEN: i32 = 19; + +pub const SPI_GETHIGHCONTRAST: UINT = 0x0042; +pub const SPI_GETNONCLIENTMETRICS: UINT = 0x0029; +pub const SPI_GETCLIENTAREAANIMATION: UINT = 0x1042; + +/// See src/apprt/win32_tween.zig for the scheduler contract. +pub const TWEEN_TIMER_ID: UINT_PTR = 0x77684701; // "whgT1" in 32-bit hex +pub const TWEEN_TIMER_INTERVAL_MS: UINT = 16; +pub const SEARCH_TIMER_ID: UINT_PTR = 0x77684702; // "whgT2" in 32-bit hex +pub const SEARCH_TIMER_INTERVAL_MS: UINT = 20; +pub const SCROLLBAR_TIMER_ID: UINT_PTR = 0x77684703; // "whgT3" in 32-bit hex +pub const SCROLLBAR_TIMER_INTERVAL_MS: UINT = 16; +pub const RESIZE_SETTLE_TIMER_ID: UINT_PTR = 0x77684704; // "whgT4" in 32-bit hex +pub const RESIZE_SETTLE_TIMER_INTERVAL_MS: UINT = 16; +pub const TERMINAL_UIA_TIMER_ID: UINT_PTR = 0x77684705; // "whgT5" in 32-bit hex +pub const RESIZE_SETTLE_REPAINT_TICKS: u8 = 12; +pub const FW_NORMAL: i32 = 400; +pub const DEFAULT_CHARSET: u8 = 1; +pub const OUT_DEFAULT_PRECIS: u8 = 0; +pub const CLIP_DEFAULT_PRECIS: u8 = 0; +pub const CLEARTYPE_QUALITY: u8 = 5; +pub const DEFAULT_PITCH: u8 = 0; +pub const FF_DONTCARE: u8 = 0; +pub const LF_FACESIZE = 32; +pub const HCF_HIGHCONTRASTON: DWORD = 0x00000001; +pub const COLOR_WINDOWTEXT = 8; +pub const COLOR_WINDOWFRAME = 6; +pub const COLOR_BTNFACE = 15; +pub const COLOR_BTNTEXT = 18; +pub const COLOR_GRAYTEXT = 17; +pub const COLOR_HIGHLIGHT = 13; +pub const COLOR_HIGHLIGHTTEXT = 14; +pub const HKEY_CURRENT_USER: usize = 0x80000001; +pub const KEY_READ: DWORD = 0x20019; +pub const REG_DWORD: DWORD = 4; +pub const ERROR_SUCCESS: i32 = 0; +pub const ERROR_MOD_NOT_FOUND: DWORD = 126; +pub const PFD_DRAW_TO_WINDOW = 0x00000004; +pub const PFD_SUPPORT_OPENGL = 0x00000020; +pub const PFD_DOUBLEBUFFER = 0x00000001; +pub const PFD_TYPE_RGBA = 0; +pub const PFD_MAIN_PLANE = 0; +pub const SEM_FAILCRITICALERRORS: DWORD = 0x0001; +pub const SEM_NOOPENFILEERRORBOX: DWORD = 0x8000; + +pub const MF_STRING: UINT = 0x00000000; +pub const MF_SEPARATOR: UINT = 0x00000800; +pub const MF_GRAYED: UINT = 0x00000001; +pub const TPM_LEFTALIGN: UINT = 0x0000; +pub const TPM_TOPALIGN: UINT = 0x0000; +pub const TPM_RETURNCMD: UINT = 0x0100; +pub const TPM_RIGHTBUTTON: UINT = 0x0002; +pub const WM_NULL: UINT = 0x0000; +pub const WM_SETFONT: UINT = 0x0030; +pub const CTX_COPY: usize = 4001; +pub const CTX_PASTE: usize = 4002; +pub const CTX_SELECT_ALL: usize = 4003; +pub const CTX_FIND: usize = 4004; +pub const CTX_COMMAND_PALETTE: usize = 4005; +pub const CTX_NEW_TAB: usize = 4006; +pub const CTX_SPLIT_RIGHT: usize = 4007; +pub const CTX_NEW_WINDOW: usize = 4008; +pub const CTX_INSPECTOR: usize = 4009; +pub const CTX_SPLIT_DOWN: usize = 4010; +pub const CTX_SPLIT_LEFT: usize = 4011; +pub const CTX_SPLIT_UP: usize = 4012; +pub const CTX_TAB_RENAME: usize = 4020; +pub const CTX_TAB_CLOSE: usize = 4021; +pub const CTX_TAB_CLOSE_OTHERS: usize = 4022; +pub const CTX_TAB_MOVE_LEFT: usize = 4023; +pub const CTX_TAB_MOVE_RIGHT: usize = 4024; +pub const CTX_PROFILE_BASE: usize = 4100; // profile dropdown items: CTX_PROFILE_BASE + index +pub const SEARCH_BG_ID: usize = 2100; +pub const SEARCH_EDIT_ID: usize = 2101; +pub const SEARCH_PREV_ID: usize = 2102; +pub const SEARCH_NEXT_ID: usize = 2103; +pub const SEARCH_REGEX_ID: usize = 2104; +pub const SEARCH_CASE_ID: usize = 2105; +pub const SEARCH_WORD_ID: usize = 2106; +pub const SEARCH_RESULTS_ID: usize = 2107; +pub const SEARCH_CLOSE_ID: usize = 2108; +pub const MF_POPUP: UINT = 0x00000010; +pub const MF_CHECKED: UINT = 0x00000008; + +pub const VK_BACK = 0x08; +pub const VK_TAB = 0x09; +pub const VK_RETURN = 0x0D; +pub const VK_SHIFT = 0x10; +pub const VK_CONTROL = 0x11; +pub const VK_MENU = 0x12; +pub const VK_PAUSE = 0x13; +pub const VK_CAPITAL = 0x14; +pub const VK_ESCAPE = 0x1B; +pub const VK_SPACE = 0x20; +pub const VK_PRIOR = 0x21; +pub const VK_NEXT = 0x22; +pub const VK_END = 0x23; +pub const VK_HOME = 0x24; +pub const VK_LEFT = 0x25; +pub const VK_UP = 0x26; +pub const VK_RIGHT = 0x27; +pub const VK_DOWN = 0x28; +pub const VK_SNAPSHOT = 0x2C; +pub const VK_INSERT = 0x2D; +pub const VK_DELETE = 0x2E; +pub const VK_0 = 0x30; +pub const VK_9 = 0x39; +pub const VK_A = 0x41; +pub const VK_Z = 0x5A; +pub const VK_LWIN = 0x5B; +pub const VK_RWIN = 0x5C; +pub const VK_APPS = 0x5D; +pub const VK_NUMPAD0 = 0x60; +pub const VK_NUMPAD9 = 0x69; +pub const VK_MULTIPLY = 0x6A; +pub const VK_ADD = 0x6B; +pub const VK_SEPARATOR = 0x6C; +pub const VK_SUBTRACT = 0x6D; +pub const VK_DECIMAL = 0x6E; +pub const VK_DIVIDE = 0x6F; +pub const VK_F1 = 0x70; +pub const VK_F3 = 0x72; +pub const VK_F2 = 0x71; +pub const VK_F24 = 0x87; +pub const VK_NUMLOCK = 0x90; +pub const VK_SCROLL = 0x91; +pub const VK_LSHIFT = 0xA0; +pub const VK_RSHIFT = 0xA1; +pub const VK_LCONTROL = 0xA2; +pub const VK_RCONTROL = 0xA3; +pub const VK_LMENU = 0xA4; +pub const VK_RMENU = 0xA5; +pub const VK_OEM_1 = 0xBA; +pub const VK_OEM_PLUS = 0xBB; +pub const VK_OEM_COMMA = 0xBC; +pub const VK_OEM_MINUS = 0xBD; +pub const VK_OEM_PERIOD = 0xBE; +pub const VK_OEM_2 = 0xBF; +pub const VK_OEM_3 = 0xC0; +pub const VK_OEM_4 = 0xDB; +pub const VK_OEM_5 = 0xDC; +pub const VK_OEM_6 = 0xDD; +pub const VK_OEM_7 = 0xDE; +pub const VK_PACKET = 0xE7; + +pub const KF_EXTENDED = 1 << 24; +pub const KF_REPEAT = 1 << 30; +pub const SPI_GETWHEELSCROLLLINES = 0x0068; +pub const SPI_GETWHEELSCROLLCHARS = 0x006C; +pub const WHEEL_DELTA = 120; +pub const WHEEL_PAGESCROLL = 0xFFFF_FFFF; +pub const ERROR_FILE_NOT_FOUND = 2; +pub const PIPE_READMODE_BYTE = 0x00000000; +pub const PIPE_ACCESS_DUPLEX = 0x00000003; +pub const PIPE_UNLIMITED_INSTANCES = 255; + +/// Main-thread COM apartment for in-process STA clients (settings path +/// picker, WinRT toast factory, OLE drag-drop targets). `S_FALSE` means +/// the desired STA already exists. `RPC_E_CHANGED_MODE` means the thread +/// is already in a different apartment and is not safe for STA-only work. +pub const COINIT_APARTMENTTHREADED: u32 = 0x2; +pub const RPC_E_CHANGED_MODE: i32 = @bitCast(@as(u32, 0x80010106)); + +/// Fallback when the target doesn't yet exist (first-time save). Unlike +/// `ReplaceFileW`, `MoveFileExW` works on a missing target. +pub const MOVEFILE_REPLACE_EXISTING: u32 = 0x1; + +pub const WM_DROPFILES: UINT = 0x0233; diff --git a/src/apprt/win32/gdi.zig b/src/apprt/win32/gdi.zig new file mode 100644 index 00000000000..4b29ed59755 --- /dev/null +++ b/src/apprt/win32/gdi.zig @@ -0,0 +1,119 @@ +//! Host-independent Win32 GDI paint primitives. + +const std = @import("std"); +const builtin = @import("builtin"); + +const c = @import("consts.zig"); +const sys = @import("sys.zig"); +const win32_types = @import("../win32_types.zig"); + +const HDC = win32_types.HDC; +const RECT = sys.RECT; +const UINT = win32_types.UINT; + +/// Linear-interpolate two `COLORREF`-shaped values (`0x00BBGGRR` on +/// Windows GDI) by `alpha` in [0, 1]. `alpha = 0` returns `a`, +/// `alpha = 1` returns `b`. Used for pre-composited alpha on paths +/// where GDI cannot render RGBA directly. +pub fn blendColorRGB(a: u32, b: u32, alpha: f32) u32 { + const t: f32 = std.math.clamp(alpha, 0.0, 1.0); + const inv: f32 = 1.0 - t; + const ar: u32 = a & 0xFF; + const ag: u32 = (a >> 8) & 0xFF; + const ab: u32 = (a >> 16) & 0xFF; + const br: u32 = b & 0xFF; + const bg: u32 = (b >> 8) & 0xFF; + const bb: u32 = (b >> 16) & 0xFF; + const rr: u32 = @intFromFloat(@as(f32, @floatFromInt(ar)) * inv + @as(f32, @floatFromInt(br)) * t); + const gg: u32 = @intFromFloat(@as(f32, @floatFromInt(ag)) * inv + @as(f32, @floatFromInt(bg)) * t); + const bb2: u32 = @intFromFloat(@as(f32, @floatFromInt(ab)) * inv + @as(f32, @floatFromInt(bb)) * t); + return (bb2 << 16) | (gg << 8) | rr; +} + +pub fn fillSolidRect(hdc: HDC, rect: RECT, color: u32) void { + const brush = sys.GetStockObject(c.DC_BRUSH) orelse return; + _ = sys.SetDCBrushColor(hdc, color); + _ = sys.FillRect(hdc, &rect, brush); +} + +pub fn drawRectBorder(hdc: HDC, rect: RECT, color: u32, thickness: i32) void { + if (rect.right <= rect.left or rect.bottom <= rect.top or thickness <= 0) return; + const stroke = @min(thickness, @min(rect.right - rect.left, rect.bottom - rect.top)); + fillSolidRect(hdc, .{ .left = rect.left, .top = rect.top, .right = rect.right, .bottom = rect.top + stroke }, color); + fillSolidRect(hdc, .{ .left = rect.left, .top = rect.bottom - stroke, .right = rect.right, .bottom = rect.bottom }, color); + fillSolidRect(hdc, .{ .left = rect.left, .top = rect.top + stroke, .right = rect.left + stroke, .bottom = rect.bottom - stroke }, color); + fillSolidRect(hdc, .{ .left = rect.right - stroke, .top = rect.top + stroke, .right = rect.right, .bottom = rect.bottom - stroke }, color); +} + +pub fn textOutWz(hdc: HDC, x: i32, y: i32, text: [:0]const u16) void { + const len = utf16GdiTextLen(text); + if (len == 0) return; + _ = sys.TextOutW(hdc, x, y, text.ptr, len); +} + +pub fn drawTextWz(hdc: HDC, text: [:0]const u16, rect: *RECT, format: UINT) void { + const len = utf16GdiTextLen(text); + if (len == 0) return; + _ = sys.DrawTextW(hdc, text.ptr, len, rect, format); +} + +pub fn drawRoundedRect(hdc: HDC, rect: RECT, bg: u32, border: u32, radius: i32) void { + const stock_brush = sys.GetStockObject(c.DC_BRUSH) orelse return; + const stock_pen = sys.GetStockObject(c.DC_PEN) orelse return; + _ = sys.SetDCBrushColor(hdc, bg); + _ = sys.SetDCPenColor(hdc, border); + const old_brush = sys.SelectObject(hdc, stock_brush); + const old_pen = sys.SelectObject(hdc, stock_pen); + // GDI strokes are centered; the inset prevents clipping at the edges. + _ = sys.RoundRect(hdc, rect.left + 1, rect.top + 1, rect.right - 1, rect.bottom - 1, radius, radius); + _ = sys.SelectObject(hdc, old_pen); + _ = sys.SelectObject(hdc, old_brush); +} + +pub fn paintRectVisible(hdc: HDC, paint_rect: RECT, rect: RECT) bool { + if (rect.right <= rect.left or rect.bottom <= rect.top) return false; + if (!rectIntersects(paint_rect, rect)) return false; + return sys.RectVisible(hdc, &rect) != 0; +} + +fn rectIntersects(a: RECT, b: RECT) bool { + return a.left < b.right and + a.right > b.left and + a.top < b.bottom and + a.bottom > b.top; +} + +fn utf16GdiTextLen(text: [:0]const u16) i32 { + const max_len: usize = @intCast(std.math.maxInt(i32)); + return @intCast(@min(text.len, max_len)); +} + +test "win32 rectIntersects only trips on positive overlap" { + const testing = std.testing; + + try testing.expect(rectIntersects( + .{ .left = 0, .top = 0, .right = 10, .bottom = 10 }, + .{ .left = 5, .top = 5, .right = 15, .bottom = 15 }, + )); + try testing.expect(!rectIntersects( + .{ .left = 0, .top = 0, .right = 10, .bottom = 10 }, + .{ .left = 10, .top = 0, .right = 20, .bottom = 10 }, + )); + try testing.expect(!rectIntersects( + .{ .left = 0, .top = 0, .right = 10, .bottom = 10 }, + .{ .left = 0, .top = 10, .right = 10, .bottom = 20 }, + )); +} + +test "win32 GDI text length accepts empty sentinel slices" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const alloc = std.testing.allocator; + const empty = try std.unicode.utf8ToUtf16LeAllocZ(alloc, ""); + defer alloc.free(empty); + try std.testing.expectEqual(@as(i32, 0), utf16GdiTextLen(empty)); + + const confirm = try std.unicode.utf8ToUtf16LeAllocZ(alloc, "Confirm"); + defer alloc.free(confirm); + try std.testing.expectEqual(@as(i32, 7), utf16GdiTextLen(confirm)); +} diff --git a/src/apprt/win32/gl_startup.zig b/src/apprt/win32/gl_startup.zig new file mode 100644 index 00000000000..32acabb7412 --- /dev/null +++ b/src/apprt/win32/gl_startup.zig @@ -0,0 +1,309 @@ +//! Win32 OpenGL startup diagnostics. + +const std = @import("std"); +const builtin = @import("builtin"); + +const build_config = @import("../../build_config.zig"); +const win32_types = @import("../win32_types.zig"); +const c = @import("consts.zig"); +const sys = @import("sys.zig"); + +const BOOL = win32_types.BOOL; +const DWORD = win32_types.DWORD; +const HMODULE = win32_types.HMODULE; +const HWND = win32_types.HWND; +const LPCWSTR = win32_types.LPCWSTR; +const UINT = win32_types.UINT; + +const log = std.log.scoped(.win32); +const windows = std.os.windows; + +pub var opengl32_module: HMODULE = null; + +pub const StartupLoaderErrorDialogSuppression = struct { + previous_mode: DWORD = 0, + active: bool = false, + + pub fn restore(self: *StartupLoaderErrorDialogSuppression) void { + if (!self.active) return; + if (sys.SetThreadErrorMode(self.previous_mode, null) == 0) { + log.warn( + "failed to restore startup loader thread error mode win32_error={d}", + .{@intFromEnum(windows.kernel32.GetLastError())}, + ); + } + self.active = false; + } +}; + +pub fn suppressStartupLoaderErrorDialogs() StartupLoaderErrorDialogSuppression { + // Let WinMain report renderer startup failures with app-specific guidance + // instead of letting LoadLibrary/WGL surface generic Windows dialogs first. + var previous_mode: DWORD = 0; + if (sys.SetThreadErrorMode(c.SEM_FAILCRITICALERRORS | c.SEM_NOOPENFILEERRORBOX, &previous_mode) == 0) { + log.warn( + "failed to suppress startup loader error dialogs win32_error={d}", + .{@intFromEnum(windows.kernel32.GetLastError())}, + ); + return .{}; + } + return .{ .previous_mode = previous_mode, .active = true }; +} + +pub const OpenGLStartupStep = enum { + get_dc, + choose_pixel_format, + set_pixel_format, + create_context, + initial_make_current, + load_opengl32, + make_current, + load_functions, + version_check, + framebuffer_srgb, + + fn label(self: OpenGLStartupStep) []const u8 { + return switch (self) { + .get_dc => "acquiring the window device context", + .choose_pixel_format => "choosing a WGL pixel format", + .set_pixel_format => "setting the WGL pixel format", + .create_context => "creating the WGL context", + .initial_make_current => "making the initial WGL context current", + .load_opengl32 => "loading opengl32.dll", + .make_current => "making the WGL context current", + .load_functions => "loading OpenGL functions", + .version_check => "checking the OpenGL version", + .framebuffer_srgb => "enabling OpenGL sRGB framebuffer support", + }; + } +}; + +const OpenGLStartupFailure = struct { + step: OpenGLStartupStep, + win32_error: ?DWORD = null, + zig_error_name: ?[]const u8 = null, +}; + +var opengl_startup_diagnostics_mutex: std.Thread.Mutex = .{}; +var opengl_startup_diagnostics_active = false; +var last_opengl_startup_failure: ?OpenGLStartupFailure = null; + +pub fn beginOpenGLStartupDiagnostics() void { + opengl_startup_diagnostics_mutex.lock(); + defer opengl_startup_diagnostics_mutex.unlock(); + + opengl_startup_diagnostics_active = true; + last_opengl_startup_failure = null; +} + +pub fn clearOpenGLStartupFailure() void { + opengl_startup_diagnostics_mutex.lock(); + defer opengl_startup_diagnostics_mutex.unlock(); + + opengl_startup_diagnostics_active = false; + last_opengl_startup_failure = null; +} + +fn recordOpenGLStartupFailure(failure: OpenGLStartupFailure) bool { + opengl_startup_diagnostics_mutex.lock(); + defer opengl_startup_diagnostics_mutex.unlock(); + + if (!opengl_startup_diagnostics_active) return false; + if (last_opengl_startup_failure) |previous| { + if (previous.win32_error != null and failure.win32_error == null) return false; + } + + last_opengl_startup_failure = failure; + return true; +} + +fn openGLStartupDiagnosticsActive() bool { + opengl_startup_diagnostics_mutex.lock(); + defer opengl_startup_diagnostics_mutex.unlock(); + + return opengl_startup_diagnostics_active; +} + +fn currentOpenGLStartupFailure() ?OpenGLStartupFailure { + opengl_startup_diagnostics_mutex.lock(); + defer opengl_startup_diagnostics_mutex.unlock(); + + return last_opengl_startup_failure; +} + +pub fn recordOpenGLStartupWin32Failure(step: OpenGLStartupStep, win32_error: windows.Win32Error) void { + const code: DWORD = @intFromEnum(win32_error); + if (!recordOpenGLStartupFailure(.{ + .step = step, + .win32_error = code, + })) return; + + log.err( + "Win32 OpenGL startup failed step={s} win32_error={d}", + .{ step.label(), code }, + ); +} + +pub fn recordOpenGLStartupError(step: OpenGLStartupStep, err: anyerror) void { + if (!recordOpenGLStartupFailure(.{ + .step = step, + .zig_error_name = @errorName(err), + })) return; + + log.err("Win32 OpenGL startup failed step={s} error={s}", .{ step.label(), @errorName(err) }); +} + +pub fn reportStartupFailure(err: anyerror) void { + if (comptime builtin.os.tag != .windows) return; + + var buf: [4096]u8 = undefined; + const message = formatStartupFailureMessage(&buf, err); + + const caption = std.unicode.utf8ToUtf16LeStringLiteral("noctty failed"); + const fallback = std.unicode.utf8ToUtf16LeStringLiteral("noctty failed."); + + const message_w = std.unicode.utf8ToUtf16LeAllocZ(std.heap.page_allocator, message) catch { + _ = sys.MessageBoxW(null, fallback, caption, c.MB_OK | c.MB_ICONERROR | c.MB_SETFOREGROUND); + return; + }; + defer std.heap.page_allocator.free(message_w); + + _ = sys.MessageBoxW(null, message_w, caption, c.MB_OK | c.MB_ICONERROR | c.MB_SETFOREGROUND); +} + +fn formatStartupFailureMessage(buf: []u8, err: anyerror) []const u8 { + if (currentOpenGLStartupFailure()) |failure| { + return formatOpenGLStartupFailureMessage(buf, err, failure) catch + "noctty could not initialize the Windows OpenGL renderer."; + } + + return std.fmt.bufPrint( + buf, + "noctty {s} failed: {s}\n\nOpen an issue with the full log if this keeps happening.", + .{ build_config.version_string, @errorName(err) }, + ) catch "noctty failed."; +} + +fn formatOpenGLStartupFailureMessage(buf: []u8, err: anyerror, failure: OpenGLStartupFailure) ![]const u8 { + const zig_error_name = failure.zig_error_name orelse @errorName(err); + + if (failure.win32_error) |win32_error| { + return std.fmt.bufPrint(buf, + \\noctty {s} could not initialize the Windows OpenGL renderer while {s}. + \\ + \\Startup error: {s} + \\Win32 error: {d}{s} + \\ + \\noctty currently uses OpenGL 4.3 through WGL on Windows. This build does not include a DirectX or ANGLE fallback renderer. + \\ + \\{s} + \\ + \\Try updating or reinstalling the OEM AMD graphics driver, then the NVIDIA driver. You can also force noctty.exe to the discrete or integrated GPU in Windows Graphics settings. If it still fails, attach this text and the log to https://github.com/amanthanvi/noctty/issues/64. + , .{ + build_config.version_string, + failure.step.label(), + zig_error_name, + win32_error, + win32ErrorSuffix(win32_error), + openglStartupFailureHint(failure), + }); + } + + return std.fmt.bufPrint(buf, + \\noctty {s} could not initialize the Windows OpenGL renderer while {s}. + \\ + \\Startup error: {s} + \\Win32 error: not reported + \\ + \\noctty currently uses OpenGL 4.3 through WGL on Windows. This build does not include a DirectX or ANGLE fallback renderer. + \\ + \\{s} + \\ + \\Try updating or reinstalling the OEM AMD graphics driver, then the NVIDIA driver. You can also force noctty.exe to the discrete or integrated GPU in Windows Graphics settings. If it still fails, attach this text and the log to https://github.com/amanthanvi/noctty/issues/64. + , .{ + build_config.version_string, + failure.step.label(), + zig_error_name, + openglStartupFailureHint(failure), + }); +} + +fn win32ErrorSuffix(code: DWORD) []const u8 { + return switch (code) { + c.ERROR_MOD_NOT_FOUND => " (ERROR_MOD_NOT_FOUND)", + else => "", + }; +} + +fn openglStartupFailureHint(failure: OpenGLStartupFailure) []const u8 { + if (failure.win32_error) |code| { + if (code == c.ERROR_MOD_NOT_FOUND) { + return "Win32 error 126 means Windows could not load a graphics-driver DLL or one of its dependent DLLs. On AMD+NVIDIA hybrid GPU laptops, this can happen while WGL loads the AMD OpenGL ICD from DriverStore."; + } + } + + if (failure.step == .version_check) { + return "The active GPU driver did not expose the required OpenGL 4.3 feature level."; + } + + return "This is usually caused by an unavailable or incompatible OpenGL driver, a stale GPU driver installation, or missing OpenGL 4.3 support."; +} + +test "win32-opengl-startup-failure-message-explains-error-126" { + var buf: [4096]u8 = undefined; + const message = try formatOpenGLStartupFailureMessage(&buf, error.Unexpected, .{ + .step = .create_context, + .win32_error = c.ERROR_MOD_NOT_FOUND, + .zig_error_name = "Unexpected", + }); + + try std.testing.expect(std.mem.indexOf(u8, message, "Win32 error: 126 (ERROR_MOD_NOT_FOUND)") != null); + try std.testing.expect(std.mem.indexOf(u8, message, "AMD+NVIDIA hybrid GPU") != null); + try std.testing.expect(std.mem.indexOf(u8, message, "DirectX or ANGLE fallback") != null); + try std.testing.expect(std.mem.indexOf(u8, message, "https://github.com/amanthanvi/noctty/issues/64") != null); +} + +test "win32-opengl-startup-failure-message-explains-version-floor" { + var buf: [4096]u8 = undefined; + const message = try formatOpenGLStartupFailureMessage(&buf, error.OpenGLOutdated, .{ + .step = .version_check, + .zig_error_name = "OpenGLOutdated", + }); + + try std.testing.expect(std.mem.indexOf(u8, message, "OpenGL 4.3 through WGL") != null); + try std.testing.expect(std.mem.indexOf(u8, message, "required OpenGL 4.3 feature level") != null); + try std.testing.expect(std.mem.indexOf(u8, message, "Win32 error: not reported") != null); +} + +test "win32-opengl-startup-failure-recording-is-startup-scoped" { + clearOpenGLStartupFailure(); + recordOpenGLStartupError(.make_current, error.Unexpected); + try std.testing.expect(currentOpenGLStartupFailure() == null); + + beginOpenGLStartupDiagnostics(); + try std.testing.expect(openGLStartupDiagnosticsActive()); + + clearOpenGLStartupFailure(); + try std.testing.expect(!openGLStartupDiagnosticsActive()); + try std.testing.expect(currentOpenGLStartupFailure() == null); +} + +test "win32-opengl-startup-failure-preserves-win32-loader-cause" { + clearOpenGLStartupFailure(); + beginOpenGLStartupDiagnostics(); + + try std.testing.expect(recordOpenGLStartupFailure(.{ + .step = .load_opengl32, + .win32_error = c.ERROR_MOD_NOT_FOUND, + })); + try std.testing.expect(!recordOpenGLStartupFailure(.{ + .step = .load_functions, + .zig_error_name = "OpenGLFunctionLoadFailed", + })); + + const failure = currentOpenGLStartupFailure().?; + try std.testing.expectEqual(OpenGLStartupStep.load_opengl32, failure.step); + try std.testing.expectEqual(@as(?DWORD, c.ERROR_MOD_NOT_FOUND), failure.win32_error); + + clearOpenGLStartupFailure(); +} diff --git a/src/apprt/win32/input.zig b/src/apprt/win32/input.zig new file mode 100644 index 00000000000..b94be62f745 --- /dev/null +++ b/src/apprt/win32/input.zig @@ -0,0 +1,1042 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const apprt = @import("../../apprt.zig"); +const CoreSurface = @import("../../Surface.zig"); +const input = @import("../../input.zig"); + +const sys = @import("sys.zig"); +const c = @import("consts.zig"); + +const windows = std.os.windows; +const UINT = sys.UINT; +const WPARAM = sys.WPARAM; +const LPARAM = sys.LPARAM; + +pub const SystemWheelSettings = struct { + lines: u32 = 3, + chars: u32 = 3, +}; + +const MouseWheelAxis = enum { + horizontal, + vertical, +}; + +pub const WheelNormalizationContext = struct { + settings: SystemWheelSettings = .{}, + cell_size: apprt.action.CellSize = .{ .width = 0, .height = 0 }, + viewport: apprt.SurfaceSize = .{ .width = 0, .height = 0 }, +}; + +pub const NormalizedWheelScroll = struct { + xoff: f64 = 0, + yoff: f64 = 0, + mods: input.ScrollMods = .{}, +}; + +pub const GlobalHotkeySpec = struct { + modifiers: UINT, + vk: UINT, +}; + +pub const RegisteredGlobalHotkey = struct { + id: i32, + trigger: input.Binding.Trigger, + spec: GlobalHotkeySpec, + binding: *const input.Binding.Set.Value, +}; + +const KeyText = struct { + utf8: [8]u8 = [_]u8{0} ** 8, + len: usize = 0, + consumed_mods: input.Mods = .{}, + unshifted_codepoint: u21 = 0, + deferred_utf16_units: usize = 0, +}; + +pub const DeferredCharState = struct { + pending_units: usize = 0, + high_surrogate: ?u16 = null, + + pub fn authorize(self: *DeferredCharState, expected_units: usize) void { + self.pending_units = self.pending_units +| expected_units; + } + + pub fn clear(self: *DeferredCharState) void { + self.* = .{}; + } + + pub fn consumeDeadChar(self: *DeferredCharState) void { + if (self.pending_units > 0) self.pending_units -= 1; + self.high_surrogate = null; + } + + pub fn consumeCodeUnit( + self: *DeferredCharState, + code_unit: u16, + ime_composing: bool, + ) ?u21 { + if (ime_composing) { + self.clear(); + return null; + } + if (self.pending_units == 0) { + self.high_surrogate = null; + return null; + } + self.pending_units -= 1; + + if (self.high_surrogate) |high| { + if (!std.unicode.utf16IsLowSurrogate(code_unit)) { + self.clear(); + return null; + } + const codepoint = std.unicode.utf16DecodeSurrogatePair( + &.{ high, code_unit }, + ) catch { + self.clear(); + return null; + }; + self.high_surrogate = null; + return codepoint; + } + + if (std.unicode.utf16IsHighSurrogate(code_unit)) { + self.high_surrogate = code_unit; + return null; + } + if (std.unicode.utf16IsLowSurrogate(code_unit)) { + self.clear(); + return null; + } + + return code_unit; + } +}; + +const Win32KeyMessage = struct { + event: input.KeyEvent, + deferred_utf16_units: usize = 0, +}; + +pub fn lParamBits(lParam: LPARAM) usize { + return @as(usize, @bitCast(lParam)); +} + +pub fn highWord(value: usize) u16 { + return @truncate((value >> 16) & 0xFFFF); +} + +fn scanCodeFromLParam(lParam: LPARAM) u32 { + return @as(u32, highWord(lParamBits(lParam))) & 0xFF; +} + +fn isExtendedKey(lParam: LPARAM) bool { + return (lParamBits(lParam) & c.KF_EXTENDED) != 0; +} + +fn isRepeatedKey(lParam: LPARAM) bool { + return (lParamBits(lParam) & c.KF_REPEAT) != 0; +} + +pub fn readSystemWheelSetting(action: UINT, fallback: u32) u32 { + var value: UINT = fallback; + if (sys.SystemParametersInfoW(action, 0, @ptrCast(&value), 0) == 0) { + return fallback; + } + return value; +} + +pub fn wheelDeltaFromWParam(wParam: WPARAM) i16 { + const bits = @as(usize, @intCast(wParam)); + return @bitCast(highWord(bits)); +} + +fn wheelSettingForAxis(settings: SystemWheelSettings, axis: MouseWheelAxis) u32 { + return switch (axis) { + .vertical => settings.lines, + .horizontal => settings.chars, + }; +} + +fn wheelUnitSize(ctx: WheelNormalizationContext, axis: MouseWheelAxis) f64 { + const dim: u32 = switch (axis) { + .vertical => ctx.cell_size.height, + .horizontal => ctx.cell_size.width, + }; + return @floatFromInt(@max(dim, 1)); +} + +fn wheelViewportSize(ctx: WheelNormalizationContext, axis: MouseWheelAxis) f64 { + const dim: u32 = switch (axis) { + .vertical => ctx.viewport.height, + .horizontal => ctx.viewport.width, + }; + const viewport: f64 = @floatFromInt(@max(dim, 1)); + const unit = wheelUnitSize(ctx, axis); + return @max(unit, viewport - unit); +} + +pub fn normalizeWheelDelta( + ctx: WheelNormalizationContext, + axis: MouseWheelAxis, + delta: i16, +) NormalizedWheelScroll { + if (delta == 0) return .{}; + + const precision = @rem(delta, c.WHEEL_DELTA) != 0; + const notch_delta = @as(f64, @floatFromInt(delta)) / c.WHEEL_DELTA; + const pixels = if (precision) + notch_delta * wheelUnitSize(ctx, axis) + else discrete: { + const setting = wheelSettingForAxis(ctx.settings, axis); + if (setting == 0) return .{}; + + break :discrete if (setting == c.WHEEL_PAGESCROLL) + notch_delta * wheelViewportSize(ctx, axis) + else + notch_delta * @as(f64, @floatFromInt(setting)) * wheelUnitSize(ctx, axis); + }; + + return switch (axis) { + .vertical => .{ + .yoff = pixels, + .mods = .{ + .precision = precision, + .pixel_delta = true, + }, + }, + .horizontal => .{ + .xoff = pixels, + .mods = .{ + .precision = precision, + .pixel_delta = true, + }, + }, + }; +} + +pub fn keyPressed(vk: i32) bool { + return sys.GetKeyState(vk) < 0; +} + +fn keyToggled(vk: i32) bool { + return (sys.GetKeyState(vk) & 1) != 0; +} + +fn modsFromKeyboardState(state: *const [256]u8) input.Mods { + const pressed = struct { + fn check(state_: *const [256]u8, vk: usize) bool { + return (state_[vk] & 0x80) != 0; + } + }; + + return .{ + .shift = pressed.check(state, c.VK_SHIFT), + .ctrl = pressed.check(state, c.VK_CONTROL), + .alt = pressed.check(state, c.VK_MENU), + .super = pressed.check(state, c.VK_LWIN) or pressed.check(state, c.VK_RWIN), + .caps_lock = (state[c.VK_CAPITAL] & 1) != 0, + .num_lock = (state[c.VK_NUMLOCK] & 1) != 0, + .sides = .{ + .shift = if (pressed.check(state, c.VK_RSHIFT)) .right else .left, + .ctrl = if (pressed.check(state, c.VK_RCONTROL)) .right else .left, + .alt = if (pressed.check(state, c.VK_RMENU)) .right else .left, + .super = if (pressed.check(state, c.VK_RWIN)) .right else .left, + }, + }; +} + +fn fallbackMods() input.Mods { + return .{ + .shift = keyPressed(c.VK_SHIFT), + .ctrl = keyPressed(c.VK_CONTROL), + .alt = keyPressed(c.VK_MENU), + .super = keyPressed(c.VK_LWIN) or keyPressed(c.VK_RWIN), + .caps_lock = keyToggled(c.VK_CAPITAL), + .num_lock = keyToggled(c.VK_NUMLOCK), + .sides = .{ + .shift = if (keyPressed(c.VK_RSHIFT)) .right else .left, + .ctrl = if (keyPressed(c.VK_RCONTROL)) .right else .left, + .alt = if (keyPressed(c.VK_RMENU)) .right else .left, + .super = if (keyPressed(c.VK_RWIN)) .right else .left, + }, + }; +} + +fn currentKeyboardState(state: *[256]u8) ?*const [256]u8 { + if (sys.GetKeyboardState(state) == 0) return null; + return state; +} + +fn currentModsFromKeyboardState(state: ?*const [256]u8) input.Mods { + return if (state) |keyboard_state| modsFromKeyboardState(keyboard_state) else fallbackMods(); +} + +pub fn currentMods() input.Mods { + var state: [256]u8 = [_]u8{0} ** 256; + return currentModsFromKeyboardState(currentKeyboardState(&state)); +} + +fn keyFromVirtualKey(vk: UINT, lParam: LPARAM) input.Key { + return switch (vk) { + c.VK_BACK => .backspace, + c.VK_TAB => .tab, + c.VK_RETURN => if (isExtendedKey(lParam)) .numpad_enter else .enter, + c.VK_SHIFT => if (scanCodeFromLParam(lParam) == 0x36) .shift_right else .shift_left, + c.VK_LSHIFT => .shift_left, + c.VK_RSHIFT => .shift_right, + c.VK_CONTROL, c.VK_LCONTROL => if (isExtendedKey(lParam) or vk == c.VK_RCONTROL) .control_right else .control_left, + c.VK_RCONTROL => .control_right, + c.VK_MENU, c.VK_LMENU => if (isExtendedKey(lParam) or vk == c.VK_RMENU) .alt_right else .alt_left, + c.VK_RMENU => .alt_right, + c.VK_PAUSE => .pause, + c.VK_CAPITAL => .caps_lock, + c.VK_ESCAPE => .escape, + c.VK_SPACE => .space, + c.VK_PRIOR => .page_up, + c.VK_NEXT => .page_down, + c.VK_END => .end, + c.VK_HOME => .home, + c.VK_LEFT => .arrow_left, + c.VK_UP => .arrow_up, + c.VK_RIGHT => .arrow_right, + c.VK_DOWN => .arrow_down, + c.VK_SNAPSHOT => .print_screen, + c.VK_INSERT => .insert, + c.VK_DELETE => .delete, + c.VK_LWIN => .meta_left, + c.VK_RWIN => .meta_right, + c.VK_APPS => .context_menu, + c.VK_MULTIPLY => .numpad_multiply, + c.VK_ADD => .numpad_add, + c.VK_SEPARATOR => .numpad_separator, + c.VK_SUBTRACT => .numpad_subtract, + c.VK_DECIMAL => .numpad_decimal, + c.VK_DIVIDE => .numpad_divide, + c.VK_NUMLOCK => .num_lock, + c.VK_SCROLL => .scroll_lock, + c.VK_OEM_1 => .semicolon, + c.VK_OEM_PLUS => .equal, + c.VK_OEM_COMMA => .comma, + c.VK_OEM_MINUS => .minus, + c.VK_OEM_PERIOD => .period, + c.VK_OEM_2 => .slash, + c.VK_OEM_3 => .backquote, + c.VK_OEM_4 => .bracket_left, + c.VK_OEM_5 => .backslash, + c.VK_OEM_6 => .bracket_right, + c.VK_OEM_7 => .quote, + c.VK_0...c.VK_9 => input.Key.fromASCII(@as(u8, @intCast('0' + (vk - c.VK_0)))) orelse .unidentified, + c.VK_A...c.VK_Z => input.Key.fromASCII(@as(u8, @intCast('a' + (vk - c.VK_A)))) orelse .unidentified, + c.VK_NUMPAD0...c.VK_NUMPAD9 => @enumFromInt( + @intFromEnum(input.Key.numpad_0) + @as(c_int, @intCast(vk - c.VK_NUMPAD0)), + ), + c.VK_F1...c.VK_F24 => @enumFromInt( + @intFromEnum(input.Key.f1) + @as(c_int, @intCast(vk - c.VK_F1)), + ), + else => .unidentified, + }; +} + +fn unshiftedCodepointForVirtualKey(vk: UINT) u21 { + return switch (vk) { + c.VK_0...c.VK_9 => @as(u21, @intCast('0' + (vk - c.VK_0))), + c.VK_A...c.VK_Z => @as(u21, @intCast('a' + (vk - c.VK_A))), + c.VK_SPACE => ' ', + c.VK_OEM_1 => ';', + c.VK_OEM_PLUS => '=', + c.VK_OEM_COMMA => ',', + c.VK_OEM_MINUS => '-', + c.VK_OEM_PERIOD => '.', + c.VK_OEM_2 => '/', + c.VK_OEM_3 => '`', + c.VK_OEM_4 => '[', + c.VK_OEM_5 => '\\', + c.VK_OEM_6 => ']', + c.VK_OEM_7 => '\'', + else => 0, + }; +} + +fn hotkeyModifiers(mods: input.Mods) UINT { + var result: UINT = 0; + if (mods.alt) result |= c.MOD_ALT; + if (mods.ctrl) result |= c.MOD_CONTROL; + if (mods.shift) result |= c.MOD_SHIFT; + if (mods.super) result |= c.MOD_WIN; + return result; +} + +fn hotkeyPhysicalVirtualKey(key: input.Key) ?UINT { + const key_int = @intFromEnum(key); + if (key_int >= @intFromEnum(input.Key.key_a) and key_int <= @intFromEnum(input.Key.key_z)) { + return c.VK_A + @as(UINT, @intCast(key_int - @intFromEnum(input.Key.key_a))); + } + if (key_int >= @intFromEnum(input.Key.digit_0) and key_int <= @intFromEnum(input.Key.digit_9)) { + return c.VK_0 + @as(UINT, @intCast(key_int - @intFromEnum(input.Key.digit_0))); + } + if (key_int >= @intFromEnum(input.Key.numpad_0) and key_int <= @intFromEnum(input.Key.numpad_9)) { + return c.VK_NUMPAD0 + @as(UINT, @intCast(key_int - @intFromEnum(input.Key.numpad_0))); + } + if (key_int >= @intFromEnum(input.Key.f1) and key_int <= @intFromEnum(input.Key.f24)) { + return c.VK_F1 + @as(UINT, @intCast(key_int - @intFromEnum(input.Key.f1))); + } + + return switch (key) { + .backspace => c.VK_BACK, + .tab => c.VK_TAB, + .enter, .numpad_enter => c.VK_RETURN, + .escape => c.VK_ESCAPE, + .space => c.VK_SPACE, + .page_up, .numpad_page_up => c.VK_PRIOR, + .page_down, .numpad_page_down => c.VK_NEXT, + .end, .numpad_end => c.VK_END, + .home, .numpad_home => c.VK_HOME, + .arrow_left, .numpad_left => c.VK_LEFT, + .arrow_up, .numpad_up => c.VK_UP, + .arrow_right, .numpad_right => c.VK_RIGHT, + .arrow_down, .numpad_down => c.VK_DOWN, + .print_screen => c.VK_SNAPSHOT, + .insert, .numpad_insert => c.VK_INSERT, + .delete, .numpad_delete => c.VK_DELETE, + .meta_left => c.VK_LWIN, + .meta_right => c.VK_RWIN, + .context_menu => c.VK_APPS, + .numpad_multiply => c.VK_MULTIPLY, + .numpad_add => c.VK_ADD, + .numpad_separator => c.VK_SEPARATOR, + .numpad_subtract => c.VK_SUBTRACT, + .numpad_decimal => c.VK_DECIMAL, + .numpad_divide => c.VK_DIVIDE, + .num_lock => c.VK_NUMLOCK, + .scroll_lock => c.VK_SCROLL, + .semicolon => c.VK_OEM_1, + .equal => c.VK_OEM_PLUS, + .comma => c.VK_OEM_COMMA, + .minus => c.VK_OEM_MINUS, + .period => c.VK_OEM_PERIOD, + .slash => c.VK_OEM_2, + .backquote => c.VK_OEM_3, + .bracket_left => c.VK_OEM_4, + .backslash => c.VK_OEM_5, + .bracket_right => c.VK_OEM_6, + .quote => c.VK_OEM_7, + else => null, + }; +} + +fn hotkeyUnicodeVirtualKey(cp: u21) ?struct { vk: UINT, shift: bool } { + return switch (cp) { + 'a'...'z' => .{ .vk = c.VK_A + @as(UINT, @intCast(cp - 'a')), .shift = false }, + 'A'...'Z' => .{ .vk = c.VK_A + @as(UINT, @intCast(cp - 'A')), .shift = true }, + '0'...'9' => .{ .vk = c.VK_0 + @as(UINT, @intCast(cp - '0')), .shift = false }, + ')' => .{ .vk = c.VK_0, .shift = true }, + '!' => .{ .vk = c.VK_0 + 1, .shift = true }, + '@' => .{ .vk = c.VK_0 + 2, .shift = true }, + '#' => .{ .vk = c.VK_0 + 3, .shift = true }, + '$' => .{ .vk = c.VK_0 + 4, .shift = true }, + '%' => .{ .vk = c.VK_0 + 5, .shift = true }, + '^' => .{ .vk = c.VK_0 + 6, .shift = true }, + '&' => .{ .vk = c.VK_0 + 7, .shift = true }, + '*' => .{ .vk = c.VK_0 + 8, .shift = true }, + '(' => .{ .vk = c.VK_0 + 9, .shift = true }, + ' ' => .{ .vk = c.VK_SPACE, .shift = false }, + ';' => .{ .vk = c.VK_OEM_1, .shift = false }, + ':' => .{ .vk = c.VK_OEM_1, .shift = true }, + '=' => .{ .vk = c.VK_OEM_PLUS, .shift = false }, + '+' => .{ .vk = c.VK_OEM_PLUS, .shift = true }, + ',' => .{ .vk = c.VK_OEM_COMMA, .shift = false }, + '<' => .{ .vk = c.VK_OEM_COMMA, .shift = true }, + '-' => .{ .vk = c.VK_OEM_MINUS, .shift = false }, + '_' => .{ .vk = c.VK_OEM_MINUS, .shift = true }, + '.' => .{ .vk = c.VK_OEM_PERIOD, .shift = false }, + '>' => .{ .vk = c.VK_OEM_PERIOD, .shift = true }, + '/' => .{ .vk = c.VK_OEM_2, .shift = false }, + '?' => .{ .vk = c.VK_OEM_2, .shift = true }, + '`' => .{ .vk = c.VK_OEM_3, .shift = false }, + '~' => .{ .vk = c.VK_OEM_3, .shift = true }, + '[' => .{ .vk = c.VK_OEM_4, .shift = false }, + '{' => .{ .vk = c.VK_OEM_4, .shift = true }, + '\\' => .{ .vk = c.VK_OEM_5, .shift = false }, + '|' => .{ .vk = c.VK_OEM_5, .shift = true }, + ']' => .{ .vk = c.VK_OEM_6, .shift = false }, + '}' => .{ .vk = c.VK_OEM_6, .shift = true }, + '\'' => .{ .vk = c.VK_OEM_7, .shift = false }, + '"' => .{ .vk = c.VK_OEM_7, .shift = true }, + else => null, + }; +} + +pub fn hotkeySpecForTrigger(trigger: input.Binding.Trigger) ?GlobalHotkeySpec { + var mods = trigger.mods.binding(); + const vk = switch (trigger.key) { + .catch_all => return null, + .physical => |key| hotkeyPhysicalVirtualKey(key) orelse return null, + .unicode => |cp| unicode: { + const mapped = hotkeyUnicodeVirtualKey(cp) orelse return null; + if (mapped.shift) mods.shift = true; + break :unicode mapped.vk; + }, + }; + + return .{ + .modifiers = hotkeyModifiers(mods), + .vk = vk, + }; +} + +pub fn hotkeySpecEql(a: GlobalHotkeySpec, b: GlobalHotkeySpec) bool { + return a.modifiers == b.modifiers and a.vk == b.vk; +} + +pub fn hotkeyRegistrationFailureReason(err: windows.Win32Error) []const u8 { + return switch (err) { + .HOTKEY_ALREADY_REGISTERED => "already registered by another app or another noctty instance", + .ACCESS_DENIED => "access denied; hotkey may be reserved, occupied by an elevated app, or blocked by policy", + .INVALID_PARAMETER => "invalid modifier or virtual-key combination", + else => "unknown Win32 RegisterHotKey failure", + }; +} + +fn isControlCodepoint(codepoint: u21) bool { + return codepoint < 0x20 or codepoint == 0x7F; +} + +fn utf16CodeUnitCount(codepoint: u21) usize { + if (codepoint == 0) return 0; + return if (codepoint <= std.math.maxInt(u16)) 1 else 2; +} + +fn shouldDeferTextToCharMessage( + action: input.Action, + key: input.Key, + mods: input.Mods, + translated: KeyText, +) bool { + if (action == .release) return false; + // Windows reports AltGr as synthetic left Ctrl plus right Alt. The + // resulting WM_CHAR is layout text, not a Ctrl+Alt terminal chord. + const alt_gr = mods.ctrl and mods.alt and + mods.sides.ctrl == .left and mods.sides.alt == .right; + if (mods.super or ((mods.ctrl or mods.alt) and !alt_gr)) return false; + if (key.modifier()) return false; + + switch (key) { + .enter, .backspace, .tab, .escape => return false, + else => {}, + } + + if (translated.deferred_utf16_units == 0) return false; + if (translated.len > 0) return true; + if (translated.unshifted_codepoint == 0) return true; + return !isControlCodepoint(translated.unshifted_codepoint); +} + +pub fn shouldAuthorizeDeferredCharMessage(effect: CoreSurface.InputEffect) bool { + return effect == .ignored; +} + +pub fn charCommitEvent( + codepoint: u21, + lParam: LPARAM, + utf8_buf: *[8]u8, +) ?input.KeyEvent { + const utf8_len = std.unicode.utf8Encode(codepoint, utf8_buf) catch return null; + return .{ + .action = if (isRepeatedKey(lParam)) .repeat else .press, + .key = .unidentified, + .mods = .{}, + .unshifted_codepoint = codepoint, + .utf8 = utf8_buf[0..utf8_len], + }; +} + +fn translateKeyTextToUnicode( + vk: UINT, + scan_code: UINT, + state: *const [256]u8, + utf16: *[4]u16, +) i32 { + // TranslateMessage owns the stateful dead-key composition path. This + // helper only probes text metadata for key events, so it must not consume + // or reset the layout's pending dead key. + return sys.ToUnicode(vk, scan_code, state, utf16, utf16.len, c.TO_UNICODE_NO_STATE_CHANGE); +} + +fn translateKeyText( + vk: UINT, + lParam: LPARAM, + mods: input.Mods, + keyboard_state: ?*const [256]u8, +) KeyText { + const state = keyboard_state orelse { + const unshifted = unshiftedCodepointForVirtualKey(vk); + return .{ + .unshifted_codepoint = unshifted, + .deferred_utf16_units = utf16CodeUnitCount(unshifted), + }; + }; + + var utf16: [4]u16 = [_]u16{0} ** 4; + const count = translateKeyTextToUnicode(vk, scanCodeFromLParam(lParam), state, &utf16); + if (count < 0) { + return .{ + .unshifted_codepoint = unshiftedCodepointForVirtualKey(vk), + .deferred_utf16_units = 1, + }; + } + if (count == 0) { + return .{ .unshifted_codepoint = unshiftedCodepointForVirtualKey(vk) }; + } + + var result: KeyText = .{ + .unshifted_codepoint = unshiftedCodepointForVirtualKey(vk), + .deferred_utf16_units = @intCast(count), + }; + + const codepoint: u21 = cp: { + if (count >= 2 and std.unicode.utf16IsHighSurrogate(utf16[0]) and std.unicode.utf16IsLowSurrogate(utf16[1])) { + break :cp std.unicode.utf16DecodeSurrogatePair(&.{ utf16[0], utf16[1] }) catch return result; + } + if (utf16[0] < 0x20 or utf16[0] == 0x7F) return result; + break :cp utf16[0]; + }; + + result.len = std.unicode.utf8Encode(codepoint, &result.utf8) catch 0; + if (result.len > 0) { + result.consumed_mods = .{ + .shift = mods.shift, + }; + } + + return result; +} + +fn packetKeyMessage(action: input.Action) Win32KeyMessage { + const commit_pending = action != .release; + return .{ + .event = .{ + .action = action, + .key = .unidentified, + .mods = .{}, + .consumed_mods = .{}, + .unshifted_codepoint = 0, + .utf8 = "", + .composing = commit_pending, + }, + .deferred_utf16_units = if (commit_pending) 1 else 0, + }; +} + +pub fn keyEventFromWin32Message( + msg: UINT, + wParam: WPARAM, + lParam: LPARAM, +) ?Win32KeyMessage { + const action: input.Action = switch (msg) { + c.WM_KEYUP, c.WM_SYSKEYUP => .release, + c.WM_KEYDOWN, c.WM_SYSKEYDOWN => if (isRepeatedKey(lParam)) .repeat else .press, + else => return null, + }; + + const vk: UINT = @intCast(wParam & 0xFFFF); + // KEYEVENTF_UNICODE arrives as VK_PACKET followed by one WM_CHAR UTF-16 + // code unit. Authorize that unit explicitly without consulting live + // keyboard modifiers or ToUnicode; both belong to physical-key handling. + if (vk == c.VK_PACKET) return packetKeyMessage(action); + + const key = keyFromVirtualKey(vk, lParam); + var keyboard_state_storage: [256]u8 = [_]u8{0} ** 256; + const keyboard_state = currentKeyboardState(&keyboard_state_storage); + const mods = currentModsFromKeyboardState(keyboard_state); + + var result: Win32KeyMessage = .{ .event = .{ + .action = action, + .key = key, + .mods = mods, + .unshifted_codepoint = unshiftedCodepointForVirtualKey(vk), + } }; + + if (action != .release) { + const translated = translateKeyText(vk, lParam, mods, keyboard_state); + result.event.utf8 = translated.utf8[0..translated.len]; + result.event.consumed_mods = translated.consumed_mods; + if (translated.unshifted_codepoint != 0) { + result.event.unshifted_codepoint = translated.unshifted_codepoint; + } + if (shouldDeferTextToCharMessage(action, key, mods, translated)) { + // Keep the physical-key event visible to bindings/modifier state + // but defer text emission to WM_CHAR so plain typing doesn't rely + // on ToUnicode/GetKeyboardState timing. + result.event.utf8 = ""; + result.event.consumed_mods = .{}; + result.event.composing = true; + result.deferred_utf16_units = translated.deferred_utf16_units; + } + } + + return result; +} + +test "win32 keyFromVirtualKey maps core keys" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(input.Key.key_a, keyFromVirtualKey(c.VK_A, 0)); + try std.testing.expectEqual(input.Key.enter, keyFromVirtualKey(c.VK_RETURN, 0)); + try std.testing.expectEqual(input.Key.numpad_enter, keyFromVirtualKey(c.VK_RETURN, c.KF_EXTENDED)); + try std.testing.expectEqual(input.Key.arrow_left, keyFromVirtualKey(c.VK_LEFT, 0)); + try std.testing.expectEqual(input.Key.f12, keyFromVirtualKey(c.VK_F1 + 11, 0)); + try std.testing.expectEqual(input.Key.quote, keyFromVirtualKey(c.VK_OEM_7, 0)); +} + +test "win32 shouldDeferTextToCharMessage only defers plain text keys" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(usize, 1), utf16CodeUnitCount('a')); + try std.testing.expectEqual(@as(usize, 2), utf16CodeUnitCount(0x1F642)); + try std.testing.expect(shouldDeferTextToCharMessage( + .press, + .key_a, + .{}, + .{ .len = 1, .unshifted_codepoint = 'a', .deferred_utf16_units = 1 }, + )); + try std.testing.expect(shouldDeferTextToCharMessage( + .repeat, + .space, + .{}, + .{ .len = 1, .unshifted_codepoint = ' ', .deferred_utf16_units = 1 }, + )); + try std.testing.expect(shouldDeferTextToCharMessage( + .press, + .quote, + .{}, + .{ .unshifted_codepoint = '\'', .deferred_utf16_units = 1 }, + )); + try std.testing.expect(!shouldDeferTextToCharMessage( + .press, + .digit_2, + .{ .ctrl = true, .alt = true }, + .{ .unshifted_codepoint = '2', .deferred_utf16_units = 1 }, + )); + try std.testing.expect(shouldDeferTextToCharMessage( + .press, + .equal, + .{ .ctrl = true, .alt = true, .sides = .{ .alt = .right } }, + .{ .len = 1, .unshifted_codepoint = '=', .deferred_utf16_units = 1 }, + )); + try std.testing.expect(!shouldDeferTextToCharMessage( + .press, + .equal, + .{ .alt = true, .sides = .{ .alt = .right } }, + .{ .len = 1, .unshifted_codepoint = '=', .deferred_utf16_units = 1 }, + )); + try std.testing.expect(!shouldDeferTextToCharMessage( + .press, + .equal, + .{ + .ctrl = true, + .alt = true, + .sides = .{ .ctrl = .right, .alt = .right }, + }, + .{ .len = 1, .unshifted_codepoint = '=', .deferred_utf16_units = 1 }, + )); + try std.testing.expect(!shouldDeferTextToCharMessage( + .press, + .enter, + .{}, + .{ .unshifted_codepoint = 0x0D }, + )); +} + +test "win32 deferred char authorization respects key handling effect" { + try std.testing.expect(shouldAuthorizeDeferredCharMessage(.ignored)); + try std.testing.expect(!shouldAuthorizeDeferredCharMessage(.consumed)); + try std.testing.expect(!shouldAuthorizeDeferredCharMessage(.closed)); +} + +test "win32 VK_PACKET key down authorizes one unit without direct text or modifiers" { + const message = keyEventFromWin32Message(c.WM_KEYDOWN, c.VK_PACKET, 0).?; + try std.testing.expectEqual(input.Action.press, message.event.action); + try std.testing.expectEqual(input.Key.unidentified, message.event.key); + try std.testing.expectEqualStrings("", message.event.utf8); + try std.testing.expect(message.event.composing); + try std.testing.expectEqual(@as(u21, 0), message.event.unshifted_codepoint); + try std.testing.expect(std.meta.eql(input.Mods{}, message.event.mods)); + try std.testing.expect(std.meta.eql(input.Mods{}, message.event.consumed_mods)); + try std.testing.expectEqual(@as(usize, 1), message.deferred_utf16_units); +} + +test "win32 VK_PACKET key up authorizes no units or text" { + const message = keyEventFromWin32Message(c.WM_KEYUP, c.VK_PACKET, 0).?; + try std.testing.expectEqual(input.Action.release, message.event.action); + try std.testing.expectEqual(input.Key.unidentified, message.event.key); + try std.testing.expectEqualStrings("", message.event.utf8); + try std.testing.expect(!message.event.composing); + try std.testing.expectEqual(@as(u21, 0), message.event.unshifted_codepoint); + try std.testing.expect(std.meta.eql(input.Mods{}, message.event.mods)); + try std.testing.expect(std.meta.eql(input.Mods{}, message.event.consumed_mods)); + try std.testing.expectEqual(@as(usize, 0), message.deferred_utf16_units); +} + +test "win32 authorized char commit preserves packet control characters" { + var utf8_buf: [8]u8 = undefined; + const fixtures = [_]struct { codepoint: u21, expected: []const u8 }{ + .{ .codepoint = '\r', .expected = "\r" }, + .{ .codepoint = '\n', .expected = "\n" }, + .{ .codepoint = '\t', .expected = "\t" }, + .{ .codepoint = 0x08, .expected = "\x08" }, + .{ .codepoint = 0x1B, .expected = "\x1B" }, + }; + for (fixtures) |fixture| { + const event = charCommitEvent(fixture.codepoint, 0, &utf8_buf).?; + try std.testing.expectEqual(input.Key.unidentified, event.key); + try std.testing.expectEqual(fixture.codepoint, event.unshifted_codepoint); + try std.testing.expectEqualStrings(fixture.expected, event.utf8); + } +} + +test "win32 deferred char authorization preserves pending units across non-text events" { + var state: DeferredCharState = .{}; + state.authorize(1); + // Release and unrelated non-text key messages authorize zero units. + state.authorize(0); + try std.testing.expectEqual(@as(usize, 1), state.pending_units); + try std.testing.expectEqual(@as(?u21, 'a'), state.consumeCodeUnit('a', false)); + try std.testing.expectEqual(@as(usize, 0), state.pending_units); +} + +test "win32 deferred char dead key and composition consume exact units" { + var state: DeferredCharState = .{}; + state.authorize(1); + state.consumeDeadChar(); + try std.testing.expectEqual(@as(usize, 0), state.pending_units); + + state.authorize(1); + try std.testing.expectEqual(@as(?u21, 0x00E9), state.consumeCodeUnit(0x00E9, false)); + try std.testing.expectEqual(@as(usize, 0), state.pending_units); +} + +test "win32 deferred char authorization blocks unsolicited and IME text" { + var state: DeferredCharState = .{}; + try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit('a', false)); + + state.authorize(1); + try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit('a', true)); + try std.testing.expectEqual(@as(usize, 0), state.pending_units); + try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit('a', false)); +} + +test "win32 deferred char two surrogate keydowns consume two code units" { + var state: DeferredCharState = .{}; + state.authorize(1); + state.authorize(1); + try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit(0xD83D, false)); + try std.testing.expectEqual(@as(usize, 1), state.pending_units); + try std.testing.expectEqual(@as(?u21, 0x1F642), state.consumeCodeUnit(0xDE42, false)); + try std.testing.expectEqual(@as(usize, 0), state.pending_units); +} + +test "win32 deferred char supplementary expectation authorizes both units" { + var state: DeferredCharState = .{}; + state.authorize(2); + try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit(0xD83D, false)); + try std.testing.expectEqual(@as(usize, 1), state.pending_units); + try std.testing.expectEqual(@as(?u21, 0x1F642), state.consumeCodeUnit(0xDE42, false)); + try std.testing.expectEqual(@as(usize, 0), state.pending_units); +} + +test "win32 deferred char commits 256 delayed BMP authorizations" { + var state: DeferredCharState = .{}; + for (0..256) |_| state.authorize(1); + try std.testing.expectEqual(@as(usize, 256), state.pending_units); + + for (0..256) |_| { + try std.testing.expectEqual(@as(?u21, 'a'), state.consumeCodeUnit('a', false)); + } + try std.testing.expectEqual(@as(usize, 0), state.pending_units); +} + +test "win32 deferred char malformed surrogate clears authorization state" { + var state: DeferredCharState = .{}; + state.authorize(3); + try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit(0xD83D, false)); + try std.testing.expectEqual(@as(?u21, null), state.consumeCodeUnit('a', false)); + try std.testing.expectEqual(@as(usize, 0), state.pending_units); + try std.testing.expectEqual(@as(?u16, null), state.high_surrogate); +} + +test "win32 deferred char authorization saturates only at usize maximum" { + var state: DeferredCharState = .{ + .pending_units = std.math.maxInt(usize) - 1, + }; + state.authorize(2); + try std.testing.expectEqual(std.math.maxInt(usize), state.pending_units); +} + +test "win32 hotkeySpecForTrigger maps physical key triggers" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const spec = hotkeySpecForTrigger(.{ + .key = .{ .physical = .key_a }, + .mods = .{ .ctrl = true, .shift = true }, + }).?; + + try std.testing.expectEqual(@as(UINT, c.MOD_CONTROL | c.MOD_SHIFT), spec.modifiers); + try std.testing.expectEqual(@as(UINT, c.VK_A), spec.vk); +} + +test "win32 hotkeySpecForTrigger maps unicode triggers with implicit shift" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const spec = hotkeySpecForTrigger(.{ + .key = .{ .unicode = '+' }, + .mods = .{ .alt = true }, + }).?; + + try std.testing.expectEqual(@as(UINT, c.MOD_ALT | c.MOD_SHIFT), spec.modifiers); + try std.testing.expectEqual(@as(UINT, c.VK_OEM_PLUS), spec.vk); +} + +test "win32 hotkeySpecForTrigger rejects unsupported catch-all triggers" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(hotkeySpecForTrigger(.{ + .key = .catch_all, + .mods = .{ .ctrl = true }, + }) == null); +} + +test "win32 hotkeySpecEql detects duplicate resolved triggers" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const physical = hotkeySpecForTrigger(.{ + .key = .{ .physical = .backquote }, + .mods = .{ .ctrl = true }, + }).?; + const unicode = hotkeySpecForTrigger(.{ + .key = .{ .unicode = '`' }, + .mods = .{ .ctrl = true }, + }).?; + const shifted = hotkeySpecForTrigger(.{ + .key = .{ .unicode = '~' }, + .mods = .{ .ctrl = true }, + }).?; + + try std.testing.expect(hotkeySpecEql(physical, unicode)); + try std.testing.expect(!hotkeySpecEql(physical, shifted)); +} + +test "win32 hotkeyRegistrationFailureReason names conflicts" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqualStrings( + "already registered by another app or another noctty instance", + hotkeyRegistrationFailureReason(.HOTKEY_ALREADY_REGISTERED), + ); + try std.testing.expectEqualStrings( + "access denied; hotkey may be reserved, occupied by an elevated app, or blocked by policy", + hotkeyRegistrationFailureReason(.ACCESS_DENIED), + ); +} + +test "win32 XButton wParam decoding maps forward and back buttons" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + // XBUTTON1 in HIWORD of wParam + const wp1 = (@as(usize, c.XBUTTON1) << 16) | @as(usize, c.MK_XBUTTON1); + try std.testing.expectEqual(c.XBUTTON1, highWord(wp1)); + + // XBUTTON2 in HIWORD of wParam + const wp2 = (@as(usize, c.XBUTTON2) << 16) | @as(usize, c.MK_XBUTTON2); + try std.testing.expectEqual(c.XBUTTON2, highWord(wp2)); +} + +test "win32 normalizeWheelDelta maps discrete wheel steps to pixel deltas" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const event = normalizeWheelDelta(.{ + .settings = .{ .lines = 3, .chars = 5 }, + .cell_size = .{ .width = 8, .height = 16 }, + .viewport = .{ .width = 800, .height = 600 }, + }, .vertical, 120); + + try std.testing.expectApproxEqAbs(48.0, event.yoff, 0.0001); + try std.testing.expectEqual(@as(f64, 0), event.xoff); + try std.testing.expect(!event.mods.precision); + try std.testing.expect(event.mods.pixel_delta); +} + +test "win32 normalizeWheelDelta maps horizontal wheel steps to pixel deltas" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const event = normalizeWheelDelta(.{ + .settings = .{ .lines = 3, .chars = 5 }, + .cell_size = .{ .width = 8, .height = 16 }, + .viewport = .{ .width = 800, .height = 600 }, + }, .horizontal, 120); + + try std.testing.expectApproxEqAbs(40.0, event.xoff, 0.0001); + try std.testing.expectEqual(@as(f64, 0), event.yoff); + try std.testing.expect(!event.mods.precision); + try std.testing.expect(event.mods.pixel_delta); +} + +test "win32 normalizeWheelDelta scales high-resolution input proportionally" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const event = normalizeWheelDelta(.{ + .settings = .{ .lines = 3, .chars = 3 }, + .cell_size = .{ .width = 8, .height = 16 }, + .viewport = .{ .width = 800, .height = 600 }, + }, .vertical, 40); + + try std.testing.expectApproxEqAbs(16.0 / 3.0, event.yoff, 0.0001); + try std.testing.expect(event.mods.precision); + try std.testing.expect(event.mods.pixel_delta); +} + +test "win32 normalizeWheelDelta honors page scroll settings" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const event = normalizeWheelDelta(.{ + .settings = .{ .lines = c.WHEEL_PAGESCROLL, .chars = 3 }, + .cell_size = .{ .width = 8, .height = 16 }, + .viewport = .{ .width = 800, .height = 600 }, + }, .vertical, 120); + + try std.testing.expectApproxEqAbs(584.0, event.yoff, 0.0001); + try std.testing.expect(!event.mods.precision); + try std.testing.expect(event.mods.pixel_delta); +} + +test "win32 normalizeWheelDelta ignores page scroll settings for high-resolution input" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const event = normalizeWheelDelta(.{ + .settings = .{ .lines = c.WHEEL_PAGESCROLL, .chars = 3 }, + .cell_size = .{ .width = 8, .height = 16 }, + .viewport = .{ .width = 800, .height = 600 }, + }, .vertical, 40); + + try std.testing.expectApproxEqAbs(16.0 / 3.0, event.yoff, 0.0001); + try std.testing.expect(event.mods.precision); + try std.testing.expect(event.mods.pixel_delta); +} + +test "win32 normalizeWheelDelta ignores disabled notch settings for high-resolution input" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const event = normalizeWheelDelta(.{ + .settings = .{ .lines = 0, .chars = 0 }, + .cell_size = .{ .width = 8, .height = 16 }, + .viewport = .{ .width = 800, .height = 600 }, + }, .vertical, 40); + + try std.testing.expectApproxEqAbs(16.0 / 3.0, event.yoff, 0.0001); + try std.testing.expect(event.mods.precision); + try std.testing.expect(event.mods.pixel_delta); +} + +// End of input declarations. diff --git a/src/apprt/win32/labels.zig b/src/apprt/win32/labels.zig new file mode 100644 index 00000000000..0d4e5085f83 --- /dev/null +++ b/src/apprt/win32/labels.zig @@ -0,0 +1,4015 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; + +const apprt = @import("../../apprt.zig"); +const windows_shell = @import("../../config/windows_shell.zig"); +const input = @import("../../input.zig"); +const terminal = @import("../../terminal/main.zig"); + +const win32_theme = @import("../win32_theme.zig"); +const win32_palette = @import("../win32_palette.zig"); +const win32_chrome_state = @import("../win32_chrome_state.zig"); +const win32_taskbar_progress = @import("../win32_taskbar_progress.zig"); +const win32_search_bar = @import("../win32_search_bar.zig"); +const win32_types = @import("../win32_types.zig"); +const c = @import("consts.zig"); + +const ThemeColors = win32_theme.ThemeColors; +const HostOverlayMode = win32_theme.HostOverlayMode; +const darkTheme = win32_theme.darkTheme; +const adjustColor = win32_theme.adjustColor; +const overlayAccentColor = win32_theme.overlayAccentColor; +const rgb = win32_theme.rgb; +const LayoutChildPaintPlan = win32_chrome_state.LayoutChildPaintPlan; +const PaletteSnapshot = win32_palette.Snapshot; +const RankedIndex = win32_palette.RankedIndex; +const PaletteStableId = @TypeOf((@as(win32_palette.catalog.Item, undefined)).id); +const palette_max_tokens = win32_palette.max_tokens; +const palette_max_ranked = win32_palette.max_ranked; +const tokenizePaletteQuery = win32_palette.tokenizeQuery; +const rankPaletteEntry = win32_palette.rankEntry; +const rankedIndicesForQuery = win32_palette.rankedForQuery; +const RECT = win32_types.RECT; +const LPCWSTR = win32_types.LPCWSTR; +const WPARAM = usize; + +const default_metrics: win32_theme.ThemeMetrics = .{}; +const host_tab_label_max_len: usize = default_metrics.tab_label_max_len; +const host_tab_min_button_width: i32 = default_metrics.tab_min_width; + +pub const PalettePresentation = struct { + match_count: usize = 0, + title: ?[]const u8 = null, + subtitle: ?[]const u8 = null, + available: bool = false, +}; + +pub const PaletteCompletion = struct { + text: []const u8, + id: PaletteStableId, +}; + +pub const command_palette_unknown_action = "Unknown Noctty action. Example: new_tab or toggle_fullscreen"; + +const search_prev_label = std.unicode.utf8ToUtf16LeStringLiteral("Prev match"); +const search_next_label = std.unicode.utf8ToUtf16LeStringLiteral("Next match"); +const search_regex_label = std.unicode.utf8ToUtf16LeStringLiteral("Regex"); +const search_case_label = std.unicode.utf8ToUtf16LeStringLiteral("Case sensitive"); +const search_word_label = std.unicode.utf8ToUtf16LeStringLiteral("Whole word"); +const search_close_label = std.unicode.utf8ToUtf16LeStringLiteral("Close search"); + +pub const host_banner_inspector_inactive = "Inspector hidden. Terminal view is active."; +const search_results_idle = "Type to search"; +const search_results_pending = "Searching"; +const search_results_none = "No matches"; + +pub const SearchStatus = struct { + active: bool = false, + needle: ?[]const u8 = null, + total: ?usize = null, + selected: ?usize = null, +}; + +pub const SurfaceStatus = struct { + pwd: ?[]const u8 = null, + scrollbar: terminal.Scrollbar = .zero, + readonly: bool = false, + secure_input: bool = false, + key_sequence_active: bool = false, + key_table_name: ?[]const u8 = null, + search: SearchStatus = .{}, + progress: ?[]const u8 = null, +}; + +pub const HostTabStatus = struct { + index: usize = 0, + total: usize = 1, +}; + +const VisibleTabRange = struct { + start: usize, + count: usize, +}; + +const TabOverviewEntry = struct { + title: ?[]const u8 = null, + pane_count: usize = 1, + active: bool = false, +}; + +pub const HostBannerKind = enum { + none, + info, + err, +}; + +const TabButtonKeyAction = enum { + previous, + next, + first, + last, + move_previous, + move_next, + move_first, + move_last, + rename, + close, + overview, +}; + +const SearchButtonKeyAction = enum { + next, + previous, + dismiss, +}; + +const TabsButtonKeyAction = enum { + previous, + next, + rename, + overview, +}; + +const CommandButtonKeyAction = enum { + toggle, + previous, + next, + dismiss, +}; + +const ProfilesButtonKeyAction = enum { + open, + toggle, + previous, + next, + first, + last, +}; + +const QuickSlotFocusKeyAction = enum { + previous, + next, + first, + last, + open, +}; + +pub const LaunchTargetButtonKeyAction = enum { + previous, + next, + first, + last, +}; + +pub const ProfileOpenTarget = enum { + tab, + window, + split, +}; + +pub const ProfileSelection = union(enum) { + exact: usize, + ambiguous: usize, + invalid, +}; + +pub fn ownedStringEquals(current: ?[:0]const u8, value: []const u8) bool { + return if (current) |existing| + std.mem.eql(u8, existing, value) + else + false; +} + +pub const OverlayFocusSlot = enum { edit, accept, cancel }; + +const ScrollStatusKey = struct { + visible: bool, + percent: usize, +}; + +const ResizeSplitFallbackDelta = struct { + width: i32 = 0, + height: i32 = 0, +}; + +const SurfaceOrderEntry = struct { + host_id: u32, + host_active: bool, +}; + +pub const SearchBarResultsVisual = struct { + bg: u32, + border: u32, + fg: u32, +}; + +pub const SearchBarToolbarVisual = struct { + bg: u32, + border: u32, + separator: u32, +}; + +pub const SearchBarButtonRole = enum { + prev, + next, + regex, + case_sensitive, + whole_word, + close, +}; + +pub fn tabButtonKeyAction(vk: WPARAM, ctrl_pressed: bool) ?TabButtonKeyAction { + return switch (vk) { + c.VK_LEFT => if (ctrl_pressed) .move_previous else .previous, + c.VK_RIGHT => if (ctrl_pressed) .move_next else .next, + c.VK_HOME => if (ctrl_pressed) .move_first else .first, + c.VK_END => if (ctrl_pressed) .move_last else .last, + c.VK_F2 => .rename, + c.VK_DELETE => .close, + c.VK_APPS => .overview, + else => null, + }; +} + +pub fn moveTabAmountToEdge(total: usize, current: usize, toward_start: bool) isize { + if (total <= 1 or current >= total) return 0; + if (toward_start) return -@as(isize, @intCast(current)); + return @as(isize, @intCast((total - 1) - current)); +} + +pub fn searchButtonKeyAction(vk: WPARAM, shift_pressed: bool) ?SearchButtonKeyAction { + return switch (vk) { + c.VK_F3 => if (shift_pressed) .previous else .next, + c.VK_ESCAPE => .dismiss, + else => null, + }; +} + +pub fn dockedSearchCoreDirectionFromKeyAction(action: SearchButtonKeyAction) ?input.Binding.Action.NavigateSearch { + return switch (action) { + .previous => .previous, + .next => .next, + .dismiss => null, + }; +} + +pub fn dockedSearchButtonDirection(command_id: usize) ?input.Binding.Action.NavigateSearch { + // Core scrollback search indexes matches newest-first + // (`.previous` => newer/down, `.next` => older/up). The docked + // search bar is laid out as visible up/down navigation, so the + // button IDs map to the opposite core enum. + return switch (command_id) { + c.SEARCH_PREV_ID => .next, + c.SEARCH_NEXT_ID => .previous, + else => null, + }; +} + +pub fn dockedSearchEnterDirection(shift_pressed: bool) input.Binding.Action.NavigateSearch { + return dockedSearchArrowDirection(if (shift_pressed) c.VK_UP else c.VK_DOWN).?; +} + +pub fn dockedSearchArrowDirection(vk: WPARAM) ?input.Binding.Action.NavigateSearch { + return switch (vk) { + c.VK_UP => dockedSearchButtonDirection(c.SEARCH_PREV_ID), + c.VK_DOWN => dockedSearchButtonDirection(c.SEARCH_NEXT_ID), + else => null, + }; +} + +fn searchDirectionFromWheelDelta(delta: i16) input.Binding.Action.NavigateSearch { + return if (delta > 0) + .next + else + .previous; +} + +pub fn searchSelectedRawFromCoreDisplay(selected: ?usize) ?usize { + const value = selected orelse return null; + if (value == 0) return null; + return value - 1; +} + +pub fn searchSelectedDisplayFromRaw(raw: ?usize, total: ?usize) ?usize { + const idx = raw orelse return null; + const count = total orelse return null; + if (count == 0 or idx >= count) return null; + return idx + 1; +} + +pub fn advanceSearchSelectedRaw( + raw: ?usize, + total: ?usize, + dir: input.Binding.Action.NavigateSearch, + wrap: bool, +) ?usize { + const idx = raw orelse return null; + const count = total orelse return null; + if (count == 0 or idx >= count) return null; + const last = count - 1; + return switch (dir) { + .next => if (idx >= last) if (wrap) 0 else idx else idx + 1, + .previous => if (idx == 0) if (wrap) last else 0 else idx - 1, + }; +} + +pub fn searchBarSearchedStateForTotal(total: ?usize) bool { + return total != null; +} + +pub fn searchBarSearchedStateForSelected(selected: ?usize, total: ?usize) bool { + return selected != null or total != null; +} + +pub fn searchBarDisplayStateChanged( + search_bar: *const win32_search_bar.SearchBar, + searched: bool, + total: ?usize, + selected: ?usize, +) bool { + return search_bar.searched != searched or + search_bar.total != total or + search_bar.selected != selected; +} + +pub fn profileChromeVisible(overlay_mode: HostOverlayMode, status_bar_height: i32) bool { + return win32_chrome_state.profileVisible(overlay_mode, status_bar_height); +} + +pub fn profileChromeNeedsFullTextInvalidation(overlay_mode: HostOverlayMode, status_bar_height: i32) bool { + return win32_chrome_state.profileNeedsFullTextInvalidation(overlay_mode, status_bar_height); +} + +pub fn chromeTextNeedsFullInvalidation(status_bar_height: i32) bool { + return win32_chrome_state.textNeedsFullInvalidation(status_bar_height); +} + +pub fn inspectorVisibilityChangeNeedsHostRelayout(changed: bool) bool { + return changed; +} + +pub fn inspectorPanelVisibleForState(overlay_mode: HostOverlayMode, active_tab_has_inspector: bool) bool { + return overlay_mode == .none and active_tab_has_inspector; +} + +pub fn layoutChildPaintPlan(chrome_changed: bool, content_changed: bool) LayoutChildPaintPlan { + return win32_chrome_state.layoutChildPaintPlan(chrome_changed, content_changed); +} + +pub fn overlayAcceptButtonVisible(mode: HostOverlayMode) bool { + return mode != .command_palette; +} + +pub fn overlayEditFrameVisible(mode: HostOverlayMode) bool { + return mode != .confirm; +} + +fn nextOverlayFocusSlot(mode: HostOverlayMode, current: OverlayFocusSlot, reverse: bool) OverlayFocusSlot { + if (mode == .confirm) return if (current == .accept) .cancel else .accept; + if (!overlayAcceptButtonVisible(mode)) return if (current == .edit) .cancel else .edit; + return if (reverse) + switch (current) { + .edit => .cancel, + .accept => .edit, + .cancel => .accept, + } + else switch (current) { + .edit => .accept, + .accept => .cancel, + .cancel => .edit, + }; +} + +fn overlayFocusSlotVisible( + slot: OverlayFocusSlot, + edit_visible: bool, + accept_visible: bool, + cancel_visible: bool, +) bool { + return switch (slot) { + .edit => edit_visible, + .accept => accept_visible, + .cancel => cancel_visible, + }; +} + +pub fn nextVisibleOverlayFocusSlot( + mode: HostOverlayMode, + current: OverlayFocusSlot, + reverse: bool, + edit_visible: bool, + accept_visible: bool, + cancel_visible: bool, +) ?OverlayFocusSlot { + var candidate = current; + for (0..3) |_| { + candidate = nextOverlayFocusSlot(mode, candidate, reverse); + if (candidate != current and overlayFocusSlotVisible( + candidate, + edit_visible, + accept_visible, + cancel_visible, + )) return candidate; + } + return null; +} + +pub fn inspectorChromeVisible(overlay_mode: HostOverlayMode, status_bar_height: i32) bool { + return win32_chrome_state.inspectorVisible(overlay_mode, status_bar_height); +} + +pub fn inspectorBannerStateChanged( + overlay_mode: HostOverlayMode, + banner_kind: HostBannerKind, + banner_text: ?[:0]const u8, + visible: bool, +) bool { + if (overlay_mode != .none) return false; + if (visible) return banner_kind != .none or banner_text != null; + return banner_kind != .info or !ownedStringEquals(banner_text, host_banner_inspector_inactive); +} + +pub fn windowTitleSyncChanged(current: ?[:0]const u8, next: []const u8) bool { + return !ownedStringEquals(current, next); +} + +fn scrollStatusKey(scrollbar: terminal.Scrollbar) ScrollStatusKey { + const visible = scrollbar.total > scrollbar.len and + scrollbar.offset + scrollbar.len < scrollbar.total; + const percent = if (visible and scrollbar.total > 0) + (scrollbar.offset * 100) / scrollbar.total + else + 0; + return .{ + .visible = visible, + .percent = percent, + }; +} + +pub fn scrollStatusTextChanged(previous: terminal.Scrollbar, next: terminal.Scrollbar) bool { + return !std.meta.eql(scrollStatusKey(previous), scrollStatusKey(next)); +} + +fn tabsButtonKeyAction(vk: WPARAM) ?TabsButtonKeyAction { + return switch (vk) { + c.VK_LEFT, c.VK_UP => .previous, + c.VK_RIGHT, c.VK_DOWN => .next, + c.VK_F2 => .rename, + c.VK_APPS => .overview, + else => null, + }; +} + +fn commandButtonKeyAction(vk: WPARAM) ?CommandButtonKeyAction { + return switch (vk) { + c.VK_RETURN, c.VK_SPACE => .toggle, + c.VK_UP => .previous, + c.VK_DOWN => .next, + c.VK_ESCAPE => .dismiss, + else => null, + }; +} + +pub fn bindingActionsToggleCommandPalette(actions: []const input.Binding.Action) bool { + for (actions) |action| switch (action) { + .toggle_command_palette => return true, + else => {}, + }; + return false; +} + +fn commandPaletteDirectionFromWheelDelta(delta: i16) bool { + return delta > 0; +} + +fn profileDirectionFromWheelDelta(delta: i16) bool { + return delta > 0; +} + +pub fn profileShortcutIndexFromKey(vk: WPARAM) ?usize { + if (vk >= @as(WPARAM, '1') and vk <= @as(WPARAM, '9')) { + return @as(usize, @intCast(vk - @as(WPARAM, '1'))); + } + if (vk >= 0x61 and vk <= 0x69) { + return @as(usize, @intCast(vk - 0x61)); + } + return null; +} + +pub fn quickSlotShortcutProfileIndex( + profiles_len: usize, + selected_index: ?usize, + vk: WPARAM, + alt_pressed: bool, +) ?usize { + if (!alt_pressed) return null; + const slot_ordinal = profileShortcutIndexFromKey(vk) orelse return null; + if (slot_ordinal >= 3) return null; + return quickSlotProfileIndex(profiles_len, selected_index, slot_ordinal, 3); +} + +pub fn quickSlotPinOrdinalFromKey(vk: WPARAM, alt_pressed: bool, shift_pressed: bool) ?usize { + if (!alt_pressed or !shift_pressed) return null; + const slot_ordinal = profileShortcutIndexFromKey(vk) orelse return null; + if (slot_ordinal >= 3) return null; + return slot_ordinal; +} + +pub fn clearQuickSlotPinsRequested(vk: WPARAM, alt_pressed: bool, shift_pressed: bool) bool { + if (!alt_pressed or !shift_pressed) return false; + return vk == c.VK_0 or vk == c.VK_NUMPAD0; +} + +fn quickSlotFocusKeyAction(vk: WPARAM) ?QuickSlotFocusKeyAction { + return switch (vk) { + c.VK_LEFT, c.VK_UP => .previous, + c.VK_RIGHT, c.VK_DOWN => .next, + c.VK_HOME => .first, + c.VK_END => .last, + c.VK_RETURN => .open, + else => null, + }; +} + +fn profilesButtonKeyAction(vk: WPARAM) ?ProfilesButtonKeyAction { + return switch (vk) { + c.VK_RETURN => .open, + c.VK_SPACE, c.VK_APPS => .toggle, + c.VK_LEFT, c.VK_UP => .previous, + c.VK_RIGHT, c.VK_DOWN => .next, + c.VK_HOME => .first, + c.VK_END => .last, + else => null, + }; +} + +pub fn startsWithIgnoreCase(haystack: []const u8, prefix: []const u8) bool { + if (prefix.len > haystack.len) return false; + for (haystack[0..prefix.len], prefix) |a, b| { + if (std.ascii.toLower(a) != std.ascii.toLower(b)) return false; + } + return true; +} + +pub fn resolveProfileSelection( + profiles: []const windows_shell.Profile, + input_text: []const u8, + fallback_index: usize, +) ProfileSelection { + if (profiles.len == 0) return .invalid; + const fallback = @min(fallback_index, profiles.len - 1); + if (input_text.len == 0) return .{ .exact = fallback }; + + const requested = std.fmt.parseUnsigned(usize, input_text, 10) catch null; + if (requested) |value| { + if (value == 0 or value > profiles.len) return .invalid; + return .{ .exact = value - 1 }; + } + + var exact_match: ?usize = null; + var unique_match: ?usize = null; + var match_count: usize = 0; + for (profiles, 0..) |profile, index| { + if (std.ascii.eqlIgnoreCase(profile.key, input_text) or + std.ascii.eqlIgnoreCase(profile.label, input_text)) + { + exact_match = index; + break; + } + if (startsWithIgnoreCase(profile.key, input_text) or + startsWithIgnoreCase(profile.label, input_text)) + { + unique_match = index; + match_count += 1; + } + } + if (exact_match) |index| return .{ .exact = index }; + if (match_count == 1) return .{ .exact = unique_match.? }; + if (match_count > 1) return .{ .ambiguous = match_count }; + return .invalid; +} + +pub fn profileIndexByKey(profiles: []const windows_shell.Profile, key: []const u8) ?usize { + for (profiles, 0..) |profile, index| { + if (std.ascii.eqlIgnoreCase(profile.key, key)) return index; + } + return null; +} + +pub fn preferredProfileIndex( + profiles: []const windows_shell.Profile, + selected_key: ?[]const u8, + app_key: ?[]const u8, + hint: ?[]const u8, + fallback_index: usize, +) ?usize { + if (profiles.len == 0) return null; + + const preferred_key = if (selected_key) |key| + key + else if (app_key) |key| + key + else + null; + if (preferred_key) |key| { + for (profiles, 0..) |profile, index| { + if (std.ascii.eqlIgnoreCase(profile.key, key)) return index; + } + } + + if (hint) |value| { + return switch (resolveProfileSelection(profiles, value, fallback_index)) { + .exact => |index| index, + .ambiguous, .invalid => null, + }; + } + + return null; +} + +pub fn quickSlotProfileIndex( + profiles_len: usize, + selected_index: ?usize, + slot_ordinal: usize, + max_slots: usize, +) ?usize { + if (profiles_len == 0 or slot_ordinal >= max_slots) return null; + var drawn: usize = 0; + var index: usize = 0; + while (index < profiles_len) : (index += 1) { + if (selected_index != null and index == selected_index.?) continue; + if (drawn == slot_ordinal) return index; + drawn += 1; + if (drawn >= max_slots) break; + } + return null; +} + +pub fn formatProgressStatus( + alloc: Allocator, + value: win32_taskbar_progress.ProgressReport, +) !?[]u8 { + const progress = win32_taskbar_progress.clampPercent(value.progress); + return switch (value.state) { + .remove => null, + .set => if (progress) |value_| + try std.fmt.allocPrint(alloc, "progress:{d}%", .{value_}) + else + try alloc.dupe(u8, "progress"), + .@"error" => if (progress) |value_| + try std.fmt.allocPrint(alloc, "progress error:{d}%", .{value_}) + else + try alloc.dupe(u8, "progress error"), + .indeterminate => try alloc.dupe(u8, "progress:busy"), + .pause => if (progress) |value_| + try std.fmt.allocPrint(alloc, "progress paused:{d}%", .{value_}) + else + try alloc.dupe(u8, "progress paused"), + }; +} + +fn buildWindowTitle( + alloc: Allocator, + base_title: ?[]const u8, + status: SurfaceStatus, +) ![]u8 { + var buf: std.ArrayListUnmanaged(u8) = .empty; + errdefer buf.deinit(alloc); + + try buf.appendSlice(alloc, base_title orelse "noctty"); + + const appendStatus = struct { + fn call( + list: *std.ArrayListUnmanaged(u8), + alloc_: Allocator, + comptime fmt: []const u8, + args: anytype, + ) !void { + try list.appendSlice(alloc_, " | "); + try list.writer(alloc_).print(fmt, args); + } + }.call; + + if (status.readonly) try appendStatus(&buf, alloc, "readonly", .{}); + if (status.secure_input) try appendStatus(&buf, alloc, "sensitive", .{}); + if (status.key_sequence_active) try appendStatus(&buf, alloc, "keys", .{}); + if (status.key_table_name) |name| try appendStatus(&buf, alloc, "table:{s}", .{name}); + if (status.pwd) |pwd| try appendStatus(&buf, alloc, "cwd:{s}", .{pwd}); + if (status.search.active) { + if (status.search.needle) |needle| { + if (status.search.selected) |selected| { + if (status.search.total) |total| { + try appendStatus(&buf, alloc, "find:{s} ({d}/{d})", .{ needle, selected, total }); + } else { + try appendStatus(&buf, alloc, "find:{s} ({d})", .{ needle, selected }); + } + } else if (status.search.total) |total| { + try appendStatus(&buf, alloc, "find:{s} ({d})", .{ needle, total }); + } else { + try appendStatus(&buf, alloc, "find:{s}", .{needle}); + } + } else { + try appendStatus(&buf, alloc, "find", .{}); + } + } + if (status.progress) |progress| try appendStatus(&buf, alloc, "{s}", .{progress}); + + return buf.toOwnedSlice(alloc); +} + +pub fn resolveWindowBaseTitle( + terminal_title: ?[:0]const u8, + surface_override: ?[:0]const u8, + tab_override: ?[:0]const u8, +) ?[:0]const u8 { + return tab_override orelse surface_override orelse terminal_title; +} + +pub fn normalizedBackgroundOpacity(value: f64) f64 { + return std.math.clamp(value, 0.0, 1.0); +} + +pub fn effectiveBackgroundOpacity(configured: f64, force_opaque: bool) f64 { + return if (force_opaque) 1.0 else configured; +} + +pub fn alphaByteForOpacity(value: f64) u8 { + return @intFromFloat(@round(normalizedBackgroundOpacity(value) * 255.0)); +} + +pub fn hiddenScrollbarAlphaByte() u8 { + // Keep the layered child hit-testable while visually hidden. + // Windows won't deliver real hover input to an alpha-0 layered + // child, so a true zero here strands the dynamic scrollbar in the + // hidden state until some non-mouse activity wakes it up. + return 1; +} + +pub fn resizeSplitFallbackDelta(value: apprt.action.ResizeSplit) ResizeSplitFallbackDelta { + const amount: i32 = @intCast(value.amount); + return switch (value.direction) { + .left => .{ .width = -amount }, + .right => .{ .width = amount }, + .up => .{ .height = -amount }, + .down => .{ .height = amount }, + }; +} + +fn nextInspectorVisible(current: bool, mode: apprt.action.Inspector) bool { + return switch (mode) { + .toggle => !current, + .show => true, + .hide => false, + }; +} + +pub fn nextTabInspectorVisible(active_tab_has_inspector: bool, mode: apprt.action.Inspector) bool { + return nextInspectorVisible(active_tab_has_inspector, mode); +} + +fn primarySurfaceIndex(entries: []const SurfaceOrderEntry) ?usize { + if (entries.len == 0) return null; + const first_host_id = entries[0].host_id; + for (entries, 0..) |entry, i| { + if (entry.host_id == first_host_id and entry.host_active) return i; + } + return 0; +} + +pub fn buildHostAwareBaseTitle( + alloc: Allocator, + base_title: ?[]const u8, + host: HostTabStatus, +) ![]u8 { + if (host.total <= 1) return try alloc.dupe(u8, base_title orelse "noctty"); + return try std.fmt.allocPrint( + alloc, + "[{d}/{d}] {s}", + .{ host.index + 1, host.total, base_title orelse "noctty" }, + ); +} + +fn compactHostLabel( + alloc: Allocator, + value: []const u8, + max_len: usize, +) ![]u8 { + if (value.len <= max_len) return try alloc.dupe(u8, value); + if (max_len <= 3) return try alloc.dupe(u8, "..."); + return try std.fmt.allocPrint(alloc, "{s}...", .{value[0 .. max_len - 3]}); +} + +fn compactHostLabelLen(value: []const u8, max_len: usize) usize { + if (value.len <= max_len) return value.len; + return if (max_len <= 3) 3 else max_len; +} + +pub fn hostTabLabelMaxLen(button_width: i32) usize { + const estimated = @as(usize, @intCast(@max(6, @divTrunc(button_width - 26, 8)))); + return std.math.clamp(estimated, @as(usize, 6), @as(usize, host_tab_label_max_len)); +} + +pub fn shouldShowPaneCount(button_width: i32, pane_count: usize) bool { + return pane_count > 1 and button_width >= 140; +} + +pub fn visibleTabRange(tab_count: usize, active_index: usize, tab_area_width: i32) VisibleTabRange { + if (tab_count == 0) return .{ .start = 0, .count = 0 }; + const max_visible = std.math.clamp( + @as(usize, @intCast(@max(1, @divTrunc(tab_area_width, host_tab_min_button_width)))), + @as(usize, 1), + tab_count, + ); + if (tab_count <= max_visible) return .{ .start = 0, .count = tab_count }; + + const clamped_active = @min(active_index, tab_count - 1); + var start = clamped_active; + if (max_visible > 1) start = clamped_active -| @divTrunc(max_visible - 1, 2); + if (start + max_visible > tab_count) start = tab_count - max_visible; + return .{ .start = start, .count = max_visible }; +} + +pub fn buildTabButtonLabel( + alloc: Allocator, + base_title: ?[]const u8, + index: usize, + active: bool, + pane_count: usize, + max_len: usize, + show_pane_count: bool, +) ![]u8 { + const compact = try compactHostLabel(alloc, base_title orelse "noctty", max_len); + defer alloc.free(compact); + if (show_pane_count and pane_count > 1) { + return try std.fmt.allocPrint( + alloc, + "{s}{d}: {s} ({d})", + .{ + if (active) "* " else "", + index + 1, + compact, + pane_count, + }, + ); + } + + return try std.fmt.allocPrint( + alloc, + "{s}{d}: {s}", + .{ + if (active) "* " else "", + index + 1, + compact, + }, + ); +} + +fn buildTabOverviewBannerText( + alloc: Allocator, + entries: []const TabOverviewEntry, +) ![]u8 { + if (entries.len == 0) return try alloc.dupe(u8, "Tabs: none"); + + var buf: std.ArrayListUnmanaged(u8) = .empty; + errdefer buf.deinit(alloc); + try buf.appendSlice(alloc, "Tabs: "); + for (entries, 0..) |entry, i| { + if (i > 0) try buf.appendSlice(alloc, " | "); + const compact = try compactHostLabel(alloc, entry.title orelse "noctty", 18); + defer alloc.free(compact); + try buf.writer(alloc).print("{s}{d}:{s}", .{ + if (entry.active) "*" else "", + i + 1, + compact, + }); + if (entry.pane_count > 1) { + try buf.writer(alloc).print(" ({d})", .{entry.pane_count}); + } + } + return try buf.toOwnedSlice(alloc); +} + +pub fn buildSearchOverlayLabel( + alloc: Allocator, + total: ?usize, + selected: ?usize, +) ![]u8 { + if (selected) |current| { + if (total) |count| return try std.fmt.allocPrint(alloc, "Find {d}/{d}", .{ current, count }); + } + if (total) |count| return try std.fmt.allocPrint(alloc, "Find {d}", .{count}); + return try alloc.dupe(u8, "Find"); +} + +pub fn buildSearchBarResultsText( + alloc: Allocator, + bar: *const win32_search_bar.SearchBar, +) ![]u8 { + if (bar.query.len == 0) return try alloc.dupe(u8, search_results_idle); + if (!bar.searched or bar.total == null) return try alloc.dupe(u8, search_results_pending); + + const total = bar.total.?; + if (total == 0) return try alloc.dupe(u8, search_results_none); + if (bar.selected) |selected| return try std.fmt.allocPrint(alloc, "{d}/{d}", .{ selected, total }); + return try std.fmt.allocPrint(alloc, "{d}", .{total}); +} + +pub fn searchBarButtonCommandId(role: SearchBarButtonRole) usize { + return switch (role) { + .prev => c.SEARCH_PREV_ID, + .next => c.SEARCH_NEXT_ID, + .regex => c.SEARCH_REGEX_ID, + .case_sensitive => c.SEARCH_CASE_ID, + .whole_word => c.SEARCH_WORD_ID, + .close => c.SEARCH_CLOSE_ID, + }; +} + +pub fn searchBarButtonLabel(role: SearchBarButtonRole) LPCWSTR { + return switch (role) { + .prev => search_prev_label, + .next => search_next_label, + .regex => search_regex_label, + .case_sensitive => search_case_label, + .whole_word => search_word_label, + .close => search_close_label, + }; +} + +pub fn showSearchBarResults(bar: *const win32_search_bar.SearchBar) bool { + return bar.query.len > 0; +} + +/// Docked-search geometry only changes when the fixed-width results slot +/// appears or disappears. Query text updates within the non-empty state +/// repaint child HWNDs but do not move controls. +pub fn searchBarNeedsRelayoutForQueryChange(previous_query_len: usize, next_query_len: usize) bool { + return (previous_query_len == 0) != (next_query_len == 0); +} + +/// Once a query has reached core search, any edit must invalidate it until the +/// debounced replacement query runs. +pub fn searchBarShouldInvalidateCoreSearchOnEdit(previous_query_len: usize, next_query_len: usize) bool { + _ = next_query_len; + return previous_query_len > 0; +} + +pub fn shouldAcceptCoreSearchUpdates(bar: *const win32_search_bar.SearchBar) bool { + return bar.query.len == 0 or bar.searched; +} + +pub fn searchBarButtonShowsLabel(role: SearchBarButtonRole, button_width: i32) bool { + _ = role; + _ = button_width; + return false; +} + +pub fn rectInset(rect: RECT, inset_x: i32, inset_y: i32) RECT { + return .{ + .left = rect.left + inset_x, + .top = rect.top + inset_y, + .right = rect.right - inset_x, + .bottom = rect.bottom - inset_y, + }; +} + +pub fn rectUnion(a: RECT, b: RECT) RECT { + return .{ + .left = @min(a.left, b.left), + .top = @min(a.top, b.top), + .right = @max(a.right, b.right), + .bottom = @max(a.bottom, b.bottom), + }; +} + +pub fn rectOffset(rect: RECT, dx: i32, dy: i32) RECT { + return .{ + .left = rect.left + dx, + .top = rect.top + dy, + .right = rect.right + dx, + .bottom = rect.bottom + dy, + }; +} + +pub fn searchBarResultsVisual( + theme: *const ThemeColors, + bar: *const win32_search_bar.SearchBar, +) SearchBarResultsVisual { + const accent = overlayAccentColor(.search, theme.is_dark); + if (!bar.searched or bar.total == null) { + return .{ + .bg = if (theme.is_dark) + adjustColor(theme.overlay_bg, 8, 10, 10) + else + adjustColor(theme.overlay_bg, -6, -4, -2), + .border = accent, + .fg = theme.overlay_label_fg, + }; + } + + if (bar.total.? == 0) { + return .{ + .bg = if (theme.is_dark) + rgb(48, 28, 30) + else + rgb(252, 236, 236), + .border = theme.error_fg, + .fg = theme.error_fg, + }; + } + + return .{ + .bg = if (theme.is_dark) + adjustColor(theme.edit_frame_bg, 10, 14, 10) + else + adjustColor(theme.edit_frame_bg, -6, -2, -4), + .border = accent, + .fg = theme.overlay_label_fg, + }; +} + +pub fn searchBarToolbarVisual(theme: *const ThemeColors) SearchBarToolbarVisual { + return .{ + .bg = if (theme.is_dark) + adjustColor(theme.edit_frame_bg, 6, 8, 10) + else + adjustColor(theme.edit_frame_bg, -4, -2, -2), + .border = if (theme.is_dark) + adjustColor(theme.overlay_border, 18, 18, 20) + else + adjustColor(theme.overlay_border, -18, -18, -18), + .separator = if (theme.is_dark) + adjustColor(theme.overlay_border, 30, 30, 34) + else + adjustColor(theme.overlay_border, -26, -26, -26), + }; +} + +pub fn searchBarFrameBg(theme: *const ThemeColors) u32 { + return if (theme.is_dark) + adjustColor(theme.overlay_bg, 3, 3, 5) + else + adjustColor(theme.overlay_bg, -2, -2, -1); +} + +pub fn searchBarFrameBorder(theme: *const ThemeColors) u32 { + return if (theme.is_dark) + adjustColor(theme.overlay_border, 10, 10, 12) + else + adjustColor(theme.overlay_border, -14, -14, -14); +} + +pub fn searchBarBottomLine(theme: *const ThemeColors) u32 { + return if (theme.is_dark) + adjustColor(theme.overlay_border, -10, -10, -8) + else + adjustColor(theme.overlay_border, 10, 10, 10); +} + +pub fn searchBarButtonParentBg(theme: *const ThemeColors, role: SearchBarButtonRole) u32 { + return switch (role) { + .close => theme.overlay_bg, + else => searchBarToolbarVisual(theme).bg, + }; +} + +pub fn buildTabOverviewOverlayLabel( + alloc: Allocator, + current_index: usize, + total: usize, +) ![]u8 { + if (total <= 1) return try alloc.dupe(u8, "Tab"); + return try std.fmt.allocPrint(alloc, "Tab {d}/{d}", .{ current_index + 1, total }); +} + +pub fn buildOverlayPaintLabelText( + alloc: Allocator, + mode: HostOverlayMode, + input_text: []const u8, + search_total: ?usize, + search_selected: ?usize, + host_status: HostTabStatus, + palette_presentation: PalettePresentation, +) ![]u8 { + return switch (mode) { + .none => try alloc.dupe(u8, ""), + .surface_title => try alloc.dupe(u8, "Window title"), + .tab_title => try alloc.dupe(u8, "Tab title"), + .command_palette => try buildCommandPaletteOverlayLabel( + alloc, + input_text, + palette_presentation, + ), + .profile => try alloc.dupe(u8, "Profile"), + .search => try buildSearchOverlayLabel(alloc, search_total, search_selected), + .tab_overview => try buildTabOverviewOverlayLabel(alloc, host_status.index, host_status.total), + // Confirm overlays source their prompt title from the payload + // at paint time; this default only appears when the payload + // has already been dropped (mid-teardown). + .confirm => try alloc.dupe(u8, "Confirm"), + }; +} + +pub fn buildOverlayFeedbackText( + alloc: Allocator, + banner_kind: HostBannerKind, + banner_text: ?[]const u8, + mode: HostOverlayMode, + input_text: []const u8, + active_search_needle: ?[]const u8, + search_total: ?usize, + search_selected: ?usize, + host_status: HostTabStatus, + pane_count: usize, + palette: PaletteSnapshot, + mru: []const []const u8, + palette_presentation: PalettePresentation, +) ![]u8 { + if (banner_text) |value| { + return switch (banner_kind) { + .err => try std.fmt.allocPrint(alloc, "Error: {s}", .{value}), + .info => try std.fmt.allocPrint(alloc, "Info: {s}", .{value}), + .none => try alloc.dupe(u8, value), + }; + } + if (mode == .command_palette) { + return try buildCommandPaletteFeedbackText( + alloc, + input_text, + mru, + palette_presentation, + ); + } + return try buildOverlayHintText( + alloc, + mode, + input_text, + active_search_needle, + search_total, + search_selected, + host_status, + pane_count, + palette, + mru, + ); +} + +pub fn buildOverlayAcceptLabel( + alloc: Allocator, + mode: HostOverlayMode, + input_text: []const u8, + active_search_needle: ?[]const u8, + search_total: ?usize, + search_selected: ?usize, + palette_presentation: PalettePresentation, +) ![]u8 { + return switch (mode) { + .none => try alloc.dupe(u8, "OK"), + .command_palette => blk: { + if (input_text.len == 0) break :blk try alloc.dupe(u8, "Close"); + if (palette_presentation.match_count > 0 and !palette_presentation.available) { + break :blk try alloc.dupe(u8, "Resize"); + } + if (palette_presentation.match_count > 0) break :blk try alloc.dupe(u8, "Activate"); + break :blk try alloc.dupe(u8, "Check"); + }, + .profile => try alloc.dupe(u8, "Open"), + .search => blk: { + if (input_text.len == 0) break :blk try alloc.dupe(u8, "Close"); + if (active_search_needle) |needle| { + if (std.mem.eql(u8, needle, input_text) and search_selected != null and search_total != null) { + break :blk try alloc.dupe(u8, "Next"); + } + } + if (search_total != null) break :blk try alloc.dupe(u8, "Find"); + break :blk try alloc.dupe(u8, "Find"); + }, + .surface_title, .tab_title => if (input_text.len == 0) + try alloc.dupe(u8, "Close") + else + try alloc.dupe(u8, "Apply"), + .tab_overview => if (input_text.len == 0) + try alloc.dupe(u8, "Close") + else + try alloc.dupe(u8, "Go"), + // Confirm overlays source accept/cancel labels from the + // payload. This default is never visible because + // `syncOverlayButtons` has a special-case branch for the + // `.confirm` mode that reads the payload directly; the + // switch just needs coverage to compile. + .confirm => try alloc.dupe(u8, "OK"), + }; +} + +pub fn buildOverlayHintText( + alloc: Allocator, + mode: HostOverlayMode, + input_text: []const u8, + active_search_needle: ?[]const u8, + search_total: ?usize, + search_selected: ?usize, + host_status: HostTabStatus, + pane_count: usize, + palette: PaletteSnapshot, + mru: []const []const u8, +) ![]u8 { + return switch (mode) { + .none => try alloc.dupe(u8, ""), + .command_palette => blk: { + if (try commandPaletteBannerText(alloc, palette, input_text, mru)) |text| break :blk text; + break :blk try alloc.dupe(u8, "No matching action. Try: new_tab, start_search, reload_config"); + }, + .profile => try alloc.dupe(u8, "Choose a profile by number or name. Enter opens a new tab."), + .search => blk: { + if (input_text.len == 0) break :blk try alloc.dupe(u8, "Type to search live. Enter repeats the current match. Escape closes."); + if (search_selected) |selected| { + if (search_total) |total| { + if (active_search_needle) |needle| { + if (std.mem.eql(u8, needle, input_text)) { + break :blk try std.fmt.allocPrint( + alloc, + "Live matches {d}/{d}. Enter jumps to the next match. Escape closes.", + .{ selected, total }, + ); + } + } + break :blk try std.fmt.allocPrint( + alloc, + "Live matches {d}/{d}. Enter keeps this needle active.", + .{ selected, total }, + ); + } + } + if (search_total) |total| { + break :blk try std.fmt.allocPrint( + alloc, + "Live matches {d}. Enter keeps this needle active.", + .{total}, + ); + } + break :blk try alloc.dupe(u8, "No matches yet. Keep typing to search live."); + }, + .surface_title => try alloc.dupe(u8, "Apply a window title override for this host. Submit empty text to clear it."), + .tab_title => blk: { + if (pane_count > 1) { + break :blk try std.fmt.allocPrint( + alloc, + "Rename tab {d}/{d}. This tab currently has {d} panes. Submit empty text to clear the override.", + .{ host_status.index + 1, host_status.total, pane_count }, + ); + } + break :blk try std.fmt.allocPrint( + alloc, + "Rename tab {d}/{d}. Submit empty text to clear the override.", + .{ host_status.index + 1, host_status.total }, + ); + }, + .tab_overview => blk: { + if (host_status.total <= 1) break :blk try alloc.dupe(u8, "Only one tab is open in this window."); + if (input_text.len == 0) { + break :blk try std.fmt.allocPrint( + alloc, + "Jump directly to a tab number. Current tab: {d}/{d}.", + .{ host_status.index + 1, host_status.total }, + ); + } + const requested = std.fmt.parseUnsigned(usize, input_text, 10) catch { + break :blk try std.fmt.allocPrint( + alloc, + "Enter a tab number from 1 to {d}. Current tab: {d}/{d}.", + .{ host_status.total, host_status.index + 1, host_status.total }, + ); + }; + if (requested == 0 or requested > host_status.total) { + break :blk try std.fmt.allocPrint( + alloc, + "Tab {d} is out of range. Valid range: 1 to {d}.", + .{ requested, host_status.total }, + ); + } + break :blk try std.fmt.allocPrint( + alloc, + "Jump to tab {d} of {d}.", + .{ requested, host_status.total }, + ); + }, + // Confirm overlays source their hint text from the payload + // body at paint time (the hint HWND is re-used as the body + // line). This helper produces a default so the promptTitle + // call doesn't need a special case; the real text lives on + // the payload. + .confirm => try alloc.dupe(u8, ""), + }; +} + +pub fn overlayCancelLabel(mode: HostOverlayMode) []const u8 { + return switch (mode) { + .none => "Cancel", + .command_palette, .profile, .search, .tab_overview => "Close", + .surface_title, .tab_title => "Cancel", + // Confirm overlays override this via the payload. + .confirm => "Cancel", + }; +} + +pub fn buildHostBannerText( + alloc: Allocator, + banner_kind: HostBannerKind, + value: []const u8, +) ![]u8 { + return switch (banner_kind) { + .none => try alloc.dupe(u8, value), + .info => try std.fmt.allocPrint(alloc, "Info: {s}", .{value}), + .err => try std.fmt.allocPrint(alloc, "Error: {s}", .{value}), + }; +} + +fn commandPaletteCompletionCandidate( + snap: PaletteSnapshot, + seed: []const u8, + current_text: []const u8, + reverse: bool, +) ?[]const u8 { + var ranked_buf: [palette_max_ranked]RankedIndex = undefined; + const ranked = rankedIndicesForQuery(snap, seed, &ranked_buf); + if (ranked.len == 0) return null; + if (ranked.len == 1) return std.mem.span(snap.cvals[ranked[0].index].action); + + var current_rank_index: ?usize = null; + for (ranked, 0..) |r, i| { + const action = std.mem.span(snap.cvals[r.index].action); + if (std.mem.eql(u8, action, current_text)) { + current_rank_index = i; + break; + } + } + + const target = if (current_rank_index) |value| + if (reverse) + (value + ranked.len - 1) % ranked.len + else + (value + 1) % ranked.len + else if (reverse) + ranked.len - 1 + else + 0; + return std.mem.span(snap.cvals[ranked[target].index].action); +} + +/// Look up the Command.description for a given formatted action string. +/// Returns null if no entry matches or the description is empty. +fn commandPaletteDescriptionFor(snap: PaletteSnapshot, action_text: []const u8) ?[]const u8 { + for (snap.commands, snap.cvals) |cmd, cval| { + const action = std.mem.span(cval.action); + if (!std.mem.eql(u8, action, action_text)) continue; + const desc: []const u8 = cmd.description; + return if (desc.len == 0) null else desc; + } + return null; +} + +pub fn buildCommandPaletteOverlayLabel( + alloc: Allocator, + input_text: []const u8, + presentation: PalettePresentation, +) ![]u8 { + if (input_text.len == 0) return try alloc.dupe(u8, "Command"); + if (presentation.match_count > 0) { + return try std.fmt.allocPrint(alloc, "Command {d}", .{presentation.match_count}); + } + return try alloc.dupe(u8, "Command ?"); +} + +pub fn buildCommandPaletteFeedbackText( + alloc: Allocator, + input_text: []const u8, + mru: []const []const u8, + presentation: PalettePresentation, +) ![]u8 { + if (input_text.len == 0) { + if (mru.len > 0) { + return try std.fmt.allocPrint( + alloc, + "Recent: {s}. Type to search actions, themes, tabs, panes, settings, and help.", + .{mru[0]}, + ); + } + return try alloc.dupe( + u8, + "Type to search actions, themes, tabs, panes, settings, and help.", + ); + } + if (presentation.match_count == 0) { + return try alloc.dupe( + u8, + "No matching command. Try > actions, % themes, @ tabs, / panes, : settings, or ? help.", + ); + } + if (!presentation.available) { + return try std.fmt.allocPrint( + alloc, + "{d} matches. Make the window larger to show and activate results.", + .{presentation.match_count}, + ); + } + + const title = presentation.title orelse "Selected result"; + const subtitle = presentation.subtitle orelse ""; + if (presentation.match_count == 1) { + if (subtitle.len > 0) { + return try std.fmt.allocPrint( + alloc, + "{s} — {s}. Enter activates; Escape closes.", + .{ title, subtitle }, + ); + } + return try std.fmt.allocPrint( + alloc, + "{s}. Enter activates; Escape closes.", + .{title}, + ); + } + if (subtitle.len > 0) { + return try std.fmt.allocPrint( + alloc, + "{d} matches. Selected: {s} — {s}. Up/Down selects; Enter activates.", + .{ presentation.match_count, title, subtitle }, + ); + } + return try std.fmt.allocPrint( + alloc, + "{d} matches. Selected: {s}. Up/Down selects; Enter activates.", + .{ presentation.match_count, title }, + ); +} + +pub fn paletteCompletionText(descriptor: win32_palette.catalog.Descriptor) []const u8 { + return switch (descriptor.payload) { + .action => |payload| payload.action, + .recent_command => |payload| payload.action, + .profile => |key| key, + .setting => |key| key, + .theme => |name| name, + .tab, .pane, .help => descriptor.item.title, + }; +} + +pub fn commandPaletteBannerText( + alloc: Allocator, + snap: PaletteSnapshot, + input_text: []const u8, + mru: []const []const u8, +) !?[]u8 { + if (input_text.len == 0) { + if (mru.len == 0) { + return try alloc.dupe(u8, "Try: new_tab (new tab), start_search (find), toggle_tab_overview (tab list)"); + } + var buf: std.ArrayListUnmanaged(u8) = .empty; + errdefer buf.deinit(alloc); + try buf.appendSlice(alloc, "Recent: "); + const visible = @min(mru.len, 5); + for (mru[0..visible], 0..) |action, i| { + if (i > 0) try buf.appendSlice(alloc, ", "); + try buf.appendSlice(alloc, action); + } + return try buf.toOwnedSlice(alloc); + } + + if (input.Binding.Action.parse(input_text)) |_| { + if (commandPaletteDescriptionFor(snap, input_text)) |summary| { + return try std.fmt.allocPrint(alloc, "Ready: {s} - {s}", .{ input_text, summary }); + } + return try std.fmt.allocPrint(alloc, "Ready to run: {s}", .{input_text}); + } else |_| {} + + // Cap visible matches to keep the banner readable. With ~90 + // entries a bare query like "t" can rank dozens of matches; + // showing all of them produces a wall of text. + const max_visible: usize = 5; + var ranked_buf: [palette_max_ranked]RankedIndex = undefined; + const ranked = rankedIndicesForQuery(snap, input_text, &ranked_buf); + if (ranked.len == 0) return null; + + // Single match → "Ready: {action} - {description}" so Enter + // telegraphs what it'll run. Multi-match falls through to the + // "Matches: …" list so the user sees the candidate set. + if (ranked.len == 1) { + const action = std.mem.span(snap.cvals[ranked[0].index].action); + if (commandPaletteDescriptionFor(snap, action)) |summary| { + return try std.fmt.allocPrint(alloc, "Ready: {s} - {s}", .{ action, summary }); + } + return try std.fmt.allocPrint(alloc, "Ready to run: {s}", .{action}); + } + + var buf: std.ArrayListUnmanaged(u8) = .empty; + errdefer buf.deinit(alloc); + try buf.appendSlice(alloc, "Matches: "); + const visible_len = @min(ranked.len, max_visible); + for (ranked[0..visible_len], 0..) |r, i| { + const action = std.mem.span(snap.cvals[r.index].action); + if (i > 0) try buf.appendSlice(alloc, ", "); + try buf.appendSlice(alloc, action); + if (commandPaletteDescriptionFor(snap, action)) |summary| { + try buf.appendSlice(alloc, " ("); + try buf.appendSlice(alloc, summary); + try buf.appendSlice(alloc, ")"); + } + } + if (ranked.len > visible_len) { + const rest = ranked.len - visible_len; + try std.fmt.format(buf.writer(alloc), " + {d} more", .{rest}); + } + return try buf.toOwnedSlice(alloc); +} + +pub fn overlayPaintCacheDirty( + overlay_text_dirty: bool, + overlay_mode: HostOverlayMode, + cached_label_w: ?[:0]const u16, + cached_feedback_w: ?[:0]const u16, + cached_badge_w: ?[:0]const u16, +) bool { + return win32_chrome_state.overlayPaintCacheDirty( + overlay_text_dirty, + overlay_mode, + cached_label_w != null, + cached_feedback_w != null, + cached_badge_w != null, + ); +} + +pub fn buildInspectorBannerText( + alloc: Allocator, + host: HostTabStatus, + pane_count: usize, + zoomed: bool, +) ![]u8 { + if (zoomed and pane_count > 1) { + return try std.fmt.allocPrint( + alloc, + "Inspector active | tab {d}/{d} | panes {d} | zoomed | toggle Inspect to return", + .{ host.index + 1, host.total, pane_count }, + ); + } + if (pane_count > 1) { + return try std.fmt.allocPrint( + alloc, + "Inspector active | tab {d}/{d} | panes {d} | toggle Inspect to return", + .{ host.index + 1, host.total, pane_count }, + ); + } + return try std.fmt.allocPrint( + alloc, + "Inspector active | tab {d}/{d} | toggle Inspect to return", + .{ host.index + 1, host.total }, + ); +} + +pub fn buildInspectorPanelTitleText( + alloc: Allocator, + host: HostTabStatus, + pane_count: usize, + zoomed: bool, +) ![]u8 { + if (zoomed and pane_count > 1) { + return try std.fmt.allocPrint( + alloc, + "Inspector • tab {d}/{d} • {d} panes • zoomed", + .{ host.index + 1, host.total, pane_count }, + ); + } + if (pane_count > 1) { + return try std.fmt.allocPrint( + alloc, + "Inspector • tab {d}/{d} • {d} panes", + .{ host.index + 1, host.total, pane_count }, + ); + } + if (zoomed) { + return try std.fmt.allocPrint( + alloc, + "Inspector • tab {d}/{d} • zoomed", + .{ host.index + 1, host.total }, + ); + } + return try std.fmt.allocPrint( + alloc, + "Inspector • tab {d}/{d}", + .{ host.index + 1, host.total }, + ); +} + +pub fn buildInspectorPanelHintText( + alloc: Allocator, + pane_count: usize, + zoomed: bool, +) ![]u8 { + if (zoomed) { + return try alloc.dupe( + u8, + "Core inspector is live for the zoomed pane. Toggle Inspect to return to terminal-only view.", + ); + } + if (pane_count > 1) { + return try std.fmt.allocPrint( + alloc, + "Core inspector is live across {d} panes in this tab. Toggle Inspect to return to terminal-only view.", + .{pane_count}, + ); + } + return try alloc.dupe(u8, "Core inspector is live for this tab. Toggle Inspect to return to terminal-only view."); +} + +pub fn buildInspectorDetailText( + alloc: Allocator, + host: HostTabStatus, + pane_count: usize, + zoomed: bool, +) ![]u8 { + if (zoomed and pane_count > 1) { + return try std.fmt.allocPrint( + alloc, + "Inspector is attached to tab {d}/{d}. This tab has {d} panes and split zoom is active.", + .{ host.index + 1, host.total, pane_count }, + ); + } + if (pane_count > 1) { + return try std.fmt.allocPrint( + alloc, + "Inspector is attached to tab {d}/{d}. This tab currently has {d} panes.", + .{ host.index + 1, host.total, pane_count }, + ); + } + return try std.fmt.allocPrint( + alloc, + "Inspector is attached to tab {d}/{d}. Toggle Inspect to return to the normal terminal view.", + .{ host.index + 1, host.total }, + ); +} + +pub fn buildSearchDetailText( + alloc: Allocator, + needle: ?[]const u8, + total: ?usize, + selected: ?usize, +) ![]u8 { + if (needle) |value| { + if (selected) |current| { + if (total) |count| { + return try std.fmt.allocPrint( + alloc, + "Live search for \"{s}\" is active. Match {d} of {d}. Enter moves to the next match.", + .{ value, current, count }, + ); + } + } + if (total) |count| { + return try std.fmt.allocPrint( + alloc, + "Live search for \"{s}\" is active. {d} matches currently visible.", + .{ value, count }, + ); + } + return try std.fmt.allocPrint( + alloc, + "Live search for \"{s}\" is active. Keep typing to refine the current needle.", + .{value}, + ); + } + return try alloc.dupe(u8, "Live search is active. Keep typing to refine the current needle."); +} + +fn buildCommandButtonLabel( + alloc: Allocator, + active: bool, + input_text: ?[]const u8, +) ![]u8 { + if (active) { + if (input_text) |value| { + if (value.len > 0) { + const compact = try compactHostLabel(alloc, value, 9); + defer alloc.free(compact); + return try std.fmt.allocPrint(alloc, "Cmd {s}", .{compact}); + } + } + return try alloc.dupe(u8, "[Cmd]"); + } + return try alloc.dupe(u8, "Cmd"); +} + +fn buildProfilesButtonLabel( + alloc: Allocator, + active: bool, + profiles_opt: ?[]const windows_shell.Profile, + selected_index: ?usize, + pinned_slot_ordinal: ?usize, +) ![]u8 { + _ = pinned_slot_ordinal; + const profiles = profiles_opt orelse return try alloc.dupe(u8, if (active) "Pick shell" else "Launch"); + if (profiles.len == 0) return try alloc.dupe(u8, if (active) "Pick shell" else "Launch"); + const index = selected_index orelse 0; + const profile = profiles[@min(index, profiles.len - 1)]; + const compact = try compactHostLabel(alloc, profile.label, 8); + defer alloc.free(compact); + if (active) return try std.fmt.allocPrint(alloc, "Pick {s}", .{compact}); + return try std.fmt.allocPrint(alloc, "Launch {s}", .{compact}); +} + +fn launchTargetButtonLabel( + alloc: Allocator, + target: ProfileOpenTarget, + selected_index: ?usize, + pinned_slot_ordinal: ?usize, +) ![]u8 { + const base = switch (target) { + .tab => "Tab", + .window => "Win", + .split => "Pane", + }; + if (selected_index) |index| { + if (index < 9) { + if (pinned_slot_ordinal != null and pinned_slot_ordinal.? == index) { + return try std.fmt.allocPrint(alloc, "*{d} {s}", .{ index + 1, base }); + } + return try std.fmt.allocPrint(alloc, "{d} {s}", .{ index + 1, base }); + } + } + return try alloc.dupe(u8, base); +} + +fn profileKindBadge(kind: windows_shell.ProfileKind) []const u8 { + return switch (kind) { + .wsl_default, .wsl_distro => "WSL", + .pwsh => "PWSH", + .powershell => "PS", + .git_bash => "GIT", + .cmd => "CMD", + }; +} + +fn profileKindGlyph(kind: windows_shell.ProfileKind) []const u8 { + return switch (kind) { + .wsl_default, .wsl_distro => "<>", + .pwsh => ">>", + .powershell => ">_", + .git_bash => "$>", + .cmd => "C>", + }; +} + +pub fn buildProfileChromeBadgeText(alloc: Allocator, kind: windows_shell.ProfileKind) ![]u8 { + return try std.fmt.allocPrint(alloc, "{s} {s}", .{ + profileKindBadge(kind), + profileKindGlyph(kind), + }); +} + +fn profileKindDetail(kind: windows_shell.ProfileKind) windows_shell.ShellIntegrationDiagnostic { + return windows_shell.shellIntegrationDiagnostic(kind); +} + +fn profileOpenTargetActionText(target: ProfileOpenTarget) []const u8 { + return switch (target) { + .tab => "new tab", + .window => "new window", + .split => "split", + }; +} + +pub fn buildProfileStatusBadgeText( + alloc: Allocator, + profile: *const windows_shell.Profile, + selected_index: ?usize, + pinned_slot_ordinal: ?usize, +) ![]u8 { + _ = selected_index; + _ = pinned_slot_ordinal; + const compact = try compactHostLabel(alloc, profile.label, 12); + defer alloc.free(compact); + return try alloc.dupe(u8, compact); +} + +pub fn profileStatusBadgeTextLen( + profile: *const windows_shell.Profile, + selected_index: ?usize, + pinned_slot_ordinal: ?usize, +) usize { + _ = selected_index; + _ = pinned_slot_ordinal; + return compactHostLabelLen(profile.label, 12); +} + +pub fn buildDropdownProfileLabel( + alloc: Allocator, + profile: *const windows_shell.Profile, + index: usize, +) ![]u8 { + if (index < 9) { + return try std.fmt.allocPrint(alloc, "{s}\tCtrl+Shift+{d}", .{ profile.label, index + 1 }); + } + return try alloc.dupe(u8, profile.label); +} + +pub fn buildProfileQuickSlotChipText( + alloc: Allocator, + profile: *const windows_shell.Profile, + slot_index: usize, + pinned_slot_ordinal: ?usize, +) ![]u8 { + if (slot_index < 9) { + if (pinned_slot_ordinal != null and pinned_slot_ordinal.? == slot_index) { + return try std.fmt.allocPrint(alloc, "*{d} {s}", .{ + slot_index + 1, + profileKindBadge(profile.kind), + }); + } + return try std.fmt.allocPrint(alloc, "{d} {s}", .{ + slot_index + 1, + profileKindBadge(profile.kind), + }); + } + return try alloc.dupe(u8, profileKindBadge(profile.kind)); +} + +pub fn profileQuickSlotChipTextLen( + profile: *const windows_shell.Profile, + slot_index: usize, + pinned_slot_ordinal: ?usize, +) usize { + const badge_len = profileKindBadge(profile.kind).len; + if (slot_index < 9) { + return badge_len + 2 + if (pinned_slot_ordinal != null and pinned_slot_ordinal.? == slot_index) @as(usize, 1) else 0; + } + return badge_len; +} + +pub fn launcherChipRightInset(has_slot_badge: bool, has_target_marker: bool) i32 { + if (has_slot_badge) return 16; + if (has_target_marker) return 12; + return 5; +} + +fn targetButtonLabelRightInset(target: ?ProfileOpenTarget) i32 { + return if (target != null) 12 else 0; +} + +pub fn buttonLabelRightInset(pinned_slot_ordinal: ?usize, target: ?ProfileOpenTarget) i32 { + const slot_inset: i32 = if (pinnedSlotBadgeDigit(pinned_slot_ordinal) != null) 16 else 0; + return @max(targetButtonLabelRightInset(target), slot_inset); +} + +pub fn shouldPaintQuickSlotTargetMarker(hovered: bool, focused: bool) bool { + return hovered or focused; +} + +pub fn pinnedSlotBadgeDigit(pinned_slot_ordinal: ?usize) ?u8 { + const ordinal = pinned_slot_ordinal orelse return null; + if (ordinal >= 9) return null; + return @as(u8, @intCast('1' + ordinal)); +} + +pub fn profileOpenTargetMarkerColor(target: ProfileOpenTarget) u32 { + return switch (target) { + .tab => rgb(132, 172, 238), + .window => rgb(236, 182, 118), + .split => rgb(126, 204, 148), + }; +} + +pub fn profileOpenTargetBadgeGlyph(target: ProfileOpenTarget) u8 { + return switch (target) { + .tab => 'T', + .window => 'W', + .split => 'S', + }; +} + +fn buildProfileCommandPreviewText( + alloc: Allocator, + profile: *const windows_shell.Profile, + max_len: usize, +) ![]u8 { + const command = try profile.command.string(alloc); + defer alloc.free(command); + return try compactHostLabel(alloc, command, max_len); +} + +fn buildProfileOrderSummaryText( + alloc: Allocator, + order_hint_opt: ?[]const u8, + max_items: usize, +) !?[]u8 { + const order_hint = order_hint_opt orelse return null; + var parts: std.ArrayListUnmanaged(u8) = .empty; + errdefer parts.deinit(alloc); + + var count: usize = 0; + var more = false; + var it = std.mem.splitAny(u8, order_hint, ",;"); + while (it.next()) |raw_token| { + const token = std.mem.trim(u8, raw_token, " \t\r\n"); + if (token.len == 0) continue; + if (count >= max_items) { + more = true; + break; + } + const compact = try compactHostLabel(alloc, token, 12); + defer alloc.free(compact); + if (parts.items.len > 0) try parts.appendSlice(alloc, " > "); + try parts.appendSlice(alloc, compact); + count += 1; + } + + if (count == 0) return null; + if (more) try parts.appendSlice(alloc, " > ..."); + return try parts.toOwnedSlice(alloc); +} + +fn buildProfileQuickPickText( + alloc: Allocator, + profiles: []const windows_shell.Profile, + max_items: usize, + max_label_len: usize, +) !?[]u8 { + if (profiles.len == 0 or max_items == 0) return null; + + var parts: std.ArrayListUnmanaged(u8) = .empty; + errdefer parts.deinit(alloc); + + const limit = @min(@min(max_items, profiles.len), 9); + var index: usize = 0; + while (index < limit) : (index += 1) { + const badge = try buildProfileChromeBadgeText(alloc, profiles[index].kind); + defer alloc.free(badge); + const label = try compactHostLabel(alloc, profiles[index].label, max_label_len); + defer alloc.free(label); + + if (parts.items.len > 0) try parts.appendSlice(alloc, " | "); + const item = try std.fmt.allocPrint(alloc, "{d} {s} {s}", .{ + index + 1, + badge, + label, + }); + defer alloc.free(item); + try parts.appendSlice(alloc, item); + } + + if (limit < profiles.len) try parts.appendSlice(alloc, " | ..."); + return try parts.toOwnedSlice(alloc); +} + +fn buildSearchButtonLabel( + alloc: Allocator, + active: bool, + total: ?usize, + selected: ?usize, +) ![]u8 { + if (selected) |current| { + if (total) |count| { + return try std.fmt.allocPrint(alloc, "{s}{d}/{d}", .{ + if (active) "[F] " else "Find ", + current, + count, + }); + } + } + if (total) |count| { + return try std.fmt.allocPrint(alloc, "{s}{d}", .{ + if (active) "[F] " else "Find ", + count, + }); + } + return try alloc.dupe(u8, if (active) "[Find]" else "Find"); +} + +pub fn buildProfileOverlayLabel( + alloc: Allocator, + profiles: []const windows_shell.Profile, + input_text: []const u8, + selected_index: usize, +) ![]u8 { + if (profiles.len == 0) return try alloc.dupe(u8, "Profile"); + return switch (resolveProfileSelection(profiles, input_text, selected_index)) { + .exact => |index| blk: { + const badge = try buildProfileChromeBadgeText(alloc, profiles[index].kind); + defer alloc.free(badge); + break :blk try std.fmt.allocPrint( + alloc, + "Profile {d}/{d} {s}", + .{ index + 1, profiles.len, badge }, + ); + }, + .ambiguous => |count| try std.fmt.allocPrint(alloc, "Profile {d}", .{count}), + .invalid => try alloc.dupe(u8, "Profile ?"), + }; +} + +pub fn buildProfileAcceptLabel( + alloc: Allocator, + profiles_opt: ?[]const windows_shell.Profile, + input_text: []const u8, + selected_index: usize, + default_target: ProfileOpenTarget, +) ![]u8 { + const profiles = profiles_opt orelse return try alloc.dupe(u8, "Check"); + if (profiles.len == 0) return try alloc.dupe(u8, "Check"); + return switch (resolveProfileSelection(profiles, input_text, selected_index)) { + .exact => switch (default_target) { + .tab => try alloc.dupe(u8, "Open Tab"), + .window => try alloc.dupe(u8, "Open Win"), + .split => try alloc.dupe(u8, "Split"), + }, + .ambiguous => try alloc.dupe(u8, "Pick"), + .invalid => try alloc.dupe(u8, "Check"), + }; +} + +pub fn buildProfileHintText( + alloc: Allocator, + profiles_opt: ?[]const windows_shell.Profile, + input_text: []const u8, + selected_index: usize, + default_target: ProfileOpenTarget, + pinned_slot_keys: [3]?[:0]const u8, +) ![]u8 { + const profiles = profiles_opt orelse return try alloc.dupe(u8, "No supported Windows profiles detected."); + if (profiles.len == 0) return try alloc.dupe(u8, "No supported Windows profiles detected."); + const quick_picks = try buildProfileQuickPickText(alloc, profiles, 4, 10); + defer if (quick_picks) |value| alloc.free(value); + const quick_suffix = if (quick_picks) |value| + try std.fmt.allocPrint(alloc, " Quick picks: {s}.", .{value}) + else + try alloc.dupe(u8, ""); + defer alloc.free(quick_suffix); + return switch (resolveProfileSelection(profiles, input_text, selected_index)) { + .exact => |index| blk: { + const preview = try buildProfileCommandPreviewText(alloc, &profiles[index], 36); + defer alloc.free(preview); + const badge = try buildProfileChromeBadgeText(alloc, profiles[index].kind); + defer alloc.free(badge); + const pinned_slot = try buildPinnedProfileSlotText( + alloc, + findLauncherQuickSlotOrdinal(pinned_slot_keys, profiles[index].key), + ); + defer alloc.free(pinned_slot); + break :blk try std.fmt.allocPrint( + alloc, + "{s} {s} | key {s} | run {s}.{s} Enter opens a {s}. Ctrl+Enter splits here. Shift+Enter opens a new window. Ctrl+1-9 launches directly. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning.{s}", + .{ + badge, + profiles[index].label, + profiles[index].key, + preview, + pinned_slot, + profileOpenTargetActionText(default_target), + quick_suffix, + }, + ); + }, + .ambiguous => |count| try std.fmt.allocPrint( + alloc, + "{d} profiles match. Keep typing a name or use Up/Down to cycle the current selection. Ctrl+1-9 launches directly. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning.{s}", + .{ count, quick_suffix }, + ), + .invalid => try std.fmt.allocPrint( + alloc, + "No matching profile. Try 1-{d} or a profile name like pwsh, ubuntu, git, or cmd. Ctrl+1-9 launches directly. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning. Space keeps the picker open.{s}", + .{ profiles.len, quick_suffix }, + ), + }; +} + +pub fn buildProfileDetailText( + alloc: Allocator, + profile: *const windows_shell.Profile, + profiles_opt: ?[]const windows_shell.Profile, + overlay_open: bool, + default_target: ProfileOpenTarget, + order_hint: ?[]const u8, + pinned_slot_keys: [3]?[:0]const u8, +) ![]u8 { + const preview = try buildProfileCommandPreviewText(alloc, profile, 32); + defer alloc.free(preview); + const badge = try buildProfileChromeBadgeText(alloc, profile.kind); + defer alloc.free(badge); + const pinned_slot = try buildPinnedProfileSlotText( + alloc, + findLauncherQuickSlotOrdinal(pinned_slot_keys, profile.key), + ); + defer alloc.free(pinned_slot); + const quick_picks = if (overlay_open) + try buildProfileQuickPickText(alloc, profiles_opt orelse &.{}, 4, 10) + else + try buildProfileQuickPickText(alloc, profiles_opt orelse &.{}, 3, 9); + defer if (quick_picks) |value| alloc.free(value); + const quick_suffix = if (overlay_open) + if (quick_picks) |value| + try std.fmt.allocPrint(alloc, " Quick picks: {s}.", .{value}) + else + try alloc.dupe(u8, "") + else if (quick_picks) |value| + try std.fmt.allocPrint(alloc, " Top slots: {s}.", .{value}) + else + try alloc.dupe(u8, ""); + defer alloc.free(quick_suffix); + const order_summary = try buildProfileOrderSummaryText(alloc, order_hint, 4); + defer if (order_summary) |value| alloc.free(value); + const order_suffix = if (order_summary) |value| + try std.fmt.allocPrint(alloc, " Order: {s}.", .{value}) + else + try alloc.dupe(u8, ""); + defer alloc.free(order_suffix); + const overlay_suffix = try std.fmt.allocPrint(alloc, "{s}{s}", .{ quick_suffix, order_suffix }); + defer alloc.free(overlay_suffix); + const idle_suffix = try std.fmt.allocPrint(alloc, "{s}{s}", .{ quick_suffix, order_suffix }); + defer alloc.free(idle_suffix); + const detail = profileKindDetail(profile.kind); + return if (overlay_open) + std.fmt.allocPrint( + alloc, + "Selected profile: {s} {s}. Run {s}.{s} Enter opens a {s}, Ctrl+Enter splits here, and Shift+Enter opens a new window. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning.{s}", + .{ + badge, + profile.label, + preview, + pinned_slot, + profileOpenTargetActionText(default_target), + overlay_suffix, + }, + ) + else + std.fmt.allocPrint( + alloc, + "Default profile: {s} {s}. Run {s}.{s} New hosts inherit this {s}.{s}{s} + opens a {s}, middle-click + splits here, and right-click + opens a new window. Alt+1-3 launches visible slots. Alt+Shift+1-3 pins the current profile. Alt+Shift+0 clears pinning.{s}", + .{ + badge, + profile.label, + preview, + pinned_slot, + detail.summary, + if (detail.next_step != null) " " else "", + detail.next_step orelse "", + profileOpenTargetActionText(default_target), + idle_suffix, + }, + ); +} + +fn buildPinnedProfileSlotText(alloc: Allocator, pinned_slot_ordinal: ?usize) ![]u8 { + if (pinned_slot_ordinal) |ordinal| { + return try std.fmt.allocPrint(alloc, " Pinned slot {d}.", .{ordinal + 1}); + } + return try alloc.dupe(u8, ""); +} + +fn buildInspectorButtonLabel( + alloc: Allocator, + visible: bool, + pane_count: usize, +) ![]u8 { + if (visible and pane_count > 1) { + return try std.fmt.allocPrint(alloc, "[Inspect {d}]", .{pane_count}); + } + if (visible) return try alloc.dupe(u8, "[Inspect]"); + if (pane_count > 1) return try std.fmt.allocPrint(alloc, "Inspect {d}", .{pane_count}); + return try alloc.dupe(u8, "Inspect"); +} + +pub fn findLauncherQuickSlotOrdinal(slot_keys: [3]?[:0]const u8, key: []const u8) ?usize { + for (slot_keys, 0..) |slot_key, index| { + if (slot_key) |value| { + if (std.ascii.eqlIgnoreCase(value, key)) return index; + } + } + return null; +} + +pub fn nextTabOverviewSelection(current: usize, total: usize, reverse: bool) usize { + if (total == 0) return 0; + const clamped = std.math.clamp(current, @as(usize, 1), total); + if (reverse) { + return if (clamped <= 1) total else clamped - 1; + } + return if (clamped >= total) 1 else clamped + 1; +} + +pub fn tabDirectionFromWheelDelta(delta: i16) apprt.action.GotoTab { + return if (delta > 0) .previous else .next; +} + +test "win32 profileIndexByKey finds launch profile key" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + var profiles = [_]windows_shell.Profile{ + .{ + .kind = .cmd, + .key = "cmd.exe", + .label = "Command Prompt", + .command = .{ .shell = "cmd.exe" }, + }, + .{ + .kind = .wsl_distro, + .key = "wsl:Ubuntu", + .label = "Ubuntu", + .command = .{ .shell = "wsl.exe" }, + }, + }; + + try std.testing.expectEqual(@as(?usize, 1), profileIndexByKey(&profiles, "wsl:Ubuntu")); + try std.testing.expectEqual(@as(?usize, null), profileIndexByKey(&profiles, "missing")); +} + +test "win32 buildWindowTitle appends active status segments" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const title = try buildWindowTitle(std.testing.allocator, "pwsh", .{ + .pwd = "/Users/amant", + .scrollbar = .{ + .total = 200, + .offset = 50, + .len = 40, + }, + .readonly = true, + .secure_input = true, + .key_sequence_active = true, + .key_table_name = "resize", + .search = .{ + .active = true, + .needle = "foo", + .total = 4, + .selected = 2, + }, + .progress = "progress:35%", + }); + defer std.testing.allocator.free(title); + + try std.testing.expectEqualStrings( + "pwsh | readonly | sensitive | keys | table:resize | cwd:/Users/amant | find:foo (2/4) | progress:35%", + title, + ); +} + +test "win32 buildWindowTitle uses default title when base is null" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const title = try buildWindowTitle(std.testing.allocator, null, .{}); + defer std.testing.allocator.free(title); + + try std.testing.expectEqualStrings("noctty", title); +} + +test "win32 resolveWindowBaseTitle prefers tab then surface override" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqualStrings( + "tab", + resolveWindowBaseTitle("terminal", "surface", "tab").?, + ); + try std.testing.expectEqualStrings( + "surface", + resolveWindowBaseTitle("terminal", "surface", null).?, + ); + try std.testing.expectEqualStrings( + "terminal", + resolveWindowBaseTitle("terminal", null, null).?, + ); +} + +test "win32 effectiveBackgroundOpacity respects opaque override" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(f64, 0.4), effectiveBackgroundOpacity(0.4, false)); + try std.testing.expectEqual(@as(f64, 1.0), effectiveBackgroundOpacity(0.4, true)); + try std.testing.expectEqual(@as(u8, 128), alphaByteForOpacity(0.5)); + try std.testing.expectEqual(@as(u8, 1), hiddenScrollbarAlphaByte()); +} + +test "win32 resizeSplitFallbackDelta maps directions to window deltas" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqualDeep( + ResizeSplitFallbackDelta{ .width = -24, .height = 0 }, + resizeSplitFallbackDelta(.{ .amount = 24, .direction = .left }), + ); + try std.testing.expectEqualDeep( + ResizeSplitFallbackDelta{ .width = 0, .height = 12 }, + resizeSplitFallbackDelta(.{ .amount = 12, .direction = .down }), + ); +} + +test "win32 nextInspectorVisible follows requested mode" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(nextInspectorVisible(false, .toggle)); + try std.testing.expect(!nextInspectorVisible(true, .toggle)); + try std.testing.expect(nextInspectorVisible(false, .show)); + try std.testing.expect(!nextInspectorVisible(true, .hide)); +} + +test "win32 tab inspector toggle hides any active pane inspector" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(nextTabInspectorVisible(false, .toggle)); + try std.testing.expect(!nextTabInspectorVisible(true, .toggle)); + try std.testing.expect(nextTabInspectorVisible(true, .show)); + try std.testing.expect(!nextTabInspectorVisible(false, .hide)); +} + +test "win32 primarySurfaceIndex prefers active tab of first host" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const entries = [_]SurfaceOrderEntry{ + .{ .host_id = 11, .host_active = false }, + .{ .host_id = 22, .host_active = true }, + .{ .host_id = 11, .host_active = true }, + }; + try std.testing.expectEqual(@as(?usize, 2), primarySurfaceIndex(&entries)); + + const fallback = [_]SurfaceOrderEntry{ + .{ .host_id = 11, .host_active = false }, + .{ .host_id = 22, .host_active = true }, + }; + try std.testing.expectEqual(@as(?usize, 0), primarySurfaceIndex(&fallback)); +} + +test "win32 buildHostAwareBaseTitle prefixes host tab position" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const titled = try buildHostAwareBaseTitle(std.testing.allocator, "pwsh", .{ + .index = 1, + .total = 3, + }); + defer std.testing.allocator.free(titled); + try std.testing.expectEqualStrings("[2/3] pwsh", titled); + + const single = try buildHostAwareBaseTitle(std.testing.allocator, "pwsh", .{ + .index = 0, + .total = 1, + }); + defer std.testing.allocator.free(single); + try std.testing.expectEqualStrings("pwsh", single); +} + +test "win32 buildTabButtonLabel marks active tab and pane count" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const title = try buildTabButtonLabel(std.testing.allocator, "pwsh", 1, true, 3, 24, true); + defer std.testing.allocator.free(title); + try std.testing.expectEqualStrings("* 2: pwsh (3)", title); +} + +test "win32 buildTabButtonLabel omits pane count for single pane tabs" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const title = try buildTabButtonLabel(std.testing.allocator, "pwsh", 0, false, 1, 24, false); + defer std.testing.allocator.free(title); + try std.testing.expectEqualStrings("1: pwsh", title); +} + +test "win32 buildTabButtonLabel compacts long titles" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const title = try buildTabButtonLabel(std.testing.allocator, "this-is-a-very-long-terminal-title", 0, false, 1, 24, false); + defer std.testing.allocator.free(title); + try std.testing.expectEqualStrings("1: this-is-a-very-long-t...", title); +} + +test "win32 buildTabButtonLabel drops pane count when tabs are narrow" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const title = try buildTabButtonLabel(std.testing.allocator, "logs-and-output-pane", 1, false, 3, 9, false); + defer std.testing.allocator.free(title); + try std.testing.expectEqualStrings("2: logs-a...", title); +} + +test "win32 hostTabLabelMaxLen shrinks with narrow tab widths" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(usize, 24), hostTabLabelMaxLen(260)); + try std.testing.expectEqual(@as(usize, 9), hostTabLabelMaxLen(98)); + try std.testing.expectEqual(@as(usize, 6), hostTabLabelMaxLen(40)); + try std.testing.expect(shouldShowPaneCount(180, 3)); + try std.testing.expect(!shouldShowPaneCount(120, 3)); +} + +test "win32 visibleTabRange windows tabs around the active tab" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqualDeep(VisibleTabRange{ .start = 0, .count = 3 }, visibleTabRange(6, 0, 324)); + try std.testing.expectEqualDeep(VisibleTabRange{ .start = 2, .count = 3 }, visibleTabRange(6, 3, 324)); + try std.testing.expectEqualDeep(VisibleTabRange{ .start = 3, .count = 3 }, visibleTabRange(6, 5, 324)); + try std.testing.expectEqualDeep(VisibleTabRange{ .start = 0, .count = 2 }, visibleTabRange(2, 1, 500)); +} + +test "win32 buildTabOverviewBannerText lists active tabs and pane counts" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const banner = try buildTabOverviewBannerText(std.testing.allocator, &.{ + .{ .title = "pwsh", .pane_count = 1, .active = true }, + .{ .title = "logs-and-output-pane", .pane_count = 3, .active = false }, + }); + defer std.testing.allocator.free(banner); + try std.testing.expectEqualStrings("Tabs: *1:pwsh | 2:logs-and-output... (3)", banner); +} + +test "win32 buildSearchButtonLabel reflects active search state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const active = try buildSearchButtonLabel(std.testing.allocator, true, 8, 2); + defer std.testing.allocator.free(active); + try std.testing.expectEqualStrings("[F] 2/8", active); + + const passive = try buildSearchButtonLabel(std.testing.allocator, false, 5, null); + defer std.testing.allocator.free(passive); + try std.testing.expectEqualStrings("Find 5", passive); + + const idle = try buildSearchButtonLabel(std.testing.allocator, false, null, null); + defer std.testing.allocator.free(idle); + try std.testing.expectEqualStrings("Find", idle); +} + +test "win32 buildProfilesButtonLabel reflects selected cached profile" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profiles = [_]windows_shell.Profile{ + .{ + .kind = .pwsh, + .key = "pwsh.exe", + .label = "PowerShell", + .command = .{ .direct = &.{"pwsh.exe"} }, + }, + .{ + .kind = .git_bash, + .key = "git-bash", + .label = "Git Bash", + .command = .{ .direct = &.{"bash.exe"} }, + }, + }; + + const active = try buildProfilesButtonLabel(std.testing.allocator, true, &profiles, 1, 1); + defer std.testing.allocator.free(active); + try std.testing.expectEqualStrings("Pick Git Bash", active); + + const idle = try buildProfilesButtonLabel(std.testing.allocator, false, &profiles, 0, null); + defer std.testing.allocator.free(idle); + try std.testing.expectEqualStrings("Launch Power...", idle); +} + +test "win32 profilesButtonKeyAction maps focused launcher keys" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(ProfilesButtonKeyAction.open, profilesButtonKeyAction(c.VK_RETURN).?); + try std.testing.expectEqual(ProfilesButtonKeyAction.toggle, profilesButtonKeyAction(c.VK_SPACE).?); + try std.testing.expectEqual(ProfilesButtonKeyAction.previous, profilesButtonKeyAction(c.VK_LEFT).?); + try std.testing.expectEqual(ProfilesButtonKeyAction.next, profilesButtonKeyAction(c.VK_DOWN).?); + try std.testing.expectEqual(ProfilesButtonKeyAction.first, profilesButtonKeyAction(c.VK_HOME).?); + try std.testing.expectEqual(ProfilesButtonKeyAction.last, profilesButtonKeyAction(c.VK_END).?); +} + +test "win32 profileShortcutIndexFromKey supports top row and numpad digits" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(?usize, 0), profileShortcutIndexFromKey('1')); + try std.testing.expectEqual(@as(?usize, 4), profileShortcutIndexFromKey('5')); + try std.testing.expectEqual(@as(?usize, 8), profileShortcutIndexFromKey('9')); + try std.testing.expectEqual(@as(?usize, 0), profileShortcutIndexFromKey(0x61)); + try std.testing.expectEqual(@as(?usize, 8), profileShortcutIndexFromKey(0x69)); + try std.testing.expectEqual(@as(?usize, null), profileShortcutIndexFromKey('0')); + try std.testing.expectEqual(@as(?usize, null), profileShortcutIndexFromKey('A')); +} + +test "win32 quickSlotShortcutProfileIndex maps visible launcher slots" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(?usize, 1), quickSlotShortcutProfileIndex(5, 0, '1', true)); + try std.testing.expectEqual(@as(?usize, 2), quickSlotShortcutProfileIndex(5, 0, '2', true)); + try std.testing.expectEqual(@as(?usize, 3), quickSlotShortcutProfileIndex(5, 0, '3', true)); + try std.testing.expectEqual(@as(?usize, 0), quickSlotShortcutProfileIndex(5, 3, '1', true)); + try std.testing.expectEqual(@as(?usize, 1), quickSlotShortcutProfileIndex(5, 3, '2', true)); + try std.testing.expectEqual(@as(?usize, null), quickSlotShortcutProfileIndex(5, 0, '4', true)); + try std.testing.expectEqual(@as(?usize, null), quickSlotShortcutProfileIndex(5, 0, '1', false)); +} + +test "win32 quickSlotPinOrdinalFromKey maps visible pin slots" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(?usize, 0), quickSlotPinOrdinalFromKey('1', true, true)); + try std.testing.expectEqual(@as(?usize, 2), quickSlotPinOrdinalFromKey('3', true, true)); + try std.testing.expectEqual(@as(?usize, null), quickSlotPinOrdinalFromKey('4', true, true)); + try std.testing.expectEqual(@as(?usize, null), quickSlotPinOrdinalFromKey('1', true, false)); + try std.testing.expectEqual(@as(?usize, null), quickSlotPinOrdinalFromKey('1', false, true)); +} + +test "win32 clearQuickSlotPinsRequested detects clear shortcut" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(clearQuickSlotPinsRequested(c.VK_0, true, true)); + try std.testing.expect(clearQuickSlotPinsRequested(c.VK_NUMPAD0, true, true)); + try std.testing.expect(!clearQuickSlotPinsRequested('1', true, true)); + try std.testing.expect(!clearQuickSlotPinsRequested(c.VK_0, true, false)); + try std.testing.expect(!clearQuickSlotPinsRequested(c.VK_0, false, true)); +} + +test "win32 quickSlotFocusKeyAction maps painted quick slot focus keys" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(QuickSlotFocusKeyAction.previous, quickSlotFocusKeyAction(c.VK_LEFT).?); + try std.testing.expectEqual(QuickSlotFocusKeyAction.previous, quickSlotFocusKeyAction(c.VK_UP).?); + try std.testing.expectEqual(QuickSlotFocusKeyAction.next, quickSlotFocusKeyAction(c.VK_RIGHT).?); + try std.testing.expectEqual(QuickSlotFocusKeyAction.next, quickSlotFocusKeyAction(c.VK_DOWN).?); + try std.testing.expectEqual(QuickSlotFocusKeyAction.first, quickSlotFocusKeyAction(c.VK_HOME).?); + try std.testing.expectEqual(QuickSlotFocusKeyAction.last, quickSlotFocusKeyAction(c.VK_END).?); + try std.testing.expectEqual(QuickSlotFocusKeyAction.open, quickSlotFocusKeyAction(c.VK_RETURN).?); + try std.testing.expect(quickSlotFocusKeyAction(c.VK_SPACE) == null); +} + +test "win32 progress status and taskbar mapping clamp percent to shell range" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const report: terminal.osc.Command.ProgressReport = .{ + .state = .set, + .progress = 255, + }; + + const mapped = win32_taskbar_progress.mapProgressReport(report); + try std.testing.expectEqual(@as(u64, 100), mapped.value.?.completed); + try std.testing.expectEqual(@as(u64, 100), mapped.value.?.total); + + const status = try formatProgressStatus(std.testing.allocator, report); + defer if (status) |owned| std.testing.allocator.free(owned); + try std.testing.expectEqualStrings("progress:100%", status.?); +} + +test "win32 launchTargetButtonLabel reflects selected launcher slot" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const pane = try launchTargetButtonLabel(std.testing.allocator, .split, 2, 2); + defer std.testing.allocator.free(pane); + try std.testing.expectEqualStrings("*3 Pane", pane); + + const tab = try launchTargetButtonLabel(std.testing.allocator, .tab, null, null); + defer std.testing.allocator.free(tab); + try std.testing.expectEqualStrings("Tab", tab); +} + +test "win32 launchTargetButtonLabel reflects Windows-style target wording" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const tab = try launchTargetButtonLabel(std.testing.allocator, .tab, null, null); + defer std.testing.allocator.free(tab); + try std.testing.expectEqualStrings("Tab", tab); + + const win = try launchTargetButtonLabel(std.testing.allocator, .window, null, null); + defer std.testing.allocator.free(win); + try std.testing.expectEqualStrings("Win", win); + + const pane = try launchTargetButtonLabel(std.testing.allocator, .split, null, null); + defer std.testing.allocator.free(pane); + try std.testing.expectEqualStrings("Pane", pane); +} + +test "win32 preferredProfileIndex respects host key then app key then hint" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profiles = [_]windows_shell.Profile{ + .{ + .kind = .pwsh, + .key = "pwsh.exe", + .label = "PowerShell", + .command = .{ .direct = &.{"pwsh.exe"} }, + }, + .{ + .kind = .git_bash, + .key = "git-bash", + .label = "Git Bash", + .command = .{ .direct = &.{"bash.exe"} }, + }, + .{ + .kind = .cmd, + .key = "cmd.exe", + .label = "Command Prompt", + .command = .{ .direct = &.{"cmd.exe"} }, + }, + }; + + try std.testing.expectEqual(@as(?usize, 1), preferredProfileIndex(&profiles, "git-bash", null, "cmd", 0)); + try std.testing.expectEqual(@as(?usize, 2), preferredProfileIndex(&profiles, null, "cmd.exe", "pwsh", 0)); + try std.testing.expectEqual(@as(?usize, 0), preferredProfileIndex(&profiles, null, null, "power", 2)); + try std.testing.expectEqual(@as(?usize, null), preferredProfileIndex(&profiles, null, null, "definitely_not_real", 1)); +} + +test "win32 resolveProfileSelection supports index and prefix matching" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profiles = [_]windows_shell.Profile{ + .{ + .kind = .pwsh, + .key = "pwsh.exe", + .label = "PowerShell", + .command = .{ .direct = &.{"pwsh.exe"} }, + }, + .{ + .kind = .wsl_distro, + .key = "Ubuntu", + .label = "WSL: Ubuntu", + .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu", "~" } }, + }, + .{ + .kind = .wsl_distro, + .key = "Ubuntu-Preview", + .label = "WSL: Ubuntu Preview", + .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu-Preview", "~" } }, + }, + }; + + try std.testing.expectEqualDeep(ProfileSelection{ .exact = 0 }, resolveProfileSelection(&profiles, "", 0)); + try std.testing.expectEqualDeep(ProfileSelection{ .exact = 1 }, resolveProfileSelection(&profiles, "2", 0)); + try std.testing.expectEqualDeep(ProfileSelection{ .exact = 0 }, resolveProfileSelection(&profiles, "powers", 1)); + try std.testing.expectEqualDeep(ProfileSelection{ .ambiguous = 2 }, resolveProfileSelection(&profiles, "ubu", 0)); + try std.testing.expectEqualDeep(ProfileSelection.invalid, resolveProfileSelection(&profiles, "9", 0)); +} + +test "win32 buildProfileOverlayLabel and hint reflect selected profile" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profiles = [_]windows_shell.Profile{ + .{ + .kind = .pwsh, + .key = "pwsh.exe", + .label = "PowerShell", + .command = .{ .direct = &.{"pwsh.exe"} }, + }, + .{ + .kind = .cmd, + .key = "cmd.exe", + .label = "Command Prompt", + .command = .{ .direct = &.{"cmd.exe"} }, + }, + }; + + const label = try buildProfileOverlayLabel(std.testing.allocator, &profiles, "cmd", 0); + defer std.testing.allocator.free(label); + try std.testing.expectEqualStrings("Profile 2/2 CMD C>", label); + + const hint = try buildProfileHintText(std.testing.allocator, &profiles, "cmd", 0, .window, .{ "pwsh.exe", "cmd.exe", null }); + defer std.testing.allocator.free(hint); + try std.testing.expect(std.mem.indexOf(u8, hint, "CMD C>") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "Command Prompt") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "run cmd.exe") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "Pinned slot 2.") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "opens a new window") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "Alt+1-3 launches visible slots") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "Alt+Shift+1-3 pins the current profile") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "Alt+Shift+0 clears pinning") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "Quick picks: 1 PWSH >>") != null); + try std.testing.expect(std.mem.indexOf(u8, hint, "2 CMD C>") != null); +} + +test "win32 buildProfileDetailText reflects selected launcher state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profile: windows_shell.Profile = .{ + .kind = .pwsh, + .key = "pwsh.exe", + .label = "PowerShell", + .command = .{ .direct = &.{"pwsh.exe"} }, + }; + const profiles = [_]windows_shell.Profile{ + profile, + .{ + .kind = .git_bash, + .key = "git-bash", + .label = "Git Bash", + .command = .{ .direct = &.{"bash.exe"} }, + }, + .{ + .kind = .wsl_distro, + .key = "wsl:Ubuntu", + .label = "WSL: Ubuntu", + .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu" } }, + }, + }; + + const overlay = try buildProfileDetailText(std.testing.allocator, &profile, &profiles, true, .split, "git,pwsh,Ubuntu,cmd", .{ "pwsh.exe", "git-bash", null }); + defer std.testing.allocator.free(overlay); + try std.testing.expect(std.mem.indexOf(u8, overlay, "Selected profile: PWSH >> PowerShell") != null); + try std.testing.expect(std.mem.indexOf(u8, overlay, "Run pwsh.exe") != null); + try std.testing.expect(std.mem.indexOf(u8, overlay, "Pinned slot 1.") != null); + try std.testing.expect(std.mem.indexOf(u8, overlay, "opens a split") != null); + try std.testing.expect(std.mem.indexOf(u8, overlay, "Alt+1-3 launches visible slots") != null); + try std.testing.expect(std.mem.indexOf(u8, overlay, "Alt+Shift+1-3 pins the current profile") != null); + try std.testing.expect(std.mem.indexOf(u8, overlay, "Alt+Shift+0 clears pinning") != null); + try std.testing.expect(std.mem.indexOf(u8, overlay, "Quick picks: 1 PWSH >> PowerShell") != null); + try std.testing.expect(std.mem.indexOf(u8, overlay, "Order: git > pwsh > Ubuntu > cmd.") != null); + + const idle = try buildProfileDetailText(std.testing.allocator, &profile, &profiles, false, .window, "git,pwsh,Ubuntu,cmd", .{ "pwsh.exe", "git-bash", null }); + defer std.testing.allocator.free(idle); + try std.testing.expect(std.mem.indexOf(u8, idle, "Default profile: PWSH >> PowerShell") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "Run pwsh.exe") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "Pinned slot 1.") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "New hosts inherit this PowerShell profile with automatic shell integration.") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "opens a new window") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "Alt+1-3 launches visible slots") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "Alt+Shift+1-3 pins the current profile") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "Alt+Shift+0 clears pinning") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "Top slots: 1 PWSH >>") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "2 GIT $> Git Bash") != null); + try std.testing.expect(std.mem.indexOf(u8, idle, "Order: git > pwsh > Ubuntu > cmd.") != null); +} + +test "win32 buildProfileDetailText appends shell integration guidance when present" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profile: windows_shell.Profile = .{ + .kind = .cmd, + .key = "cmd.exe", + .label = "Command Prompt", + .command = .{ .direct = &.{"cmd.exe"} }, + }; + + const detail = try buildProfileDetailText( + std.testing.allocator, + &profile, + &.{profile}, + false, + .tab, + null, + .{ null, null, null }, + ); + defer std.testing.allocator.free(detail); + + try std.testing.expect(std.mem.indexOf( + u8, + detail, + "New hosts inherit this Command Prompt profile; shell integration unavailable.", + ) != null); + try std.testing.expect(std.mem.indexOf( + u8, + detail, + "Use PowerShell or WSL when prompt marking or cwd inheritance is required.", + ) != null); + try std.testing.expect(std.mem.indexOf(u8, detail, "+ opens a new tab") != null); +} + +test "win32 buildProfileCommandPreviewText compacts shell command preview" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profile: windows_shell.Profile = .{ + .kind = .wsl_distro, + .key = "Ubuntu", + .label = "WSL: Ubuntu", + .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu", "--cd", "~" } }, + }; + + const preview = try buildProfileCommandPreviewText(std.testing.allocator, &profile, 14); + defer std.testing.allocator.free(preview); + try std.testing.expectEqualStrings("wsl.exe -d ...", preview); +} + +test "win32 buildProfileOrderSummaryText compacts launcher order" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const summary = (try buildProfileOrderSummaryText( + std.testing.allocator, + "git,pwsh,Ubuntu,cmd,powershell", + 4, + )).?; + defer std.testing.allocator.free(summary); + try std.testing.expectEqualStrings("git > pwsh > Ubuntu > cmd > ...", summary); +} + +test "win32 buildProfileQuickPickText reflects ordered launcher profiles" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profiles = [_]windows_shell.Profile{ + .{ + .kind = .git_bash, + .key = "git-bash", + .label = "Git Bash", + .command = .{ .direct = &.{"bash.exe"} }, + }, + .{ + .kind = .pwsh, + .key = "pwsh.exe", + .label = "PowerShell", + .command = .{ .direct = &.{"pwsh.exe"} }, + }, + .{ + .kind = .wsl_distro, + .key = "wsl:Ubuntu", + .label = "WSL: Ubuntu", + .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu" } }, + }, + .{ + .kind = .cmd, + .key = "cmd.exe", + .label = "Command Prompt", + .command = .{ .direct = &.{"cmd.exe"} }, + }, + .{ + .kind = .powershell, + .key = "powershell.exe", + .label = "Windows PowerShell", + .command = .{ .direct = &.{"powershell.exe"} }, + }, + }; + + const quick = (try buildProfileQuickPickText(std.testing.allocator, &profiles, 4, 10)).?; + defer std.testing.allocator.free(quick); + try std.testing.expectEqualStrings( + "1 GIT $> Git Bash | 2 PWSH >> PowerShell | 3 WSL <> WSL: Ub... | 4 CMD C> Command... | ...", + quick, + ); +} + +test "win32 buildProfileStatusBadgeText reflects selected profile kind" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profile: windows_shell.Profile = .{ + .kind = .git_bash, + .key = "git-bash", + .label = "Git Bash", + .command = .{ .direct = &.{"bash.exe"} }, + }; + + const badge = try buildProfileStatusBadgeText(std.testing.allocator, &profile, 0, 0); + defer std.testing.allocator.free(badge); + try std.testing.expectEqualStrings("Git Bash", badge); +} + +test "win32 profileStatusBadgeTextLen matches built text" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profile: windows_shell.Profile = .{ + .kind = .git_bash, + .key = "git-bash", + .label = "Git Bash", + .command = .{ .direct = &.{"bash.exe"} }, + }; + + const badge = try buildProfileStatusBadgeText(std.testing.allocator, &profile, 0, 0); + defer std.testing.allocator.free(badge); + try std.testing.expectEqual(badge.len, profileStatusBadgeTextLen(&profile, 0, 0)); +} + +test "win32 buildProfileQuickSlotChipText reflects ordered quick slot badge" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profile: windows_shell.Profile = .{ + .kind = .wsl_distro, + .key = "wsl:Ubuntu", + .label = "WSL: Ubuntu", + .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu" } }, + }; + + const chip = try buildProfileQuickSlotChipText(std.testing.allocator, &profile, 2, 2); + defer std.testing.allocator.free(chip); + try std.testing.expectEqualStrings("*3 WSL", chip); +} + +test "win32 profileQuickSlotChipTextLen matches built text" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const profile: windows_shell.Profile = .{ + .kind = .wsl_distro, + .key = "wsl:Ubuntu", + .label = "WSL: Ubuntu", + .command = .{ .direct = &.{ "wsl.exe", "-d", "Ubuntu" } }, + }; + + const pinned = try buildProfileQuickSlotChipText(std.testing.allocator, &profile, 2, 2); + defer std.testing.allocator.free(pinned); + try std.testing.expectEqual(pinned.len, profileQuickSlotChipTextLen(&profile, 2, 2)); + + const unpinned = try buildProfileQuickSlotChipText(std.testing.allocator, &profile, 1, null); + defer std.testing.allocator.free(unpinned); + try std.testing.expectEqual(unpinned.len, profileQuickSlotChipTextLen(&profile, 1, null)); +} + +test "win32 launcherChipRightInset reserves badge and target space" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(i32, 5), launcherChipRightInset(false, false)); + try std.testing.expectEqual(@as(i32, 12), launcherChipRightInset(false, true)); + try std.testing.expectEqual(@as(i32, 16), launcherChipRightInset(true, false)); + try std.testing.expectEqual(@as(i32, 16), launcherChipRightInset(true, true)); +} + +test "win32 targetButtonLabelRightInset reserves target badge space" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(i32, 0), targetButtonLabelRightInset(null)); + try std.testing.expectEqual(@as(i32, 12), targetButtonLabelRightInset(.tab)); + try std.testing.expectEqual(@as(i32, 12), targetButtonLabelRightInset(.window)); + try std.testing.expectEqual(@as(i32, 12), targetButtonLabelRightInset(.split)); +} + +test "win32 buttonLabelRightInset reserves slot and target badge space" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(i32, 0), buttonLabelRightInset(null, null)); + try std.testing.expectEqual(@as(i32, 12), buttonLabelRightInset(null, .tab)); + try std.testing.expectEqual(@as(i32, 16), buttonLabelRightInset(0, null)); + try std.testing.expectEqual(@as(i32, 16), buttonLabelRightInset(0, .split)); + try std.testing.expectEqual(@as(i32, 12), buttonLabelRightInset(9, .window)); +} + +test "win32 shouldPaintQuickSlotTargetMarker follows active chip state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!shouldPaintQuickSlotTargetMarker(false, false)); + try std.testing.expect(shouldPaintQuickSlotTargetMarker(true, false)); + try std.testing.expect(shouldPaintQuickSlotTargetMarker(false, true)); +} + +test "win32 profileOpenTargetMarkerColor reflects launcher target" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(rgb(132, 172, 238), profileOpenTargetMarkerColor(.tab)); + try std.testing.expectEqual(rgb(236, 182, 118), profileOpenTargetMarkerColor(.window)); + try std.testing.expectEqual(rgb(126, 204, 148), profileOpenTargetMarkerColor(.split)); +} + +test "win32 profileOpenTargetBadgeGlyph reflects launcher target" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(u8, 'T'), profileOpenTargetBadgeGlyph(.tab)); + try std.testing.expectEqual(@as(u8, 'W'), profileOpenTargetBadgeGlyph(.window)); + try std.testing.expectEqual(@as(u8, 'S'), profileOpenTargetBadgeGlyph(.split)); +} + +test "win32 pinnedSlotBadgeDigit reflects visible quick slot ordinals" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(?u8, '1'), pinnedSlotBadgeDigit(0)); + try std.testing.expectEqual(@as(?u8, '3'), pinnedSlotBadgeDigit(2)); + try std.testing.expectEqual(@as(?u8, null), pinnedSlotBadgeDigit(null)); + try std.testing.expectEqual(@as(?u8, null), pinnedSlotBadgeDigit(9)); +} + +test "win32 quickSlotProfileIndex skips the selected profile and preserves order" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(?usize, 1), quickSlotProfileIndex(5, 0, 0, 3)); + try std.testing.expectEqual(@as(?usize, 2), quickSlotProfileIndex(5, 0, 1, 3)); + try std.testing.expectEqual(@as(?usize, 3), quickSlotProfileIndex(5, 0, 2, 3)); + try std.testing.expectEqual(@as(?usize, 0), quickSlotProfileIndex(5, 3, 0, 3)); + try std.testing.expectEqual(@as(?usize, 1), quickSlotProfileIndex(5, 3, 1, 3)); + try std.testing.expectEqual(@as(?usize, 2), quickSlotProfileIndex(5, 3, 2, 3)); + try std.testing.expectEqual(@as(?usize, null), quickSlotProfileIndex(1, 0, 0, 3)); +} + +test "win32 findLauncherQuickSlotOrdinal finds runtime-pinned slots" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(?usize, 0), findLauncherQuickSlotOrdinal(.{ "git-bash", "cmd.exe", null }, "git-bash")); + try std.testing.expectEqual(@as(?usize, 1), findLauncherQuickSlotOrdinal(.{ "git-bash", "cmd.exe", null }, "CMD.EXE")); + try std.testing.expectEqual(@as(?usize, null), findLauncherQuickSlotOrdinal(.{ "git-bash", "cmd.exe", null }, "pwsh.exe")); +} + +test "win32 buildProfileChromeBadgeText adds profile glyph treatment" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const pwsh = try buildProfileChromeBadgeText(std.testing.allocator, .pwsh); + defer std.testing.allocator.free(pwsh); + try std.testing.expectEqualStrings("PWSH >>", pwsh); + + const wsl = try buildProfileChromeBadgeText(std.testing.allocator, .wsl_distro); + defer std.testing.allocator.free(wsl); + try std.testing.expectEqualStrings("WSL <>", wsl); +} + +test "win32 profileKindDetail exposes shell integration posture" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const pwsh = profileKindDetail(.pwsh); + try std.testing.expectEqualStrings( + "PowerShell profile with automatic shell integration", + pwsh.summary, + ); + try std.testing.expectEqual(@as(?[]const u8, null), pwsh.next_step); + + const wsl = profileKindDetail(.wsl_distro); + try std.testing.expectEqualStrings( + "WSL distro profile; shell integration depends on the Linux shell", + wsl.summary, + ); + try std.testing.expectEqualStrings( + "Enable shell integration inside the selected WSL shell startup.", + wsl.next_step.?, + ); + + const git_bash = profileKindDetail(.git_bash); + try std.testing.expectEqualStrings( + "Git Bash profile with automatic shell integration", + git_bash.summary, + ); + try std.testing.expectEqual(@as(?[]const u8, null), git_bash.next_step); + + const cmd = profileKindDetail(.cmd); + try std.testing.expectEqualStrings( + "Command Prompt profile; shell integration unavailable", + cmd.summary, + ); + try std.testing.expectEqualStrings( + "Use PowerShell or WSL when prompt marking or cwd inheritance is required.", + cmd.next_step.?, + ); +} + +test "win32 buildSearchOverlayLabel reflects match counts" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const active = try buildSearchOverlayLabel(std.testing.allocator, 8, 2); + defer std.testing.allocator.free(active); + try std.testing.expectEqualStrings("Find 2/8", active); + + const passive = try buildSearchOverlayLabel(std.testing.allocator, 5, null); + defer std.testing.allocator.free(passive); + try std.testing.expectEqualStrings("Find 5", passive); + + const idle = try buildSearchOverlayLabel(std.testing.allocator, null, null); + defer std.testing.allocator.free(idle); + try std.testing.expectEqualStrings("Find", idle); +} + +test "win32 buildSearchBarResultsText reflects docked search states" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + var bar = win32_search_bar.SearchBar.init(std.testing.allocator); + defer bar.deinit(); + + const idle = try buildSearchBarResultsText(std.testing.allocator, &bar); + defer std.testing.allocator.free(idle); + try std.testing.expectEqualStrings(search_results_idle, idle); + + try bar.setQuery("foo", 10); + const pending = try buildSearchBarResultsText(std.testing.allocator, &bar); + defer std.testing.allocator.free(pending); + try std.testing.expectEqualStrings(search_results_pending, pending); + + bar.searched = true; + bar.total = 0; + const none = try buildSearchBarResultsText(std.testing.allocator, &bar); + defer std.testing.allocator.free(none); + try std.testing.expectEqualStrings(search_results_none, none); + + bar.total = 12; + bar.selected = 3; + const active = try buildSearchBarResultsText(std.testing.allocator, &bar); + defer std.testing.allocator.free(active); + try std.testing.expectEqualStrings("3/12", active); +} + +test "win32 showSearchBarResults only shows status chip for non-empty queries" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + var bar = win32_search_bar.SearchBar.init(std.testing.allocator); + defer bar.deinit(); + + try std.testing.expect(!showSearchBarResults(&bar)); + try bar.setQuery("search-open", 10); + try std.testing.expect(showSearchBarResults(&bar)); +} + +test "win32 searchBarNeedsRelayoutForQueryChange only trips on empty-state transitions" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!searchBarNeedsRelayoutForQueryChange(0, 0)); + try std.testing.expect(searchBarNeedsRelayoutForQueryChange(0, 1)); + try std.testing.expect(!searchBarNeedsRelayoutForQueryChange(4, 9)); + try std.testing.expect(searchBarNeedsRelayoutForQueryChange(3, 0)); +} + +test "win32 searchBarShouldInvalidateCoreSearchOnEdit invalidates active query edits" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!searchBarShouldInvalidateCoreSearchOnEdit(0, 0)); + try std.testing.expect(!searchBarShouldInvalidateCoreSearchOnEdit(0, 1)); + try std.testing.expect(searchBarShouldInvalidateCoreSearchOnEdit(5, 2)); + try std.testing.expect(searchBarShouldInvalidateCoreSearchOnEdit(5, 0)); +} + +test "win32 shouldAcceptCoreSearchUpdates rejects pending docked search state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + var bar = win32_search_bar.SearchBar.init(std.testing.allocator); + defer bar.deinit(); + + try std.testing.expect(shouldAcceptCoreSearchUpdates(&bar)); + + try bar.setQuery("search-open", 10); + try std.testing.expect(!shouldAcceptCoreSearchUpdates(&bar)); + + bar.searched = true; + try std.testing.expect(shouldAcceptCoreSearchUpdates(&bar)); +} + +test "win32 profileChromeNeedsFullTextInvalidation only trips for status-only profile chrome" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!profileChromeNeedsFullTextInvalidation(.profile, 0)); + try std.testing.expect(!profileChromeNeedsFullTextInvalidation(.profile, 24)); + try std.testing.expect(!profileChromeNeedsFullTextInvalidation(.none, 0)); + try std.testing.expect(profileChromeNeedsFullTextInvalidation(.none, 24)); +} + +test "win32 chromeTextNeedsFullInvalidation only trips when status bar is visible" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!chromeTextNeedsFullInvalidation(0)); + try std.testing.expect(chromeTextNeedsFullInvalidation(24)); +} + +test "win32 searchBarResultsVisual marks no-match state with error colors" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + var bar = win32_search_bar.SearchBar.init(std.testing.allocator); + defer bar.deinit(); + + try bar.setQuery("zzzzz", 10); + bar.searched = true; + bar.total = 0; + + const theme = darkTheme(); + const visual = searchBarResultsVisual(&theme, &bar); + try std.testing.expectEqual(theme.error_fg, visual.border); + try std.testing.expectEqual(theme.error_fg, visual.fg); +} + +test "win32 searchBarButtonShowsLabel keeps docked search buttons icon-only" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!searchBarButtonShowsLabel(.prev, 80)); + try std.testing.expect(!searchBarButtonShowsLabel(.close, 80)); + try std.testing.expect(!searchBarButtonShowsLabel(.regex, 40)); + try std.testing.expect(!searchBarButtonShowsLabel(.regex, 64)); + try std.testing.expect(!searchBarButtonShowsLabel(.case_sensitive, 72)); + try std.testing.expect(!searchBarButtonShowsLabel(.whole_word, 80)); +} + +test "win32 buildTabOverviewOverlayLabel reflects current host tab" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const multi = try buildTabOverviewOverlayLabel(std.testing.allocator, 1, 4); + defer std.testing.allocator.free(multi); + try std.testing.expectEqualStrings("Tab 2/4", multi); + + const single = try buildTabOverviewOverlayLabel(std.testing.allocator, 0, 1); + defer std.testing.allocator.free(single); + try std.testing.expectEqualStrings("Tab", single); +} + +test "win32 buildOverlayPaintLabelText reflects live overlay mode" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const command = try buildOverlayPaintLabelText( + std.testing.allocator, + .command_palette, + "toggle_", + null, + null, + .{}, + .{ .match_count = 4, .title = "Toggle fullscreen", .subtitle = "Fullscreen", .available = true }, + ); + defer std.testing.allocator.free(command); + try std.testing.expectEqualStrings("Command 4", command); + + const search = try buildOverlayPaintLabelText( + std.testing.allocator, + .search, + "", + 8, + 2, + .{}, + .{}, + ); + defer std.testing.allocator.free(search); + try std.testing.expectEqualStrings("Find 2/8", search); + + const title = try buildOverlayPaintLabelText( + std.testing.allocator, + .surface_title, + "", + null, + null, + .{}, + .{}, + ); + defer std.testing.allocator.free(title); + try std.testing.expectEqualStrings("Window title", title); +} + +test "win32 buildOverlayFeedbackText prefers inline banner state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const snap = PaletteSnapshot.fromDefaults(); + const empty_mru: []const []const u8 = &.{}; + + const info = try buildOverlayFeedbackText( + std.testing.allocator, + .info, + "Try: new_tab", + .command_palette, + "", + null, + null, + null, + .{}, + 1, + snap, + empty_mru, + .{}, + ); + defer std.testing.allocator.free(info); + try std.testing.expectEqualStrings("Info: Try: new_tab", info); + + const err = try buildOverlayFeedbackText( + std.testing.allocator, + .err, + command_palette_unknown_action, + .command_palette, + "", + null, + null, + null, + .{}, + 1, + snap, + empty_mru, + .{}, + ); + defer std.testing.allocator.free(err); + try std.testing.expectEqualStrings( + "Error: Unknown Noctty action. Example: new_tab or toggle_fullscreen", + err, + ); + + const fallback = try buildOverlayFeedbackText( + std.testing.allocator, + .none, + null, + .search, + "needle", + "needle", + 8, + 2, + .{}, + 1, + snap, + empty_mru, + .{}, + ); + defer std.testing.allocator.free(fallback); + try std.testing.expect(std.mem.indexOf(u8, fallback, "next match") != null); +} + +test "win32 nextTabOverviewSelection wraps and clamps" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(usize, 2), nextTabOverviewSelection(1, 4, false)); + try std.testing.expectEqual(@as(usize, 4), nextTabOverviewSelection(1, 4, true)); + try std.testing.expectEqual(@as(usize, 1), nextTabOverviewSelection(4, 4, false)); + try std.testing.expectEqual(@as(usize, 1), nextTabOverviewSelection(9, 4, false)); +} + +test "win32 tabDirectionFromWheelDelta maps wheel direction to tab navigation" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(apprt.action.GotoTab.previous, tabDirectionFromWheelDelta(120)); + try std.testing.expectEqual(apprt.action.GotoTab.next, tabDirectionFromWheelDelta(-120)); +} + +test "win32 searchDirectionFromWheelDelta maps wheel direction to search navigation" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(input.Binding.Action.NavigateSearch.next, searchDirectionFromWheelDelta(120)); + try std.testing.expectEqual(input.Binding.Action.NavigateSearch.previous, searchDirectionFromWheelDelta(-120)); +} + +test "win32 searchBarSearchedState helpers preserve pending searches on null callbacks" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!searchBarSearchedStateForTotal(null)); + try std.testing.expect(searchBarSearchedStateForTotal(0)); + + try std.testing.expect(!searchBarSearchedStateForSelected(null, null)); + try std.testing.expect(searchBarSearchedStateForSelected(null, 5)); + try std.testing.expect(searchBarSearchedStateForSelected(2, null)); +} + +test "win32 searchBarDisplayStateChanged only trips on visible results changes" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const same = win32_search_bar.SearchBar{ + .searched = true, + .total = 12, + .selected = 3, + .alloc = std.testing.allocator, + }; + try std.testing.expect(!searchBarDisplayStateChanged(&same, true, 12, 3)); + try std.testing.expect(searchBarDisplayStateChanged(&same, false, 12, 3)); + try std.testing.expect(searchBarDisplayStateChanged(&same, true, 11, 3)); + try std.testing.expect(searchBarDisplayStateChanged(&same, true, 12, 4)); +} + +test "win32 scrollStatusTextChanged only trips on indicator visibility or percent deltas" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const hidden_bottom: terminal.Scrollbar = .{ .total = 2000, .len = 21, .offset = 1979 }; + const hidden_short: terminal.Scrollbar = .{ .total = 21, .len = 21, .offset = 0 }; + const visible_50: terminal.Scrollbar = .{ .total = 2000, .len = 21, .offset = 1000 }; + const visible_50_neighbor: terminal.Scrollbar = .{ .total = 2000, .len = 21, .offset = 1001 }; + const visible_51: terminal.Scrollbar = .{ .total = 2000, .len = 21, .offset = 1020 }; + + try std.testing.expect(!scrollStatusTextChanged(hidden_short, hidden_bottom)); + try std.testing.expect(scrollStatusTextChanged(hidden_bottom, visible_50)); + try std.testing.expect(!scrollStatusTextChanged(visible_50, visible_50_neighbor)); + try std.testing.expect(scrollStatusTextChanged(visible_50, visible_51)); +} + +test "win32 commandPaletteDirectionFromWheelDelta maps wheel direction to completion direction" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(commandPaletteDirectionFromWheelDelta(120)); + try std.testing.expect(!commandPaletteDirectionFromWheelDelta(-120)); +} + +test "win32 profileDirectionFromWheelDelta maps wheel direction to profile navigation" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(profileDirectionFromWheelDelta(120)); + try std.testing.expect(!profileDirectionFromWheelDelta(-120)); +} + +test "win32 tabButtonKeyAction maps focused-tab keys" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(TabButtonKeyAction.previous, tabButtonKeyAction(c.VK_LEFT, false).?); + try std.testing.expectEqual(TabButtonKeyAction.move_previous, tabButtonKeyAction(c.VK_LEFT, true).?); + try std.testing.expectEqual(TabButtonKeyAction.next, tabButtonKeyAction(c.VK_RIGHT, false).?); + try std.testing.expectEqual(TabButtonKeyAction.move_next, tabButtonKeyAction(c.VK_RIGHT, true).?); + try std.testing.expectEqual(TabButtonKeyAction.first, tabButtonKeyAction(c.VK_HOME, false).?); + try std.testing.expectEqual(TabButtonKeyAction.move_first, tabButtonKeyAction(c.VK_HOME, true).?); + try std.testing.expectEqual(TabButtonKeyAction.last, tabButtonKeyAction(c.VK_END, false).?); + try std.testing.expectEqual(TabButtonKeyAction.move_last, tabButtonKeyAction(c.VK_END, true).?); + try std.testing.expectEqual(TabButtonKeyAction.rename, tabButtonKeyAction(c.VK_F2, false).?); + try std.testing.expectEqual(TabButtonKeyAction.close, tabButtonKeyAction(c.VK_DELETE, false).?); + try std.testing.expectEqual(TabButtonKeyAction.overview, tabButtonKeyAction(c.VK_APPS, false).?); + try std.testing.expect(tabButtonKeyAction(c.VK_RETURN, false) == null); +} + +test "win32 moveTabAmountToEdge computes direct host reorder delta" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(isize, -2), moveTabAmountToEdge(4, 2, true)); + try std.testing.expectEqual(@as(isize, 1), moveTabAmountToEdge(4, 2, false)); + try std.testing.expectEqual(@as(isize, 0), moveTabAmountToEdge(4, 0, true)); + try std.testing.expectEqual(@as(isize, 0), moveTabAmountToEdge(4, 3, false)); +} + +test "win32 searchButtonKeyAction maps focused search button keys" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(SearchButtonKeyAction.next, searchButtonKeyAction(c.VK_F3, false).?); + try std.testing.expectEqual(SearchButtonKeyAction.previous, searchButtonKeyAction(c.VK_F3, true).?); + try std.testing.expectEqual(SearchButtonKeyAction.dismiss, searchButtonKeyAction(c.VK_ESCAPE, false).?); + try std.testing.expect(searchButtonKeyAction(c.VK_RETURN, false) == null); +} + +test "win32 docked search key actions preserve semantic next and previous navigation" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual( + input.Binding.Action.NavigateSearch.next, + dockedSearchCoreDirectionFromKeyAction(.next).?, + ); + try std.testing.expectEqual( + input.Binding.Action.NavigateSearch.previous, + dockedSearchCoreDirectionFromKeyAction(.previous).?, + ); + try std.testing.expect(dockedSearchCoreDirectionFromKeyAction(.dismiss) == null); +} + +test "win32 docked search arrow buttons follow visible up and down navigation" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual( + input.Binding.Action.NavigateSearch.next, + dockedSearchButtonDirection(c.SEARCH_PREV_ID).?, + ); + try std.testing.expectEqual( + input.Binding.Action.NavigateSearch.previous, + dockedSearchButtonDirection(c.SEARCH_NEXT_ID).?, + ); + try std.testing.expect(dockedSearchButtonDirection(0) == null); +} + +test "win32 docked search Enter and Shift+Enter follow the arrow-key contract" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual( + dockedSearchArrowDirection(c.VK_DOWN).?, + dockedSearchEnterDirection(false), + ); + try std.testing.expectEqual( + dockedSearchArrowDirection(c.VK_UP).?, + dockedSearchEnterDirection(true), + ); +} + +test "win32 docked search Up and Down keys follow the visible button contract" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual( + input.Binding.Action.NavigateSearch.next, + dockedSearchArrowDirection(c.VK_UP).?, + ); + try std.testing.expectEqual( + input.Binding.Action.NavigateSearch.previous, + dockedSearchArrowDirection(c.VK_DOWN).?, + ); + try std.testing.expect(dockedSearchArrowDirection(c.VK_LEFT) == null); +} + +test "win32 docked search selection preserves newest-first visible numbering" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(?usize, 0), searchSelectedRawFromCoreDisplay(1)); + try std.testing.expectEqual(@as(?usize, 7), searchSelectedRawFromCoreDisplay(8)); + try std.testing.expectEqual(@as(?usize, 1), searchSelectedDisplayFromRaw(0, 8)); + try std.testing.expectEqual(@as(?usize, 8), searchSelectedDisplayFromRaw(7, 8)); +} + +test "win32 docked search preview advances visible numbering with navigation" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(@as(?usize, 6), advanceSearchSelectedRaw(7, 8, .previous, true)); + try std.testing.expectEqual(@as(?usize, 0), advanceSearchSelectedRaw(7, 8, .next, true)); + try std.testing.expectEqual(@as(?usize, 7), searchSelectedDisplayFromRaw( + advanceSearchSelectedRaw(7, 8, .previous, true), + 8, + )); + try std.testing.expectEqual(@as(?usize, 1), searchSelectedDisplayFromRaw( + advanceSearchSelectedRaw(7, 8, .next, true), + 8, + )); +} + +test "win32 docked search sequential visible order increments while moving older and up" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const total: ?usize = 2500; + var raw: ?usize = null; + + try std.testing.expectEqual(@as(?usize, null), searchSelectedDisplayFromRaw(raw, total)); + + raw = searchSelectedRawFromCoreDisplay(1); + try std.testing.expectEqual(@as(?usize, 1), searchSelectedDisplayFromRaw(raw, total)); + + raw = advanceSearchSelectedRaw(raw, total, .next, true); + try std.testing.expectEqual(@as(?usize, 2), searchSelectedDisplayFromRaw(raw, total)); + + raw = advanceSearchSelectedRaw(raw, total, .next, true); + try std.testing.expectEqual(@as(?usize, 3), searchSelectedDisplayFromRaw(raw, total)); + + raw = advanceSearchSelectedRaw(raw, total, .previous, true); + try std.testing.expectEqual(@as(?usize, 2), searchSelectedDisplayFromRaw(raw, total)); + + raw = advanceSearchSelectedRaw(raw, total, .previous, true); + try std.testing.expectEqual(@as(?usize, 1), searchSelectedDisplayFromRaw(raw, total)); +} + +test "win32 tabsButtonKeyAction maps focused tabs button keys" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(TabsButtonKeyAction.previous, tabsButtonKeyAction(c.VK_LEFT).?); + try std.testing.expectEqual(TabsButtonKeyAction.previous, tabsButtonKeyAction(c.VK_UP).?); + try std.testing.expectEqual(TabsButtonKeyAction.next, tabsButtonKeyAction(c.VK_RIGHT).?); + try std.testing.expectEqual(TabsButtonKeyAction.next, tabsButtonKeyAction(c.VK_DOWN).?); + try std.testing.expectEqual(TabsButtonKeyAction.rename, tabsButtonKeyAction(c.VK_F2).?); + try std.testing.expectEqual(TabsButtonKeyAction.overview, tabsButtonKeyAction(c.VK_APPS).?); + try std.testing.expect(tabsButtonKeyAction(c.VK_RETURN) == null); +} + +test "win32 commandButtonKeyAction maps focused command button keys" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(CommandButtonKeyAction.toggle, commandButtonKeyAction(c.VK_RETURN).?); + try std.testing.expectEqual(CommandButtonKeyAction.toggle, commandButtonKeyAction(c.VK_SPACE).?); + try std.testing.expectEqual(CommandButtonKeyAction.previous, commandButtonKeyAction(c.VK_UP).?); + try std.testing.expectEqual(CommandButtonKeyAction.next, commandButtonKeyAction(c.VK_DOWN).?); + try std.testing.expectEqual(CommandButtonKeyAction.dismiss, commandButtonKeyAction(c.VK_ESCAPE).?); + try std.testing.expect(commandButtonKeyAction(c.VK_F2) == null); +} + +test "win32 command palette toggle binding is recognized inside action chains" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const absent = [_]input.Binding.Action{ .{ .copy_to_clipboard = .mixed }, .paste_from_clipboard }; + const present = [_]input.Binding.Action{ .{ .copy_to_clipboard = .mixed }, .toggle_command_palette }; + try std.testing.expect(!bindingActionsToggleCommandPalette(&absent)); + try std.testing.expect(bindingActionsToggleCommandPalette(&present)); +} + +test "win32 buildInspectorBannerText reflects host inspector context" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const multi = try buildInspectorBannerText(std.testing.allocator, .{ .index = 1, .total = 4 }, 3, false); + defer std.testing.allocator.free(multi); + try std.testing.expectEqualStrings("Inspector active | tab 2/4 | panes 3 | toggle Inspect to return", multi); + + const zoomed = try buildInspectorBannerText(std.testing.allocator, .{ .index = 0, .total = 2 }, 2, true); + defer std.testing.allocator.free(zoomed); + try std.testing.expectEqualStrings("Inspector active | tab 1/2 | panes 2 | zoomed | toggle Inspect to return", zoomed); + + const single = try buildInspectorBannerText(std.testing.allocator, .{ .index = 0, .total = 1 }, 1, false); + defer std.testing.allocator.free(single); + try std.testing.expectEqualStrings("Inspector active | tab 1/1 | toggle Inspect to return", single); +} + +test "win32 buildHostBannerText prefixes info and error banners" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const plain = try buildHostBannerText(std.testing.allocator, .none, "ready"); + defer std.testing.allocator.free(plain); + try std.testing.expectEqualStrings("ready", plain); + + const info = try buildHostBannerText(std.testing.allocator, .info, "ready"); + defer std.testing.allocator.free(info); + try std.testing.expectEqualStrings("Info: ready", info); + + const err = try buildHostBannerText(std.testing.allocator, .err, "ready"); + defer std.testing.allocator.free(err); + try std.testing.expectEqualStrings("Error: ready", err); +} + +test "win32 overlayPaintCacheDirty ignores repaint-only dirtiness" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const cached = std.unicode.utf8ToUtf16LeStringLiteral("ok"); + + try std.testing.expect(!overlayPaintCacheDirty(false, .search, cached, cached, null)); + try std.testing.expect(!overlayPaintCacheDirty(false, .profile, cached, cached, cached)); + + try std.testing.expect(overlayPaintCacheDirty(true, .search, cached, cached, null)); + try std.testing.expect(overlayPaintCacheDirty(false, .search, null, cached, null)); + try std.testing.expect(overlayPaintCacheDirty(false, .profile, cached, cached, null)); +} + +test "win32 profileChromeVisible only trips for profile overlay or visible status bar" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(profileChromeVisible(.profile, 0)); + try std.testing.expect(profileChromeVisible(.none, 24)); + try std.testing.expect(!profileChromeVisible(.none, 0)); + try std.testing.expect(!profileChromeVisible(.search, 0)); +} + +test "win32 inspector chrome visibility only trips for banner or visible status bar" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(inspectorChromeVisible(.none, 0)); + try std.testing.expect(inspectorChromeVisible(.search, 24)); + try std.testing.expect(!inspectorChromeVisible(.search, 0)); +} + +test "win32 inspector visibility changes require host relayout" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(inspectorVisibilityChangeNeedsHostRelayout(true)); + try std.testing.expect(!inspectorVisibilityChangeNeedsHostRelayout(false)); +} + +test "win32 inspector panel visibility is tab scoped" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(inspectorPanelVisibleForState(.none, true)); + try std.testing.expect(!inspectorPanelVisibleForState(.none, false)); + try std.testing.expect(!inspectorPanelVisibleForState(.command_palette, true)); +} + +test "win32 command palette hides duplicate accept button" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!overlayAcceptButtonVisible(.command_palette)); + try std.testing.expect(overlayAcceptButtonVisible(.profile)); + try std.testing.expect(overlayAcceptButtonVisible(.search)); + try std.testing.expect(overlayAcceptButtonVisible(.confirm)); + try std.testing.expect(!overlayEditFrameVisible(.confirm)); + try std.testing.expect(overlayEditFrameVisible(.command_palette)); + try std.testing.expect(overlayEditFrameVisible(.profile)); +} + +test "win32 transient overlay focus ring includes every visible control" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqual(OverlayFocusSlot.accept, nextOverlayFocusSlot(.profile, .edit, false)); + try std.testing.expectEqual(OverlayFocusSlot.cancel, nextOverlayFocusSlot(.profile, .accept, false)); + try std.testing.expectEqual(OverlayFocusSlot.edit, nextOverlayFocusSlot(.profile, .cancel, false)); + try std.testing.expectEqual(OverlayFocusSlot.cancel, nextOverlayFocusSlot(.profile, .edit, true)); + try std.testing.expectEqual(OverlayFocusSlot.edit, nextOverlayFocusSlot(.profile, .accept, true)); + try std.testing.expectEqual(OverlayFocusSlot.accept, nextOverlayFocusSlot(.profile, .cancel, true)); + + try std.testing.expectEqual(OverlayFocusSlot.cancel, nextOverlayFocusSlot(.command_palette, .edit, false)); + try std.testing.expectEqual(OverlayFocusSlot.edit, nextOverlayFocusSlot(.command_palette, .cancel, true)); + try std.testing.expectEqual(OverlayFocusSlot.cancel, nextOverlayFocusSlot(.confirm, .accept, false)); + try std.testing.expectEqual(OverlayFocusSlot.accept, nextOverlayFocusSlot(.confirm, .cancel, true)); + + try std.testing.expectEqual( + OverlayFocusSlot.cancel, + nextVisibleOverlayFocusSlot(.profile, .edit, false, true, false, true), + ); + try std.testing.expectEqual( + OverlayFocusSlot.cancel, + nextVisibleOverlayFocusSlot(.confirm, .accept, false, false, false, true), + ); + try std.testing.expectEqual( + OverlayFocusSlot.accept, + nextVisibleOverlayFocusSlot(.confirm, .cancel, true, false, true, true), + ); + try std.testing.expectEqual( + null, + nextVisibleOverlayFocusSlot(.command_palette, .edit, false, true, false, false), + ); + try std.testing.expectEqual( + null, + nextVisibleOverlayFocusSlot(.confirm, .cancel, false, false, false, true), + ); +} + +test "win32 inspectorBannerStateChanged only trips on actual banner deltas" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(!inspectorBannerStateChanged(.none, .none, null, true)); + try std.testing.expect(inspectorBannerStateChanged(.none, .none, null, false)); + try std.testing.expect(!inspectorBannerStateChanged(.none, .info, host_banner_inspector_inactive, false)); + try std.testing.expect(inspectorBannerStateChanged(.none, .err, "error", false)); + try std.testing.expect(!inspectorBannerStateChanged(.search, .none, null, true)); +} + +test "win32 windowTitleSyncChanged only trips on actual title deltas" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expect(windowTitleSyncChanged(null, "noctty")); + try std.testing.expect(!windowTitleSyncChanged("noctty", "noctty")); + try std.testing.expect(windowTitleSyncChanged("noctty", "noctty - 2")); +} + +test "win32 buildInspectorPanelTitleText reflects host inspector context" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const multi = try buildInspectorPanelTitleText(std.testing.allocator, .{ .index = 1, .total = 4 }, 3, false); + defer std.testing.allocator.free(multi); + try std.testing.expect(std.mem.indexOf(u8, multi, "Inspector") != null); + try std.testing.expect(std.mem.indexOf(u8, multi, "tab 2/4") != null); + try std.testing.expect(std.mem.indexOf(u8, multi, "3 panes") != null); + + const zoomed = try buildInspectorPanelTitleText(std.testing.allocator, .{ .index = 0, .total = 2 }, 2, true); + defer std.testing.allocator.free(zoomed); + try std.testing.expect(std.mem.indexOf(u8, zoomed, "zoomed") != null); +} + +test "win32 buildInspectorPanelHintText reflects live inspector scope" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const single = try buildInspectorPanelHintText(std.testing.allocator, 1, false); + defer std.testing.allocator.free(single); + try std.testing.expect(std.mem.indexOf(u8, single, "this tab") != null); + + const multi = try buildInspectorPanelHintText(std.testing.allocator, 3, false); + defer std.testing.allocator.free(multi); + try std.testing.expect(std.mem.indexOf(u8, multi, "3 panes") != null); + + const zoomed = try buildInspectorPanelHintText(std.testing.allocator, 2, true); + defer std.testing.allocator.free(zoomed); + try std.testing.expect(std.mem.indexOf(u8, zoomed, "zoomed pane") != null); +} + +test "win32 buildInspectorDetailText reflects pane and zoom context" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const multi = try buildInspectorDetailText(std.testing.allocator, .{ .index = 1, .total = 4 }, 3, false); + defer std.testing.allocator.free(multi); + try std.testing.expect(std.mem.indexOf(u8, multi, "tab 2/4") != null); + try std.testing.expect(std.mem.indexOf(u8, multi, "3 panes") != null); + + const zoomed = try buildInspectorDetailText(std.testing.allocator, .{ .index = 0, .total = 2 }, 2, true); + defer std.testing.allocator.free(zoomed); + try std.testing.expect(std.mem.indexOf(u8, zoomed, "zoom") != null); +} + +test "win32 buildSearchDetailText reflects live search context" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const active = try buildSearchDetailText(std.testing.allocator, "needle", 8, 2); + defer std.testing.allocator.free(active); + try std.testing.expect(std.mem.indexOf(u8, active, "needle") != null); + try std.testing.expect(std.mem.indexOf(u8, active, "2 of 8") != null); + + const pending = try buildSearchDetailText(std.testing.allocator, "logs", null, null); + defer std.testing.allocator.free(pending); + try std.testing.expect(std.mem.indexOf(u8, pending, "refine") != null); +} + +test "win32 buildOverlayAcceptLabel reflects overlay action state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const palette_idle = try buildOverlayAcceptLabel(std.testing.allocator, .command_palette, "", null, null, null, .{}); + defer std.testing.allocator.free(palette_idle); + try std.testing.expectEqualStrings("Close", palette_idle); + + const palette_run = try buildOverlayAcceptLabel( + std.testing.allocator, + .command_palette, + "new_tab", + null, + null, + null, + .{ .match_count = 1, .title = "New tab", .subtitle = "Action", .available = true }, + ); + defer std.testing.allocator.free(palette_run); + try std.testing.expectEqualStrings("Activate", palette_run); + + const palette_matches = try buildOverlayAcceptLabel( + std.testing.allocator, + .command_palette, + "0x96f", + null, + null, + null, + .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme", .available = true }, + ); + defer std.testing.allocator.free(palette_matches); + try std.testing.expectEqualStrings("Activate", palette_matches); + + const palette_hidden = try buildOverlayAcceptLabel( + std.testing.allocator, + .command_palette, + "0x96f", + null, + null, + null, + .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme" }, + ); + defer std.testing.allocator.free(palette_hidden); + try std.testing.expectEqualStrings("Resize", palette_hidden); + + const search_next = try buildOverlayAcceptLabel(std.testing.allocator, .search, "needle", "needle", 8, 2, .{}); + defer std.testing.allocator.free(search_next); + try std.testing.expectEqualStrings("Next", search_next); + + const search_find = try buildOverlayAcceptLabel(std.testing.allocator, .search, "other", "needle", 8, 2, .{}); + defer std.testing.allocator.free(search_find); + try std.testing.expectEqualStrings("Find", search_find); + + const tab_go = try buildOverlayAcceptLabel(std.testing.allocator, .tab_overview, "2", null, null, null, .{}); + defer std.testing.allocator.free(tab_go); + try std.testing.expectEqualStrings("Go", tab_go); + + const title_apply = try buildOverlayAcceptLabel(std.testing.allocator, .surface_title, "logs", null, null, null, .{}); + defer std.testing.allocator.free(title_apply); + try std.testing.expectEqualStrings("Apply", title_apply); +} + +test "win32 buildOverlayHintText reflects live overlay guidance" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const snap = PaletteSnapshot.fromDefaults(); + const empty_mru: []const []const u8 = &.{}; + + const command_unique = try buildOverlayHintText( + std.testing.allocator, + .command_palette, + "reload_", + null, + null, + null, + .{}, + 1, + snap, + empty_mru, + ); + defer std.testing.allocator.free(command_unique); + try std.testing.expect(std.mem.indexOf(u8, command_unique, "reload_config") != null); + + const search_next = try buildOverlayHintText( + std.testing.allocator, + .search, + "needle", + "needle", + 8, + 2, + .{}, + 1, + snap, + empty_mru, + ); + defer std.testing.allocator.free(search_next); + try std.testing.expect(std.mem.indexOf(u8, search_next, "2/8") != null); + try std.testing.expect(std.mem.indexOf(u8, search_next, "next match") != null); + + const tab_invalid = try buildOverlayHintText( + std.testing.allocator, + .tab_overview, + "8", + null, + null, + null, + .{ .index = 1, .total = 4 }, + 2, + snap, + empty_mru, + ); + defer std.testing.allocator.free(tab_invalid); + try std.testing.expect(std.mem.indexOf(u8, tab_invalid, "out of range") != null); + + const tab_title = try buildOverlayHintText( + std.testing.allocator, + .tab_title, + "logs", + null, + null, + null, + .{ .index = 0, .total = 3 }, + 2, + snap, + empty_mru, + ); + defer std.testing.allocator.free(tab_title); + try std.testing.expect(std.mem.indexOf(u8, tab_title, "tab 1/3") != null); + try std.testing.expect(std.mem.indexOf(u8, tab_title, "2 panes") != null); +} + +test "win32 overlayCancelLabel reflects overlay mode" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + try std.testing.expectEqualStrings("Close", overlayCancelLabel(.search)); + try std.testing.expectEqualStrings("Cancel", overlayCancelLabel(.surface_title)); +} + +test "win32 buildCommandButtonLabel reflects live palette state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const active = try buildCommandButtonLabel(std.testing.allocator, true, "toggle_fullscreen"); + defer std.testing.allocator.free(active); + try std.testing.expectEqualStrings("Cmd toggle...", active); + + const armed = try buildCommandButtonLabel(std.testing.allocator, true, ""); + defer std.testing.allocator.free(armed); + try std.testing.expectEqualStrings("[Cmd]", armed); + + const idle = try buildCommandButtonLabel(std.testing.allocator, false, null); + defer std.testing.allocator.free(idle); + try std.testing.expectEqualStrings("Cmd", idle); +} + +test "win32 buildInspectorButtonLabel reflects inspector and pane state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const visible = try buildInspectorButtonLabel(std.testing.allocator, true, 3); + defer std.testing.allocator.free(visible); + try std.testing.expectEqualStrings("[Inspect 3]", visible); + + const multi = try buildInspectorButtonLabel(std.testing.allocator, false, 2); + defer std.testing.allocator.free(multi); + try std.testing.expectEqualStrings("Inspect 2", multi); + + const idle = try buildInspectorButtonLabel(std.testing.allocator, false, 1); + defer std.testing.allocator.free(idle); + try std.testing.expectEqualStrings("Inspect", idle); +} + +test "win32 buildCommandPaletteOverlayLabel reflects palette state" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const idle = try buildCommandPaletteOverlayLabel(std.testing.allocator, "", .{}); + defer std.testing.allocator.free(idle); + try std.testing.expectEqualStrings("Command", idle); + + const matches = try buildCommandPaletteOverlayLabel( + std.testing.allocator, + "0x96f", + .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme", .available = true }, + ); + defer std.testing.allocator.free(matches); + try std.testing.expectEqualStrings("Command 1", matches); + + const invalid = try buildCommandPaletteOverlayLabel( + std.testing.allocator, + "definitely_not_real", + .{}, + ); + defer std.testing.allocator.free(invalid); + try std.testing.expectEqualStrings("Command ?", invalid); +} + +test "win32 command palette rich feedback never calls a theme an action miss" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const feedback = try buildCommandPaletteFeedbackText( + std.testing.allocator, + "0x96f", + &.{}, + .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme", .available = true }, + ); + defer std.testing.allocator.free(feedback); + try std.testing.expect(std.mem.indexOf(u8, feedback, "0x96f") != null); + try std.testing.expect(std.mem.indexOf(u8, feedback, "Bundled theme") != null); + try std.testing.expect(std.mem.indexOf(u8, feedback, "No matching action") == null); + + const hidden_feedback = try buildCommandPaletteFeedbackText( + std.testing.allocator, + "0x96f", + &.{}, + .{ .match_count = 1, .title = "0x96f", .subtitle = "Bundled theme" }, + ); + defer std.testing.allocator.free(hidden_feedback); + try std.testing.expect(std.mem.indexOf(u8, hidden_feedback, "window larger") != null); +} + +test "win32 commandPaletteCompletionCandidate resolves and cycles defaults" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const snap = PaletteSnapshot.fromDefaults(); + + try std.testing.expectEqualStrings( + "reload_config", + commandPaletteCompletionCandidate(snap, "reload_", "reload_", false).?, + ); + + // toggle_* has multiple matches in defaults; first-next/reverse should + // land on entries that all begin with "toggle_". + const first = commandPaletteCompletionCandidate(snap, "toggle_", "toggle_", false).?; + try std.testing.expect(std.mem.startsWith(u8, first, "toggle_")); + + const second = commandPaletteCompletionCandidate(snap, "toggle_", first, false).?; + try std.testing.expect(std.mem.startsWith(u8, second, "toggle_")); + try std.testing.expect(!std.mem.eql(u8, first, second)); + + const reverse = commandPaletteCompletionCandidate(snap, "toggle_", first, true).?; + try std.testing.expect(std.mem.startsWith(u8, reverse, "toggle_")); + try std.testing.expect(!std.mem.eql(u8, reverse, first)); +} + +test "win32 commandPaletteBannerText shows ready banner for valid action" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const snap = PaletteSnapshot.fromDefaults(); + const empty_mru: []const []const u8 = &.{}; + const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "new_tab", empty_mru)).?; + defer std.testing.allocator.free(banner); + try std.testing.expect(std.mem.startsWith(u8, banner, "Ready: new_tab")); + try std.testing.expect(std.mem.indexOf(u8, banner, "Open a new tab") != null); +} + +test "win32 commandPaletteBannerText suggests matching actions" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const snap = PaletteSnapshot.fromDefaults(); + const empty_mru: []const []const u8 = &.{}; + const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "new_", empty_mru)).?; + defer std.testing.allocator.free(banner); + // Top-5 match cap means a specific "new_split:*" may or may not + // appear, but the banner should list at least new_tab and at + // least one new_split variant. + try std.testing.expect(std.mem.indexOf(u8, banner, "new_tab") != null); + try std.testing.expect(std.mem.indexOf(u8, banner, "new_split") != null); +} + +test "win32 commandPaletteBannerText resolves unique prefix" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const snap = PaletteSnapshot.fromDefaults(); + const empty_mru: []const []const u8 = &.{}; + const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "reload_", empty_mru)).?; + defer std.testing.allocator.free(banner); + try std.testing.expect(std.mem.startsWith(u8, banner, "Ready: reload_config")); + try std.testing.expect(std.mem.indexOf(u8, banner, "Reload") != null); +} + +test "win32 commandPaletteBannerText uses fullscreen description" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const snap = PaletteSnapshot.fromDefaults(); + const empty_mru: []const []const u8 = &.{}; + const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "toggle_fullscreen", empty_mru)).?; + defer std.testing.allocator.free(banner); + try std.testing.expect(std.mem.startsWith(u8, banner, "Ready: toggle_fullscreen")); + try std.testing.expect(std.mem.indexOf(u8, banner, "fullscreen") != null); +} + +test "win32 commandPaletteBannerText suggests tab overview action" { + if (builtin.os.tag != .windows) return error.SkipZigTest; + + const snap = PaletteSnapshot.fromDefaults(); + const empty_mru: []const []const u8 = &.{}; + const banner = (try commandPaletteBannerText(std.testing.allocator, snap, "toggle_tab", empty_mru)).?; + defer std.testing.allocator.free(banner); + try std.testing.expect(std.mem.indexOf(u8, banner, "toggle_tab_overview") != null); + // "tab overview" is part of the default description verbatim. + try std.testing.expect(std.mem.indexOf(u8, banner, "tab overview") != null); +} + +// End of labels declarations. diff --git a/src/apprt/win32/render_trace.zig b/src/apprt/win32/render_trace.zig new file mode 100644 index 00000000000..44331f04e62 --- /dev/null +++ b/src/apprt/win32/render_trace.zig @@ -0,0 +1,368 @@ +//! Opt-in Win32 renderer trace collection. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const internal_os = @import("../../os/main.zig"); +const sys = @import("sys.zig"); + +const log = std.log.scoped(.win32); + +var render_trace_file_claimed = std.atomic.Value(bool).init(false); + +pub const RenderTrace = struct { + const startup_window_ms: u64 = 1000; + const startup_paint_gap_ceiling_ms: u64 = 1500; + const paint_gap_limit_ms: u64 = 300; + + const PaintGapTargets = struct { + max_gap_ms: *std.atomic.Value(u64), + max_gap_ended_at_ms: *std.atomic.Value(u64), + over_limit_count: *std.atomic.Value(u64), + }; + + path: ?[]const u8 = null, + start_tick_ms: u64 = 0, + renderer_update_frame_count: std.atomic.Value(u64) = .init(0), + renderer_draw_request_count: std.atomic.Value(u64) = .init(0), + wakeup_callback_count: std.atomic.Value(u64) = .init(0), + render_callback_count: std.atomic.Value(u64) = .init(0), + renderer_repaint_accept_count: std.atomic.Value(u64) = .init(0), + renderer_repaint_coalesced_count: std.atomic.Value(u64) = .init(0), + queue_paint_count: std.atomic.Value(u64) = .init(0), + queue_paint_update_now_count: std.atomic.Value(u64) = .init(0), + force_paint_now_count: std.atomic.Value(u64) = .init(0), + paint_draw_count: std.atomic.Value(u64) = .init(0), + paint_retry_count: std.atomic.Value(u64) = .init(0), + swap_buffers_count: std.atomic.Value(u64) = .init(0), + max_renderer_update_gap_ms: std.atomic.Value(u64) = .init(0), + max_paint_gap_ms: std.atomic.Value(u64) = .init(0), + max_swap_gap_ms: std.atomic.Value(u64) = .init(0), + max_renderer_update_gap_ended_at_ms: std.atomic.Value(u64) = .init(0), + max_paint_gap_ended_at_ms: std.atomic.Value(u64) = .init(0), + max_swap_gap_ended_at_ms: std.atomic.Value(u64) = .init(0), + max_sustained_paint_gap_ms: std.atomic.Value(u64) = .init(0), + max_sustained_paint_gap_ended_at_ms: std.atomic.Value(u64) = .init(0), + paint_gap_over_limit_count: std.atomic.Value(u64) = .init(0), + max_paint_draw_duration_ms: std.atomic.Value(u64) = .init(0), + max_paint_draw_duration_at_ms: std.atomic.Value(u64) = .init(0), + paint_draw_duration_over_20ms_count: std.atomic.Value(u64) = .init(0), + paint_draw_duration_over_33ms_count: std.atomic.Value(u64) = .init(0), + last_renderer_update_tick_ms: std.atomic.Value(u64) = .init(0), + last_paint_tick_ms: std.atomic.Value(u64) = .init(0), + last_swap_tick_ms: std.atomic.Value(u64) = .init(0), + first_renderer_update_at_ms: std.atomic.Value(u64) = .init(0), + first_paint_at_ms: std.atomic.Value(u64) = .init(0), + first_swap_at_ms: std.atomic.Value(u64) = .init(0), + + pub fn init(alloc: Allocator) RenderTrace { + const owned = (internal_os.getEnvVarOwnedTrimmedNotEmpty( + alloc, + "NOCTTY_RENDER_TRACE_FILE", + ) catch return .{}) orelse return .{}; + const trace_path = absolutizeTracePath(alloc, owned) catch |err| { + log.warn("unable to resolve render trace path err={}", .{err}); + return .{}; + }; + + return initWithClaimedPath(alloc, &render_trace_file_claimed, trace_path); + } + + fn initWithClaimedPath( + alloc: Allocator, + claimed: *std.atomic.Value(bool), + owned: []const u8, + ) RenderTrace { + // A trace path names one JSON document. The interactive harness waits + // for the initial surface before seeding tabs, so the first claimant + // is deterministic. Keeping that claim for the process lifetime stops + // later tab teardown from overwriting the initial surface's evidence. + const trace_path = claimTracePath(alloc, claimed, owned) orelse + return .{}; + return .{ + .path = trace_path, + .start_tick_ms = sys.GetTickCount64(), + }; + } + + pub fn deinit(self: *RenderTrace, alloc: Allocator) void { + defer { + if (self.path) |path| alloc.free(path); + self.* = .{}; + } + + if (self.path == null) return; + self.writeSnapshot(); + } + + pub fn enabled(self: *const RenderTrace) bool { + return self.path != null; + } + + pub fn noteRendererUpdateFrame(self: *RenderTrace) void { + if (!self.enabled()) return; + self.noteTimedCounter( + &self.renderer_update_frame_count, + &self.last_renderer_update_tick_ms, + &self.max_renderer_update_gap_ms, + &self.max_renderer_update_gap_ended_at_ms, + &self.first_renderer_update_at_ms, + null, + ); + } + + pub fn noteRendererDrawRequest(self: *RenderTrace) void { + if (!self.enabled()) return; + _ = self.renderer_draw_request_count.fetchAdd(1, .acq_rel); + } + + pub fn noteRendererWakeupCallback(self: *RenderTrace) void { + if (!self.enabled()) return; + _ = self.wakeup_callback_count.fetchAdd(1, .acq_rel); + } + + pub fn noteRendererFollowupCallback(self: *RenderTrace) void { + if (!self.enabled()) return; + _ = self.render_callback_count.fetchAdd(1, .acq_rel); + } + + pub fn noteRendererRepaintAccepted(self: *RenderTrace) void { + if (!self.enabled()) return; + _ = self.renderer_repaint_accept_count.fetchAdd(1, .acq_rel); + } + + pub fn noteRendererRepaintCoalesced(self: *RenderTrace) void { + if (!self.enabled()) return; + _ = self.renderer_repaint_coalesced_count.fetchAdd(1, .acq_rel); + } + + pub fn noteQueuePaint(self: *RenderTrace, update_now: bool) void { + if (!self.enabled()) return; + _ = self.queue_paint_count.fetchAdd(1, .acq_rel); + if (update_now) _ = self.queue_paint_update_now_count.fetchAdd(1, .acq_rel); + } + + pub fn noteForcePaintNow(self: *RenderTrace) void { + if (!self.enabled()) return; + _ = self.force_paint_now_count.fetchAdd(1, .acq_rel); + } + + pub fn notePaintDraw(self: *RenderTrace) void { + if (!self.enabled()) return; + self.noteTimedCounter( + &self.paint_draw_count, + &self.last_paint_tick_ms, + &self.max_paint_gap_ms, + &self.max_paint_gap_ended_at_ms, + &self.first_paint_at_ms, + .{ + .max_gap_ms = &self.max_sustained_paint_gap_ms, + .max_gap_ended_at_ms = &self.max_sustained_paint_gap_ended_at_ms, + .over_limit_count = &self.paint_gap_over_limit_count, + }, + ); + } + + pub fn notePaintDrawDuration(self: *RenderTrace, duration_ms: u64) void { + if (!self.enabled()) return; + updateMaxAtomicWithTimestamp( + &self.max_paint_draw_duration_ms, + &self.max_paint_draw_duration_at_ms, + duration_ms, + elapsedTraceMs(self.start_tick_ms, sys.GetTickCount64()), + ); + if (duration_ms > 20) _ = self.paint_draw_duration_over_20ms_count.fetchAdd(1, .acq_rel); + if (duration_ms > 33) _ = self.paint_draw_duration_over_33ms_count.fetchAdd(1, .acq_rel); + } + + pub fn notePaintRetry(self: *RenderTrace) void { + if (!self.enabled()) return; + _ = self.paint_retry_count.fetchAdd(1, .acq_rel); + } + + pub fn noteSwapBuffers(self: *RenderTrace) void { + if (!self.enabled()) return; + self.noteTimedCounter( + &self.swap_buffers_count, + &self.last_swap_tick_ms, + &self.max_swap_gap_ms, + &self.max_swap_gap_ended_at_ms, + &self.first_swap_at_ms, + null, + ); + } + + fn noteTimedCounter( + self: *RenderTrace, + counter: *std.atomic.Value(u64), + last_tick_ms: *std.atomic.Value(u64), + max_gap_ms: *std.atomic.Value(u64), + max_gap_ended_at_ms: *std.atomic.Value(u64), + first_at_ms: *std.atomic.Value(u64), + paint_gaps: ?PaintGapTargets, + ) void { + const now = sys.GetTickCount64(); + const elapsed_ms = elapsedTraceMs(self.start_tick_ms, now); + _ = counter.fetchAdd(1, .acq_rel); + _ = first_at_ms.cmpxchgStrong(0, elapsed_ms, .acq_rel, .acquire); + const prev = last_tick_ms.swap(now, .acq_rel); + if (prev == 0 or now <= prev) return; + const gap_ms = now - prev; + updateMaxAtomicWithTimestamp( + max_gap_ms, + max_gap_ended_at_ms, + gap_ms, + elapsed_ms, + ); + if (paint_gaps) |targets| { + if (gapExceedsPaintLimit(gap_ms)) { + _ = targets.over_limit_count.fetchAdd(1, .acq_rel); + } + if (gapIsSustained(elapsed_ms, gap_ms)) { + updateMaxAtomicWithTimestamp( + targets.max_gap_ms, + targets.max_gap_ended_at_ms, + gap_ms, + elapsed_ms, + ); + } + } + } + + fn gapIsSustained(elapsed_ms: u64, gap_ms: u64) bool { + return elapsed_ms -| gap_ms >= startup_window_ms; + } + + fn gapExceedsPaintLimit(gap_ms: u64) bool { + return gap_ms > paint_gap_limit_ms; + } + + fn writeSnapshot(self: *const RenderTrace) void { + const trace_path = self.path orelse return; + const file = std.fs.createFileAbsolute(trace_path, .{ .truncate = true }) catch |err| { + log.warn("unable to create render trace file path={s} err={}", .{ trace_path, err }); + return; + }; + defer file.close(); + + var buffer: [1024]u8 = undefined; + var writer = file.writer(&buffer); + const stream = &writer.interface; + stream.print("{f}", .{std.json.fmt(.{ + .runtime_ms = sys.GetTickCount64() - self.start_tick_ms, + .startup_window_ms = startup_window_ms, + .startup_paint_gap_ceiling_ms = startup_paint_gap_ceiling_ms, + .paint_gap_limit_ms = paint_gap_limit_ms, + .renderer_update_frame_count = self.renderer_update_frame_count.load(.acquire), + .renderer_draw_request_count = self.renderer_draw_request_count.load(.acquire), + .wakeup_callback_count = self.wakeup_callback_count.load(.acquire), + .render_callback_count = self.render_callback_count.load(.acquire), + .renderer_repaint_accept_count = self.renderer_repaint_accept_count.load(.acquire), + .renderer_repaint_coalesced_count = self.renderer_repaint_coalesced_count.load(.acquire), + .queue_paint_count = self.queue_paint_count.load(.acquire), + .queue_paint_update_now_count = self.queue_paint_update_now_count.load(.acquire), + .force_paint_now_count = self.force_paint_now_count.load(.acquire), + .paint_draw_count = self.paint_draw_count.load(.acquire), + .paint_retry_count = self.paint_retry_count.load(.acquire), + .swap_buffers_count = self.swap_buffers_count.load(.acquire), + .max_renderer_update_gap_ms = self.max_renderer_update_gap_ms.load(.acquire), + .max_paint_gap_ms = self.max_paint_gap_ms.load(.acquire), + .max_swap_gap_ms = self.max_swap_gap_ms.load(.acquire), + .max_renderer_update_gap_ended_at_ms = self.max_renderer_update_gap_ended_at_ms.load(.acquire), + .max_paint_gap_ended_at_ms = self.max_paint_gap_ended_at_ms.load(.acquire), + .max_swap_gap_ended_at_ms = self.max_swap_gap_ended_at_ms.load(.acquire), + .max_sustained_paint_gap_ms = self.max_sustained_paint_gap_ms.load(.acquire), + .max_sustained_paint_gap_ended_at_ms = self.max_sustained_paint_gap_ended_at_ms.load(.acquire), + .paint_gap_over_limit_count = self.paint_gap_over_limit_count.load(.acquire), + .max_paint_draw_duration_ms = self.max_paint_draw_duration_ms.load(.acquire), + .max_paint_draw_duration_at_ms = self.max_paint_draw_duration_at_ms.load(.acquire), + .paint_draw_duration_over_20ms_count = self.paint_draw_duration_over_20ms_count.load(.acquire), + .paint_draw_duration_over_33ms_count = self.paint_draw_duration_over_33ms_count.load(.acquire), + .first_renderer_update_at_ms = self.first_renderer_update_at_ms.load(.acquire), + .first_paint_at_ms = self.first_paint_at_ms.load(.acquire), + .first_swap_at_ms = self.first_swap_at_ms.load(.acquire), + }, .{})}) catch return; + stream.flush() catch return; + } +}; + +fn absolutizeTracePath(alloc: Allocator, owned: []const u8) ![]const u8 { + if (std.fs.path.isAbsolute(owned)) return owned; + defer alloc.free(owned); + const cwd = try std.process.getCwdAlloc(alloc); + defer alloc.free(cwd); + return try std.fs.path.join(alloc, &.{ cwd, owned }); +} + +fn claimTraceFile(claimed: *std.atomic.Value(bool)) bool { + return !claimed.swap(true, .acq_rel); +} + +fn claimTracePath( + alloc: Allocator, + claimed: *std.atomic.Value(bool), + owned: []const u8, +) ?[]const u8 { + if (claimTraceFile(claimed)) return owned; + log.debug("render trace path already has a process owner; tracing disabled for this surface", .{}); + alloc.free(owned); + return null; +} + +fn elapsedTraceMs(start_tick_ms: u64, now_tick_ms: u64) u64 { + if (now_tick_ms <= start_tick_ms) return 0; + return now_tick_ms - start_tick_ms; +} + +fn updateMaxAtomicWithTimestamp( + value: *std.atomic.Value(u64), + at_ms: *std.atomic.Value(u64), + candidate: u64, + candidate_at_ms: u64, +) void { + var current = value.load(.acquire); + while (candidate > current) { + const observed = value.cmpxchgWeak(current, candidate, .acq_rel, .acquire); + if (observed) |next| { + current = next; + continue; + } + at_ms.store(candidate_at_ms, .release); + return; + } +} + +test "win32 render trace classifies gaps by start time" { + try std.testing.expect(!RenderTrace.gapIsSustained(1250, 578)); + try std.testing.expect(!RenderTrace.gapIsSustained(1299, 300)); + try std.testing.expect(RenderTrace.gapIsSustained(1300, 300)); + try std.testing.expect(!RenderTrace.gapIsSustained(500, 600)); +} + +test "win32 render trace classifies visible paint gaps" { + try std.testing.expect(RenderTrace.startup_paint_gap_ceiling_ms >= RenderTrace.startup_window_ms); + try std.testing.expect(!RenderTrace.gapExceedsPaintLimit(300)); + try std.testing.expect(RenderTrace.gapExceedsPaintLimit(301)); +} + +test "win32 render trace init rejects and frees a second process owner" { + var claimed = std.atomic.Value(bool).init(false); + const first = try std.testing.allocator.dupe(u8, "first.json"); + const first_trace = RenderTrace.initWithClaimedPath(std.testing.allocator, &claimed, first); + defer if (first_trace.path) |path| std.testing.allocator.free(path); + try std.testing.expect(first_trace.path != null); + + const second = try std.testing.allocator.dupe(u8, "second.json"); + const second_trace = RenderTrace.initWithClaimedPath(std.testing.allocator, &claimed, second); + defer if (second_trace.path) |path| std.testing.allocator.free(path); + try std.testing.expect(second_trace.path == null); +} + +test "win32 render trace resolves relative output paths against cwd" { + const relative = try std.testing.allocator.dupe(u8, "render-trace.json"); + const absolute = try absolutizeTracePath(std.testing.allocator, relative); + defer std.testing.allocator.free(absolute); + + try std.testing.expect(std.fs.path.isAbsolute(absolute)); + try std.testing.expectEqualStrings("render-trace.json", std.fs.path.basename(absolute)); +} diff --git a/src/apprt/win32/sys.zig b/src/apprt/win32/sys.zig new file mode 100644 index 00000000000..e7e8289b358 --- /dev/null +++ b/src/apprt/win32/sys.zig @@ -0,0 +1,672 @@ +//! Central Win32 ABI declarations used by the Windows application runtime. + +const std = @import("std"); +const windows = std.os.windows; +const win32_types = @import("../win32_types.zig"); +const c = @import("consts.zig"); + +const ATOM = win32_types.ATOM; +const LPCWSTR = win32_types.LPCWSTR; +const HBRUSH = win32_types.HBRUSH; +const HCURSOR = win32_types.HCURSOR; +const HDC = win32_types.HDC; +const HGLRC = win32_types.HGLRC; +const HGDIOBJ = win32_types.HGDIOBJ; +const HPEN = win32_types.HPEN; +const HMODULE = win32_types.HMODULE; +const HMENU = win32_types.HMENU; +const HICON = win32_types.HICON; +pub const LPARAM = win32_types.LPARAM; +pub const WPARAM = win32_types.WPARAM; +const LRESULT = win32_types.LRESULT; +const LONG_PTR = win32_types.LONG_PTR; +pub const UINT = win32_types.UINT; +const UINT_PTR = win32_types.UINT_PTR; +const DWORD = win32_types.DWORD; +const WORD = win32_types.WORD; +const BYTE = win32_types.BYTE; +const BOOL = win32_types.BOOL; +pub const HWND = win32_types.HWND; +const HINSTANCE = win32_types.HINSTANCE; +const INTRESOURCE = win32_types.INTRESOURCE; +const COLORREF = win32_types.COLORREF; +const HANDLE = windows.HANDLE; +const SIZE_T = windows.SIZE_T; +const SECURITY_ATTRIBUTES = windows.SECURITY_ATTRIBUTES; +const GUID = windows.GUID; +const HRESULT = windows.HRESULT; +pub const HMONITOR = *opaque {}; +pub const HKEY = windows.HKEY; +const HRGN = *anyopaque; +const REGSAM = DWORD; +const SHORT = i16; + +pub const POINT = win32_types.POINT; +pub const RECT = win32_types.RECT; +pub const PIXELFORMATDESCRIPTOR = extern struct { + nSize: WORD, + nVersion: WORD, + dwFlags: u32, + iPixelType: BYTE, + cColorBits: BYTE, + cRedBits: BYTE, + cRedShift: BYTE, + cGreenBits: BYTE, + cGreenShift: BYTE, + cBlueBits: BYTE, + cBlueShift: BYTE, + cAlphaBits: BYTE, + cAlphaShift: BYTE, + cAccumBits: BYTE, + cAccumRedBits: BYTE, + cAccumGreenBits: BYTE, + cAccumBlueBits: BYTE, + cAccumAlphaBits: BYTE, + cDepthBits: BYTE, + cStencilBits: BYTE, + cAuxBuffers: BYTE, + iLayerType: BYTE, + bReserved: BYTE, + dwLayerMask: u32, + dwVisibleMask: u32, + dwDamageMask: u32, +}; + +pub const MSG = extern struct { + hwnd: HWND, + message: UINT, + wParam: WPARAM, + lParam: LPARAM, + time: u32, + pt: POINT, + lPrivate: u32, +}; + +pub const PAINTSTRUCT = win32_types.PAINTSTRUCT; + +pub const DRAWITEMSTRUCT = extern struct { + CtlType: UINT, + CtlID: UINT, + itemID: UINT, + itemAction: UINT, + itemState: UINT, + hwndItem: HWND, + hDC: HDC, + rcItem: RECT, + itemData: usize, +}; + +pub const TRACKMOUSEEVENT = extern struct { + cbSize: DWORD, + dwFlags: DWORD, + hwndTrack: HWND, + dwHoverTime: DWORD, +}; + +pub const WINDOWPOS = extern struct { + hwnd: HWND, + hwndInsertAfter: ?HWND, + x: i32, + y: i32, + cx: i32, + cy: i32, + flags: UINT, +}; + +pub const WINDOWPLACEMENT = extern struct { + length: UINT, + flags: UINT, + showCmd: UINT, + ptMinPosition: POINT, + ptMaxPosition: POINT, + rcNormalPosition: RECT, +}; + +pub const NCCALCSIZE_PARAMS = extern struct { + rgrc: [3]RECT, + lppos: *WINDOWPOS, +}; + +pub const WNDCLASSEXW = win32_types.WNDCLASSEXW; +pub const CREATESTRUCTW = win32_types.CREATESTRUCTW; + +pub const MONITORINFO = extern struct { + cbSize: u32, + rcMonitor: RECT, + rcWork: RECT, + dwFlags: u32, +}; + +pub const MINMAXINFO = extern struct { + ptReserved: POINT, + ptMaxSize: POINT, + ptMaxPosition: POINT, + ptMinTrackSize: POINT, + ptMaxTrackSize: POINT, +}; + +pub const LOGFONTW = extern struct { + lfHeight: i32 = 0, + lfWidth: i32 = 0, + lfEscapement: i32 = 0, + lfOrientation: i32 = 0, + lfWeight: i32 = c.FW_NORMAL, + lfItalic: u8 = 0, + lfUnderline: u8 = 0, + lfStrikeOut: u8 = 0, + lfCharSet: u8 = c.DEFAULT_CHARSET, + lfOutPrecision: u8 = c.OUT_DEFAULT_PRECIS, + lfClipPrecision: u8 = c.CLIP_DEFAULT_PRECIS, + lfQuality: u8 = c.CLEARTYPE_QUALITY, + lfPitchAndFamily: u8 = c.DEFAULT_PITCH | c.FF_DONTCARE, + lfFaceName: [c.LF_FACESIZE]u16 = [_]u16{0} ** c.LF_FACESIZE, +}; + +pub const NONCLIENTMETRICSW = extern struct { + cbSize: UINT, + iBorderWidth: i32, + iScrollWidth: i32, + iScrollHeight: i32, + iCaptionWidth: i32, + iCaptionHeight: i32, + lfCaptionFont: LOGFONTW, + iSmCaptionWidth: i32, + iSmCaptionHeight: i32, + lfSmCaptionFont: LOGFONTW, + iMenuWidth: i32, + iMenuHeight: i32, + lfMenuFont: LOGFONTW, + lfStatusFont: LOGFONTW, + lfMessageFont: LOGFONTW, + iPaddedBorderWidth: i32, +}; + +/// RTL_OSVERSIONINFOW for `RtlGetVersion`. `GetVersionExW` shims are +/// manifest-gated and lie about build numbers on anything newer than +/// Win8.1 unless the exe carries a Win10 application-manifest GUID. +/// `RtlGetVersion` is unshimmed and returns the real kernel build. +pub const RTL_OSVERSIONINFOW = extern struct { + dwOSVersionInfoSize: u32, + dwMajorVersion: u32, + dwMinorVersion: u32, + dwBuildNumber: u32, + dwPlatformId: u32, + szCSDVersion: [128]u16, +}; + +pub const COMPOSITIONFORM = extern struct { + dwStyle: u32, + ptCurrentPos: POINT, + rcArea: RECT, +}; + +pub const CANDIDATEFORM = extern struct { + dwIndex: u32, + dwStyle: u32, + ptCurrentPos: POINT, + rcArea: RECT, +}; + +pub const JOBOBJECTINFOCLASS = enum(i32) { + basic_limit_information = 2, + basic_process_id_list = 3, + extended_limit_information = 9, + _, +}; + +pub extern "user32" fn RegisterClipboardFormatW(lpszFormat: [*:0]const u16) callconv(.winapi) UINT; + +pub extern "user32" fn RegisterClassExW(lpWndClass: *const WNDCLASSEXW) callconv(.winapi) ATOM; + +pub extern "user32" fn CreateWindowExW( + dwExStyle: u32, + lpClassName: LPCWSTR, + lpWindowName: LPCWSTR, + dwStyle: u32, + X: i32, + Y: i32, + nWidth: i32, + nHeight: i32, + hWndParent: ?HWND, + hMenu: HMENU, + hInstance: ?HINSTANCE, + lpParam: ?*anyopaque, +) callconv(.winapi) ?HWND; + +pub extern "user32" fn CallWindowProcW(lpPrevWndFunc: ?*const anyopaque, hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; + +pub extern "user32" fn DefWindowProcW(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; + +pub extern "user32" fn DestroyWindow(hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn DrawTextW(hDC: HDC, lpchText: [*:0]const u16, cchText: i32, lprc: *RECT, format: UINT) callconv(.winapi) i32; + +pub extern "user32" fn DispatchMessageW(lpMsg: *const MSG) callconv(.winapi) LRESULT; + +pub extern "user32" fn GetFocus() callconv(.winapi) ?HWND; + +pub extern "user32" fn GetMessageW(lpMsg: *MSG, hWnd: ?HWND, wMsgFilterMin: UINT, wMsgFilterMax: UINT) callconv(.winapi) i32; + +pub extern "user32" fn MessageBoxW(hwnd: ?HWND, text: LPCWSTR, caption: LPCWSTR, flags: UINT) callconv(.winapi) c_int; + +pub extern "user32" fn GetClientRect(hWnd: HWND, lpRect: *RECT) callconv(.winapi) BOOL; + +pub extern "user32" fn GetKeyState(nVirtKey: i32) callconv(.winapi) SHORT; + +pub extern "user32" fn GetKeyboardState(lpKeyState: *[256]u8) callconv(.winapi) BOOL; + +pub extern "user32" fn GetMonitorInfoW(hMonitor: HMONITOR, lpmi: *MONITORINFO) callconv(.winapi) BOOL; + +pub extern "user32" fn GetWindowRect(hWnd: HWND, lpRect: *RECT) callconv(.winapi) BOOL; + +pub extern "user32" fn GetWindowPlacement(hWnd: HWND, lpwndpl: *WINDOWPLACEMENT) callconv(.winapi) BOOL; + +pub extern "user32" fn GetWindowTextLengthW(hWnd: HWND) callconv(.winapi) i32; + +pub extern "user32" fn GetWindowTextW(hWnd: HWND, lpString: [*]u16, nMaxCount: i32) callconv(.winapi) i32; + +pub extern "user32" fn IsWindow(hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn IsWindowVisible(hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn IsIconic(hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn IsZoomed(hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn MonitorFromWindow(hwnd: HWND, dwFlags: DWORD) callconv(.winapi) ?HMONITOR; + +pub extern "user32" fn EnumDisplayMonitors(hdc: HDC, lprcClip: ?*const RECT, lpfnEnum: *const fn (HMONITOR, HDC, *RECT, LPARAM) callconv(.winapi) BOOL, dwData: LPARAM) callconv(.winapi) BOOL; + +pub extern "user32" fn ReleaseCapture() callconv(.winapi) BOOL; + +pub extern "user32" fn ScreenToClient(hWnd: HWND, lpPoint: *POINT) callconv(.winapi) BOOL; + +pub extern "user32" fn TrackMouseEvent(lpEventTrack: *TRACKMOUSEEVENT) callconv(.winapi) BOOL; + +pub extern "user32" fn BeginPaint(hWnd: HWND, lpPaint: *PAINTSTRUCT) callconv(.winapi) HDC; + +pub extern "user32" fn EndPaint(hWnd: HWND, lpPaint: *const PAINTSTRUCT) callconv(.winapi) BOOL; + +pub extern "user32" fn GetDC(hWnd: HWND) callconv(.winapi) HDC; + +pub extern "gdi32" fn RectVisible(hdc: HDC, lprc: *const RECT) callconv(.winapi) BOOL; + +pub extern "user32" fn OpenClipboard(hWndNewOwner: ?HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn CloseClipboard() callconv(.winapi) BOOL; + +pub extern "user32" fn EmptyClipboard() callconv(.winapi) BOOL; + +pub extern "user32" fn GetClipboardData(uFormat: UINT) callconv(.winapi) ?*anyopaque; + +pub extern "user32" fn SetClipboardData(uFormat: UINT, hMem: ?*anyopaque) callconv(.winapi) ?*anyopaque; + +pub extern "user32" fn IsClipboardFormatAvailable(format: UINT) callconv(.winapi) BOOL; + +pub extern "user32" fn LoadCursorW(hInstance: ?HINSTANCE, lpCursorName: INTRESOURCE) callconv(.winapi) HCURSOR; + +pub extern "user32" fn LoadImageW(hInst: HINSTANCE, name: INTRESOURCE, @"type": UINT, cx: i32, cy: i32, fuLoad: UINT) callconv(.winapi) ?*anyopaque; + +pub extern "user32" fn GetSystemMetrics(nIndex: i32) callconv(.winapi) i32; + +pub extern "user32" fn MessageBeep(uType: UINT) callconv(.winapi) BOOL; + +pub extern "user32" fn InvalidateRect(hWnd: HWND, lpRect: ?*const RECT, bErase: BOOL) callconv(.winapi) BOOL; + +pub extern "user32" fn MoveWindow(hWnd: HWND, X: i32, Y: i32, nWidth: i32, nHeight: i32, bRepaint: BOOL) callconv(.winapi) BOOL; + +pub extern "user32" fn PeekMessageW(lpMsg: *MSG, hWnd: ?HWND, wMsgFilterMin: UINT, wMsgFilterMax: UINT, wRemoveMsg: UINT) callconv(.winapi) BOOL; + +pub extern "user32" fn PostMessageW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) BOOL; + +pub extern "user32" fn PostThreadMessageW(idThread: DWORD, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) BOOL; + +pub extern "user32" fn PostQuitMessage(nExitCode: i32) callconv(.winapi) void; + +pub extern "user32" fn ReleaseDC(hWnd: HWND, hDC: HDC) callconv(.winapi) i32; + +pub extern "user32" fn RegisterHotKey(hWnd: ?HWND, id: i32, fsModifiers: UINT, vk: UINT) callconv(.winapi) BOOL; + +pub extern "user32" fn RedrawWindow(hWnd: HWND, lprcUpdate: ?*const RECT, hrgnUpdate: ?*anyopaque, flags: UINT) callconv(.winapi) BOOL; + +pub extern "user32" fn SetTimer(hWnd: ?HWND, nIDEvent: UINT_PTR, uElapse: UINT, lpTimerFunc: ?*const anyopaque) callconv(.winapi) UINT_PTR; + +pub extern "user32" fn SetCursor(hCursor: HCURSOR) callconv(.winapi) HCURSOR; + +pub extern "user32" fn SetCapture(hWnd: HWND) callconv(.winapi) ?HWND; + +pub extern "user32" fn SetForegroundWindow(hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn GetCursorPos(lpPoint: *POINT) callconv(.winapi) BOOL; + +pub extern "user32" fn GetCaretPos(lpPoint: *POINT) callconv(.winapi) BOOL; + +pub extern "user32" fn MonitorFromPoint(pt: POINT, dwFlags: DWORD) callconv(.winapi) ?HMONITOR; + +pub extern "user32" fn SetLayeredWindowAttributes(hwnd: HWND, crKey: u32, bAlpha: BYTE, dwFlags: u32) callconv(.winapi) BOOL; + +pub extern "user32" fn SetWindowLongPtrW(hWnd: HWND, nIndex: i32, dwNewLong: LONG_PTR) callconv(.winapi) LONG_PTR; + +pub extern "user32" fn SetWindowPos(hWnd: HWND, hWndInsertAfter: ?*anyopaque, X: i32, Y: i32, cx: i32, cy: i32, uFlags: UINT) callconv(.winapi) BOOL; + +pub extern "user32" fn SetFocus(hWnd: HWND) callconv(.winapi) ?HWND; + +pub extern "user32" fn SendMessageW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; + +pub extern "user32" fn SetWindowTextW(hWnd: HWND, lpString: LPCWSTR) callconv(.winapi) BOOL; + +pub extern "user32" fn GetWindowLongPtrW(hWnd: HWND, nIndex: i32) callconv(.winapi) LONG_PTR; + +pub extern "user32" fn ShowWindow(hWnd: HWND, nCmdShow: i32) callconv(.winapi) BOOL; + +pub extern "user32" fn SystemParametersInfoW(uiAction: UINT, uiParam: UINT, pvParam: ?*anyopaque, fWinIni: UINT) callconv(.winapi) BOOL; + +pub extern "user32" fn GetSysColor(nIndex: i32) callconv(.winapi) COLORREF; + +pub extern "user32" fn GetDpiForWindow(hwnd: HWND) callconv(.winapi) UINT; + +pub extern "user32" fn CreatePopupMenu() callconv(.winapi) HMENU; + +pub extern "user32" fn AppendMenuW(hMenu: HMENU, uFlags: UINT, uIDNewItem: usize, lpNewItem: ?LPCWSTR) callconv(.winapi) BOOL; + +pub extern "user32" fn TrackPopupMenu(hMenu: HMENU, uFlags: UINT, x: i32, y: i32, nReserved: i32, hWnd: HWND, prcRect: ?*const RECT) callconv(.winapi) BOOL; + +pub extern "user32" fn DestroyMenu(hMenu: HMENU) callconv(.winapi) BOOL; + +pub extern "user32" fn ClientToScreen(hWnd: HWND, lpPoint: *POINT) callconv(.winapi) BOOL; + +pub extern "user32" fn ToUnicode( + wVirtKey: UINT, + wScanCode: UINT, + lpKeyState: *const [256]u8, + pwszBuff: [*]u16, + cchBuff: i32, + wFlags: UINT, +) callconv(.winapi) i32; + +pub extern "user32" fn TranslateMessage(lpMsg: *const MSG) callconv(.winapi) BOOL; + +pub extern "user32" fn IsDialogMessageW(hDlg: HWND, lpMsg: *MSG) callconv(.winapi) BOOL; + +pub extern "user32" fn UnregisterHotKey(hWnd: ?HWND, id: i32) callconv(.winapi) BOOL; + +pub extern "user32" fn UpdateWindow(hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn KillTimer(hWnd: ?HWND, uIDEvent: UINT_PTR) callconv(.winapi) BOOL; + +pub extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.winapi) HINSTANCE; + +pub extern "ole32" fn CoInitializeEx(pvReserved: ?*anyopaque, dwCoInit: u32) callconv(.winapi) i32; + +pub extern "ole32" fn CoUninitialize() callconv(.winapi) void; + +pub extern "ntdll" fn RtlGetVersion(lpVersionInformation: *RTL_OSVERSIONINFOW) callconv(.winapi) i32; + +/// Atomic replace of an existing file. Used by the settings save path +/// so a crash between write + rename can't corrupt `ghostty.conf`. +/// Returns non-zero on success. `dwReplaceFlags = 0` gives the default +/// (non-write-through) behaviour, which is fine for a user config file. +pub extern "kernel32" fn ReplaceFileW( + lpReplacedFileName: LPCWSTR, + lpReplacementFileName: LPCWSTR, + lpBackupFileName: ?LPCWSTR, + dwReplaceFlags: DWORD, + lpExclude: ?*anyopaque, + lpReserved: ?*anyopaque, +) callconv(.winapi) BOOL; + +pub extern "kernel32" fn MoveFileExW( + lpExistingFileName: LPCWSTR, + lpNewFileName: ?LPCWSTR, + dwFlags: u32, +) callconv(.winapi) BOOL; + +pub extern "kernel32" fn GetCurrentThreadId() callconv(.winapi) DWORD; + +pub extern "kernel32" fn GetCurrentProcessId() callconv(.winapi) DWORD; + +pub extern "kernel32" fn GetTickCount64() callconv(.winapi) u64; + +pub extern "kernel32" fn SetThreadErrorMode(dwNewMode: DWORD, lpOldMode: ?*DWORD) callconv(.winapi) BOOL; + +pub extern "kernel32" fn CreateNamedPipeW( + lpName: LPCWSTR, + dwOpenMode: DWORD, + dwPipeMode: DWORD, + nMaxInstances: DWORD, + nOutBufferSize: DWORD, + nInBufferSize: DWORD, + nDefaultTimeOut: DWORD, + lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, +) callconv(.winapi) windows.HANDLE; + +pub extern "kernel32" fn ConnectNamedPipe( + hNamedPipe: windows.HANDLE, + lpOverlapped: ?*anyopaque, +) callconv(.winapi) BOOL; + +pub extern "kernel32" fn GetProcAddress(hModule: HMODULE, lpProcName: [*:0]const u8) callconv(.winapi) ?*const anyopaque; + +pub extern "kernel32" fn LoadLibraryA(lpLibFileName: [*:0]const u8) callconv(.winapi) HMODULE; + +pub extern "kernel32" fn SetCurrentDirectoryW(lpPathName: LPCWSTR) callconv(.winapi) BOOL; + +pub extern "kernel32" fn WaitNamedPipeW(lpNamedPipeName: LPCWSTR, nTimeOut: DWORD) callconv(.winapi) BOOL; + +pub extern "kernel32" fn GlobalAlloc(uFlags: UINT, dwBytes: usize) callconv(.winapi) ?*anyopaque; + +pub extern "kernel32" fn GlobalFree(hMem: ?*anyopaque) callconv(.winapi) ?*anyopaque; + +pub extern "kernel32" fn GlobalLock(hMem: ?*anyopaque) callconv(.winapi) ?*anyopaque; + +pub extern "kernel32" fn GlobalUnlock(hMem: ?*anyopaque) callconv(.winapi) BOOL; + +pub extern "gdi32" fn ChoosePixelFormat(hdc: HDC, ppfd: *const PIXELFORMATDESCRIPTOR) callconv(.winapi) i32; + +pub extern "gdi32" fn CreateSolidBrush(color: COLORREF) callconv(.winapi) HBRUSH; + +pub extern "gdi32" fn DeleteObject(ho: HGDIOBJ) callconv(.winapi) BOOL; + +pub extern "user32" fn FillRect(hdc: HDC, lprc: *const RECT, hbr: HBRUSH) callconv(.winapi) i32; + +pub extern "gdi32" fn SetBkColor(hdc: HDC, color: COLORREF) callconv(.winapi) COLORREF; + +pub extern "gdi32" fn SetBkMode(hdc: HDC, mode: i32) callconv(.winapi) i32; + +pub extern "gdi32" fn SetPixelFormat(hdc: HDC, format: i32, ppfd: *const PIXELFORMATDESCRIPTOR) callconv(.winapi) BOOL; + +pub extern "gdi32" fn SetTextColor(hdc: HDC, color: COLORREF) callconv(.winapi) COLORREF; + +pub extern "gdi32" fn SwapBuffers(hdc: HDC) callconv(.winapi) BOOL; + +pub extern "gdi32" fn TextOutW(hdc: HDC, x: i32, y: i32, lpString: LPCWSTR, c: i32) callconv(.winapi) BOOL; + +pub extern "gdi32" fn CreateFontIndirectW(lplf: *const LOGFONTW) callconv(.winapi) ?*anyopaque; + +pub extern "gdi32" fn SelectObject(hdc: HDC, h: HGDIOBJ) callconv(.winapi) HGDIOBJ; + +pub extern "gdi32" fn GetStockObject(i: i32) callconv(.winapi) HGDIOBJ; + +pub extern "gdi32" fn SetDCBrushColor(hdc: HDC, color: COLORREF) callconv(.winapi) COLORREF; + +pub extern "gdi32" fn SetDCPenColor(hdc: HDC, color: u32) callconv(.winapi) u32; + +pub extern "gdi32" fn RoundRect(hdc: HDC, left: i32, top: i32, right: i32, bottom: i32, width: i32, height: i32) callconv(.winapi) BOOL; + +pub extern "advapi32" fn RegOpenKeyExW(hKey: HKEY, lpSubKey: LPCWSTR, ulOptions: DWORD, samDesired: REGSAM, phkResult: *HKEY) callconv(.winapi) i32; + +pub extern "advapi32" fn RegQueryValueExW(hKey: HKEY, lpValueName: LPCWSTR, lpReserved: ?*DWORD, lpType: ?*DWORD, lpData: ?*u8, lpcbData: ?*DWORD) callconv(.winapi) i32; + +pub extern "advapi32" fn RegCloseKey(hKey: HKEY) callconv(.winapi) i32; + +pub extern "opengl32" fn wglCreateContext(hdc: HDC) callconv(.winapi) HGLRC; + +pub extern "opengl32" fn wglDeleteContext(hglrc: HGLRC) callconv(.winapi) BOOL; + +pub extern "opengl32" fn wglGetCurrentContext() callconv(.winapi) HGLRC; + +pub extern "opengl32" fn wglGetCurrentDC() callconv(.winapi) HDC; + +pub extern "opengl32" fn wglGetProcAddress(lpszProc: [*:0]const u8) callconv(.winapi) ?*const anyopaque; + +pub extern "opengl32" fn wglMakeCurrent(hdc: HDC, hglrc: HGLRC) callconv(.winapi) BOOL; + +pub extern "dwmapi" fn DwmDefWindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM, plResult: *LRESULT) callconv(.winapi) BOOL; + +pub extern "imm32" fn ImmGetContext(hWnd: HWND) callconv(.winapi) ?*anyopaque; + +pub extern "imm32" fn ImmReleaseContext(hWnd: HWND, hIMC: ?*anyopaque) callconv(.winapi) BOOL; + +pub extern "imm32" fn ImmGetCompositionStringW(hIMC: *anyopaque, dwIndex: u32, lpBuf: ?[*]u16, dwBufLen: u32) callconv(.winapi) i32; + +pub extern "imm32" fn ImmSetCompositionWindow(hIMC: *anyopaque, lpCompForm: *const COMPOSITIONFORM) callconv(.winapi) BOOL; + +pub extern "imm32" fn ImmSetCandidateWindow(hIMC: *anyopaque, lpCandidate: *const CANDIDATEFORM) callconv(.winapi) BOOL; + +pub extern "shell32" fn ShellExecuteW( + hwnd: ?HWND, + lpOperation: ?LPCWSTR, + lpFile: LPCWSTR, + lpParameters: ?LPCWSTR, + lpDirectory: ?LPCWSTR, + nShowCmd: i32, +) callconv(.winapi) ?*anyopaque; + +pub extern "shell32" fn DragAcceptFiles(hWnd: HWND, fAccept: BOOL) callconv(.winapi) void; + +pub extern "shell32" fn DragQueryFileW(hDrop: *anyopaque, iFile: UINT, lpszFile: ?[*]u16, cch: UINT) callconv(.winapi) UINT; + +pub extern "shell32" fn DragFinish(hDrop: *anyopaque) callconv(.winapi) void; + +pub extern "user32" fn EnableWindow(hWnd: HWND, bEnable: BOOL) callconv(.winapi) BOOL; + +pub extern "user32" fn GetParent(hWnd: HWND) callconv(.winapi) ?HWND; + +pub extern "user32" fn IsChild(hWndParent: HWND, hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn IsWindowEnabled(hWnd: HWND) callconv(.winapi) BOOL; + +pub extern "user32" fn SetScrollRange(hWnd: HWND, nBar: c_int, nMinPos: c_int, nMaxPos: c_int, bRedraw: BOOL) callconv(.winapi) BOOL; + +pub extern "user32" fn SetScrollPos(hWnd: HWND, nBar: c_int, nPos: c_int, bRedraw: BOOL) callconv(.winapi) c_int; + +pub extern "user32" fn ShowScrollBar(hWnd: HWND, wBar: c_int, bShow: BOOL) callconv(.winapi) BOOL; + +pub extern "user32" fn SetWindowRgn(hWnd: HWND, hRgn: ?HRGN, bRedraw: BOOL) callconv(.winapi) c_int; + +pub extern "user32" fn NotifyWinEvent(event: u32, hwnd: HWND, idObject: i32, idChild: i32) callconv(.winapi) void; + +pub extern "user32" fn EnumChildWindows(hWndParent: HWND, lpEnumFunc: *const fn (HWND, LPARAM) callconv(.winapi) BOOL, lParam: LPARAM) callconv(.winapi) BOOL; + +pub extern "gdi32" fn CreateFontW( + cHeight: i32, + cWidth: i32, + cEscapement: i32, + cOrientation: i32, + cWeight: i32, + bItalic: u32, + bUnderline: u32, + bStrikeOut: u32, + iCharSet: u32, + iOutPrecision: u32, + iClipPrecision: u32, + iQuality: u32, + iPitchAndFamily: u32, + pszFaceName: LPCWSTR, +) callconv(.winapi) HGDIOBJ; + +pub extern "gdi32" fn CreateRectRgn(x1: i32, y1: i32, x2: i32, y2: i32) callconv(.winapi) ?HRGN; + +pub extern "kernel32" fn MulDiv(nNumber: i32, nNumerator: i32, nDenominator: i32) callconv(.winapi) i32; + +pub extern "ole32" fn CoCreateInstance( + class_id: *const GUID, + outer: ?*anyopaque, + context: u32, + interface_id: *const GUID, + result: *?*anyopaque, +) callconv(.winapi) HRESULT; + +pub extern "gdi32" fn CreatePen(style: i32, width: i32, color: u32) callconv(.winapi) HPEN; + +pub extern "gdi32" fn MoveToEx(hdc: HDC, x: i32, y: i32, lp: ?*POINT) callconv(.winapi) i32; + +pub extern "gdi32" fn LineTo(hdc: HDC, x: i32, y: i32) callconv(.winapi) i32; + +pub extern "gdi32" fn Ellipse(hdc: HDC, x1: i32, y1: i32, x2: i32, y2: i32) callconv(.winapi) i32; + +pub extern "gdi32" fn Rectangle(hdc: HDC, x1: i32, y1: i32, x2: i32, y2: i32) callconv(.winapi) i32; + +pub extern "gdi32" fn Polygon(hdc: HDC, points: [*]const POINT, count: i32) callconv(.winapi) i32; + +pub extern "kernel32" fn LoadLibraryW(name: [*:0]const u16) callconv(.winapi) HMODULE; + +pub extern "kernel32" fn FreeLibrary(module: HMODULE) callconv(.winapi) BOOL; + +pub extern "kernel32" fn CreateJobObjectW( + lpJobAttributes: ?*SECURITY_ATTRIBUTES, + lpName: ?LPCWSTR, +) callconv(.winapi) ?HANDLE; + +pub extern "kernel32" fn SetInformationJobObject( + hJob: HANDLE, + JobObjectInfoClass: JOBOBJECTINFOCLASS, + lpJobObjectInfo: *const anyopaque, + cbJobObjectInfoLength: DWORD, +) callconv(.winapi) BOOL; + +pub extern "kernel32" fn QueryInformationJobObject( + hJob: HANDLE, + JobObjectInfoClass: JOBOBJECTINFOCLASS, + lpJobObjectInfo: *anyopaque, + cbJobObjectInfoLength: DWORD, + lpReturnLength: ?*DWORD, +) callconv(.winapi) BOOL; + +pub extern "kernel32" fn AssignProcessToJobObject( + hJob: HANDLE, + hProcess: HANDLE, +) callconv(.winapi) BOOL; + +pub extern "kernel32" fn GetProcessId(Process: HANDLE) callconv(.winapi) DWORD; + +pub extern "kernel32" fn OpenProcess( + dwDesiredAccess: DWORD, + bInheritHandle: BOOL, + dwProcessId: DWORD, +) callconv(.winapi) ?HANDLE; + +pub extern "user32" fn GetAncestor(hwnd: HWND, flags: u32) callconv(.winapi) ?HWND; + +pub extern "user32" fn SendMessageTimeoutW( + HWND, + UINT, + WPARAM, + LPARAM, + UINT, + UINT, + *usize, +) callconv(.winapi) isize; + +pub extern "shell32" fn SetCurrentProcessExplicitAppUserModelID(AppID: LPCWSTR) callconv(.winapi) HRESULT; + +pub extern "advapi32" fn RegCreateKeyExW( + hKey: HKEY, + lpSubKey: LPCWSTR, + Reserved: DWORD, + lpClass: ?LPCWSTR, + dwOptions: DWORD, + samDesired: REGSAM, + lpSecurityAttributes: ?*anyopaque, + phkResult: *HKEY, + lpdwDisposition: ?*DWORD, +) callconv(.winapi) i32; + +pub extern "advapi32" fn RegSetValueExW( + hKey: HKEY, + lpValueName: LPCWSTR, + Reserved: DWORD, + dwType: DWORD, + lpData: [*]const u8, + cbData: DWORD, +) callconv(.winapi) i32; diff --git a/src/apprt/win32_aumid.zig b/src/apprt/win32_aumid.zig index f10da387196..37f938b8e4d 100644 --- a/src/apprt/win32_aumid.zig +++ b/src/apprt/win32_aumid.zig @@ -33,10 +33,11 @@ const std = @import("std"); const windows = std.os.windows; const win32_types = @import("win32_types.zig"); +const sys = @import("win32/sys.zig"); const HRESULT = windows.HRESULT; const LPCWSTR = win32_types.LPCWSTR; -const HKEY = *opaque {}; +const HKEY = sys.HKEY; const REGSAM = u32; const DWORD = win32_types.DWORD; @@ -47,31 +48,6 @@ const REG_SZ: DWORD = 1; const REG_DWORD: DWORD = 4; const ERROR_SUCCESS: i32 = 0; -extern "shell32" fn SetCurrentProcessExplicitAppUserModelID(AppID: LPCWSTR) callconv(.winapi) HRESULT; - -extern "advapi32" fn RegCreateKeyExW( - hKey: HKEY, - lpSubKey: LPCWSTR, - Reserved: DWORD, - lpClass: ?LPCWSTR, - dwOptions: DWORD, - samDesired: REGSAM, - lpSecurityAttributes: ?*anyopaque, - phkResult: *HKEY, - lpdwDisposition: ?*DWORD, -) callconv(.winapi) i32; - -extern "advapi32" fn RegSetValueExW( - hKey: HKEY, - lpValueName: LPCWSTR, - Reserved: DWORD, - dwType: DWORD, - lpData: [*]const u8, - cbData: DWORD, -) callconv(.winapi) i32; - -extern "advapi32" fn RegCloseKey(hKey: HKEY) callconv(.winapi) i32; - const aumid_wide: [*:0]const u16 = std.unicode.utf8ToUtf16LeStringLiteral("io.github.amanthanvi.noctty"); pub const aumid_utf8 = "io.github.amanthanvi.noctty"; @@ -81,7 +57,7 @@ pub const aumid_utf8 = "io.github.amanthanvi.noctty"; /// advisory — we log and proceed on failure rather than abort, since /// toasts are not essential for the app to function. pub fn setProcessAumid() void { - const hr = SetCurrentProcessExplicitAppUserModelID(aumid_wide); + const hr = sys.SetCurrentProcessExplicitAppUserModelID(aumid_wide); if (hr < 0) { std.log.warn("AUMID: SetCurrentProcessExplicitAppUserModelID failed hr=0x{x:0>8}", .{@as(u32, @bitCast(hr))}); } @@ -97,7 +73,7 @@ pub fn registerAumidDisplayName(alloc: std.mem.Allocator) void { ); var hkey: HKEY = undefined; - const open_rc = RegCreateKeyExW( + const open_rc = sys.RegCreateKeyExW( HKEY_CURRENT_USER, subkey, 0, @@ -112,7 +88,7 @@ pub fn registerAumidDisplayName(alloc: std.mem.Allocator) void { std.log.warn("AUMID: RegCreateKeyExW failed rc={d}", .{open_rc}); return; } - defer _ = RegCloseKey(hkey); + defer _ = sys.RegCloseKey(hkey); const display_name = std.unicode.utf8ToUtf16LeStringLiteral("noctty"); const set_rc = writeRegSz(hkey, std.unicode.utf8ToUtf16LeStringLiteral("DisplayName"), display_name); @@ -138,7 +114,7 @@ pub fn registerAumidDisplayName(alloc: std.mem.Allocator) void { } var show_in_settings: u32 = 1; - const show_rc = RegSetValueExW( + const show_rc = sys.RegSetValueExW( hkey, std.unicode.utf8ToUtf16LeStringLiteral("ShowInSettings"), 0, @@ -152,7 +128,7 @@ pub fn registerAumidDisplayName(alloc: std.mem.Allocator) void { } fn writeRegSz(hkey: HKEY, value_name: LPCWSTR, value: [:0]const u16) i32 { - return RegSetValueExW( + return sys.RegSetValueExW( hkey, value_name, 0, diff --git a/src/apprt/win32_compositor.zig b/src/apprt/win32_compositor.zig index 2134f7734c0..ba336af2ff8 100644 --- a/src/apprt/win32_compositor.zig +++ b/src/apprt/win32_compositor.zig @@ -8,6 +8,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const sys = @import("win32/sys.zig"); pub const TerminalPresentation = enum { wgl_swap_buffers_unchanged, @@ -48,15 +49,11 @@ pub fn probeNativeCapability() Capability { const HMODULE = ?*anyopaque; const BOOL = i32; -extern "kernel32" fn LoadLibraryA(name: [*:0]const u8) callconv(.winapi) HMODULE; -extern "kernel32" fn GetProcAddress(module: HMODULE, name: [*:0]const u8) callconv(.winapi) ?*const anyopaque; -extern "kernel32" fn FreeLibrary(module: HMODULE) callconv(.winapi) BOOL; - fn hasExport(comptime library: [:0]const u8, comptime symbol: [:0]const u8) bool { if (comptime builtin.os.tag != .windows) return false; - const module = LoadLibraryA(library.ptr) orelse return false; - defer _ = FreeLibrary(module); - return GetProcAddress(module, symbol.ptr) != null; + const module = sys.LoadLibraryA(library.ptr) orelse return false; + defer _ = sys.FreeLibrary(module); + return sys.GetProcAddress(module, symbol.ptr) != null; } pub const WindowHandle = usize; diff --git a/src/apprt/win32_compositor_content_native.zig b/src/apprt/win32_compositor_content_native.zig index bbf6a4db3d7..9118b90179f 100644 --- a/src/apprt/win32_compositor_content_native.zig +++ b/src/apprt/win32_compositor_content_native.zig @@ -8,6 +8,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const sys = @import("win32/sys.zig"); const windows = std.os.windows; const GUID = windows.GUID; @@ -142,10 +143,6 @@ const D3D11CreateDeviceFn = *const fn ( immediate_context: ?*?*anyopaque, ) callconv(.winapi) HRESULT; -extern "kernel32" fn LoadLibraryW(name: [*:0]const u16) callconv(.winapi) HMODULE; -extern "kernel32" fn GetProcAddress(module: HMODULE, name: [*:0]const u8) callconv(.winapi) ?*const anyopaque; -extern "kernel32" fn FreeLibrary(module: HMODULE) callconv(.winapi) i32; - pub const ResourceError = error{ Unavailable, InitializationFailed, @@ -176,9 +173,9 @@ const Api = struct { var self: Api = .{}; errdefer self.unload(); - self.d2d1_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d2d1.dll")) orelse + self.d2d1_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d2d1.dll")) orelse return error.Unavailable; - self.dwrite_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("dwrite.dll")) orelse + self.dwrite_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("dwrite.dll")) orelse return error.Unavailable; self.d2d1_create_factory = loadProc(D2D1CreateFactoryFn, self.d2d1_module, "D2D1CreateFactory") orelse return error.Unavailable; @@ -194,8 +191,8 @@ const Api = struct { self.d2d1_create_device = null; self.dwrite_create_factory = null; if (comptime builtin.os.tag == .windows) { - if (self.dwrite_module) |module| _ = FreeLibrary(module); - if (self.d2d1_module) |module| _ = FreeLibrary(module); + if (self.dwrite_module) |module| _ = sys.FreeLibrary(module); + if (self.d2d1_module) |module| _ = sys.FreeLibrary(module); } self.* = .{}; } @@ -346,7 +343,7 @@ pub const ResourceGraph = struct { }; fn loadProc(comptime T: type, module: HMODULE, comptime name: [:0]const u8) ?T { - const raw = GetProcAddress(module, name.ptr) orelse return null; + const raw = sys.GetProcAddress(module, name.ptr) orelse return null; return @ptrCast(@alignCast(raw)); } @@ -408,9 +405,9 @@ test "shell content stop and loss notification are idempotent" { test "native shell content resources recover against a live DXGI device" { if (builtin.os.tag != .windows) return error.SkipZigTest; - const d3d11_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d3d11.dll")) orelse + const d3d11_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d3d11.dll")) orelse return error.SkipZigTest; - defer _ = FreeLibrary(d3d11_module); + defer _ = sys.FreeLibrary(d3d11_module); const create_device = loadProc(D3D11CreateDeviceFn, d3d11_module, "D3D11CreateDevice") orelse return error.SkipZigTest; diff --git a/src/apprt/win32_compositor_native.zig b/src/apprt/win32_compositor_native.zig index 88aa1793954..aa617828ba6 100644 --- a/src/apprt/win32_compositor_native.zig +++ b/src/apprt/win32_compositor_native.zig @@ -6,6 +6,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const sys = @import("win32/sys.zig"); const boundary = @import("win32_compositor.zig"); const content_native = @import("win32_compositor_content_native.zig"); const window_content_native = @import("win32_compositor_window_content_native.zig"); @@ -17,14 +18,9 @@ const HRESULT = windows.HRESULT; const BOOL = i32; const UINT = u32; const HMODULE = ?*anyopaque; -const HWND = ?*anyopaque; +const HWND = sys.HWND; -const RECT = extern struct { - left: i32, - top: i32, - right: i32, - bottom: i32, -}; +const RECT = sys.RECT; const S_OK: HRESULT = 0; const D3D_DRIVER_TYPE_HARDWARE: u32 = 1; @@ -104,28 +100,7 @@ const DCompositionCreateDeviceFn = *const fn ( ) callconv(.winapi) HRESULT; const IsWindowFn = *const fn (HWND) callconv(.winapi) BOOL; -const GetAncestorFn = *const fn (HWND, UINT) callconv(.winapi) HWND; - -extern "kernel32" fn LoadLibraryW(name: [*:0]const u16) callconv(.winapi) HMODULE; -extern "kernel32" fn GetProcAddress(module: HMODULE, name: [*:0]const u8) callconv(.winapi) ?*const anyopaque; -extern "kernel32" fn FreeLibrary(module: HMODULE) callconv(.winapi) BOOL; -extern "user32" fn CreateWindowExW( - ex_style: u32, - class_name: [*:0]const u16, - window_name: [*:0]const u16, - style: u32, - x: i32, - y: i32, - width: i32, - height: i32, - parent: HWND, - menu: ?*anyopaque, - instance: HMODULE, - param: ?*anyopaque, -) callconv(.winapi) HWND; -extern "user32" fn DestroyWindow(hwnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn GetClientRect(hwnd: HWND, rect: *RECT) callconv(.winapi) BOOL; -extern "user32" fn GetDpiForWindow(hwnd: HWND) callconv(.winapi) UINT; +const GetAncestorFn = *const fn (HWND, UINT) callconv(.winapi) ?HWND; const Api = struct { d3d11_module: HMODULE = null, @@ -143,15 +118,15 @@ const Api = struct { var self: Api = .{}; errdefer self.unload(); - self.d3d11_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d3d11.dll")) orelse + self.d3d11_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d3d11.dll")) orelse return error.Unavailable; - self.dcomp_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("dcomp.dll")) orelse + self.dcomp_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("dcomp.dll")) orelse return error.Unavailable; - self.d2d1_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d2d1.dll")) orelse + self.d2d1_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d2d1.dll")) orelse return error.Unavailable; - self.dwrite_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("dwrite.dll")) orelse + self.dwrite_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("dwrite.dll")) orelse return error.Unavailable; - self.user32_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("user32.dll")) orelse + self.user32_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("user32.dll")) orelse return error.Unavailable; self.d3d11_create_device = loadProc(D3D11CreateDeviceFn, self.d3d11_module, "D3D11CreateDevice") orelse @@ -160,8 +135,8 @@ const Api = struct { return error.Unavailable; // Drawing factories are capability prerequisites, but no factory is // created until shell content actually needs D2D/DWrite resources. - if (GetProcAddress(self.d2d1_module, "D2D1CreateFactory") == null) return error.Unavailable; - if (GetProcAddress(self.dwrite_module, "DWriteCreateFactory") == null) return error.Unavailable; + if (sys.GetProcAddress(self.d2d1_module, "D2D1CreateFactory") == null) return error.Unavailable; + if (sys.GetProcAddress(self.dwrite_module, "DWriteCreateFactory") == null) return error.Unavailable; self.is_window = loadProc(IsWindowFn, self.user32_module, "IsWindow") orelse return error.Unavailable; self.get_ancestor = loadProc(GetAncestorFn, self.user32_module, "GetAncestor") orelse @@ -178,17 +153,17 @@ const Api = struct { self.dcomposition_create_device = null; self.is_window = null; self.get_ancestor = null; - if (self.user32_module) |module| _ = FreeLibrary(module); - if (self.dwrite_module) |module| _ = FreeLibrary(module); - if (self.d2d1_module) |module| _ = FreeLibrary(module); - if (self.dcomp_module) |module| _ = FreeLibrary(module); - if (self.d3d11_module) |module| _ = FreeLibrary(module); + if (self.user32_module) |module| _ = sys.FreeLibrary(module); + if (self.dwrite_module) |module| _ = sys.FreeLibrary(module); + if (self.d2d1_module) |module| _ = sys.FreeLibrary(module); + if (self.dcomp_module) |module| _ = sys.FreeLibrary(module); + if (self.d3d11_module) |module| _ = sys.FreeLibrary(module); self.* = .{}; } }; fn loadProc(comptime T: type, module: HMODULE, comptime name: [:0]const u8) ?T { - const raw = GetProcAddress(module, name.ptr) orelse return null; + const raw = sys.GetProcAddress(module, name.ptr) orelse return null; return @ptrCast(@alignCast(raw)); } @@ -557,7 +532,9 @@ pub const NativeDriver = struct { const is_window = self.api.is_window orelse return false; const get_ancestor = self.api.get_ancestor orelse return false; const hwnd: HWND = @ptrFromInt(window); - return is_window(hwnd) != 0 and get_ancestor(hwnd, GA_ROOT) == hwnd; + if (is_window(hwnd) == 0) return false; + const ancestor = get_ancestor(hwnd, GA_ROOT) orelse return false; + return ancestor == hwnd; } }; @@ -569,8 +546,8 @@ const ClientGeometry = struct { fn clientGeometry(hwnd: HWND) ?ClientGeometry { var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) return null; - const dpi_value = GetDpiForWindow(hwnd); + if (sys.GetClientRect(hwnd, &rect) == 0) return null; + const dpi_value = sys.GetDpiForWindow(hwnd); const dpi: f32 = @floatFromInt(if (dpi_value == 0) @as(UINT, 96) else dpi_value); const scale = 96.0 / dpi; return .{ @@ -693,7 +670,7 @@ test "native driver attaches commits and detaches a real top-level HWND" { defer backend.deinit(); if (backend.status().state != .ready) return error.SkipZigTest; - const hwnd = CreateWindowExW( + const hwnd = sys.CreateWindowExW( 0, std.unicode.utf8ToUtf16LeStringLiteral("STATIC"), std.unicode.utf8ToUtf16LeStringLiteral("noctty compositor smoke"), @@ -707,7 +684,7 @@ test "native driver attaches commits and detaches a real top-level HWND" { null, null, ) orelse return error.SkipZigTest; - defer _ = DestroyWindow(hwnd); + defer _ = sys.DestroyWindow(hwnd); try backend.attachWindow(@intFromPtr(hwnd)); try std.testing.expect(backend.status().compositionActive()); diff --git a/src/apprt/win32_compositor_window_content_native.zig b/src/apprt/win32_compositor_window_content_native.zig index e4af5331fb9..87e5e1cf540 100644 --- a/src/apprt/win32_compositor_window_content_native.zig +++ b/src/apprt/win32_compositor_window_content_native.zig @@ -12,6 +12,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const sys = @import("win32/sys.zig"); const windows = std.os.windows; const GUID = windows.GUID; @@ -624,40 +625,20 @@ const QueryInterfaceFn = *const fn ( *?*anyopaque, ) callconv(.winapi) HRESULT; -extern "kernel32" fn LoadLibraryW(name: [*:0]const u16) callconv(.winapi) HMODULE; -extern "kernel32" fn GetProcAddress(module: HMODULE, name: [*:0]const u8) callconv(.winapi) ?*const anyopaque; -extern "kernel32" fn FreeLibrary(module: HMODULE) callconv(.winapi) i32; -extern "user32" fn CreateWindowExW( - u32, - [*:0]const u16, - [*:0]const u16, - u32, - i32, - i32, - i32, - i32, - HWND, - ?*anyopaque, - HMODULE, - ?*anyopaque, -) callconv(.winapi) HWND; -extern "user32" fn DestroyWindow(hwnd: HWND) callconv(.winapi) i32; -extern "user32" fn GetAncestor(hwnd: HWND, flags: u32) callconv(.winapi) HWND; - fn loadProc(comptime T: type, module: HMODULE, comptime name: [:0]const u8) ?T { - const raw = GetProcAddress(module, name.ptr) orelse return null; + const raw = sys.GetProcAddress(module, name.ptr) orelse return null; return @ptrCast(@alignCast(raw)); } test "native hidden HWND renders and resizes DirectComposition shell content" { if (builtin.os.tag != .windows) return error.SkipZigTest; - const d3d_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d3d11.dll")) orelse + const d3d_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("d3d11.dll")) orelse return error.SkipZigTest; - defer _ = FreeLibrary(d3d_module); - const dcomp_module = LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("dcomp.dll")) orelse + defer _ = sys.FreeLibrary(d3d_module); + const dcomp_module = sys.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral("dcomp.dll")) orelse return error.SkipZigTest; - defer _ = FreeLibrary(dcomp_module); + defer _ = sys.FreeLibrary(dcomp_module); const create_d3d = loadProc(D3D11CreateDeviceFn, d3d_module, "D3D11CreateDevice") orelse return error.SkipZigTest; const create_dcomp = loadProc(DCompositionCreateDeviceFn, dcomp_module, "DCompositionCreateDevice") orelse @@ -683,7 +664,7 @@ test "native hidden HWND renders and resizes DirectComposition shell content" { if (failed(hr) or dcomp_device == null) return error.SkipZigTest; defer releaseCom(dcomp_device); - const hwnd = CreateWindowExW( + const hwnd = sys.CreateWindowExW( 0, std.unicode.utf8ToUtf16LeStringLiteral("STATIC"), std.unicode.utf8ToUtf16LeStringLiteral("noctty dcomp content smoke"), @@ -697,8 +678,9 @@ test "native hidden HWND renders and resizes DirectComposition shell content" { null, null, ) orelse return error.SkipZigTest; - defer _ = DestroyWindow(hwnd); - try std.testing.expectEqual(hwnd, GetAncestor(hwnd, GA_ROOT)); + defer _ = sys.DestroyWindow(hwnd); + const ancestor = sys.GetAncestor(hwnd, GA_ROOT) orelse return error.SkipZigTest; + try std.testing.expectEqual(hwnd, ancestor); var target: ?*anyopaque = null; hr = call(DeviceCreateTargetFn, dcomp_device.?, 6)(dcomp_device.?, hwnd, 0, &target); diff --git a/src/apprt/win32_icons.zig b/src/apprt/win32_icons.zig index 7d13319a9b1..80a8a8ed627 100644 --- a/src/apprt/win32_icons.zig +++ b/src/apprt/win32_icons.zig @@ -23,6 +23,7 @@ const std = @import("std"); const geometry = @import("win32_geometry.zig"); const win32_types = @import("win32_types.zig"); +const sys = @import("win32/sys.zig"); // -- Public types ----------------------------------------------------------- @@ -53,7 +54,7 @@ pub const Kind = enum { // -- Win32 GDI externs ------------------------------------------------------ -const POINT = geometry.Point; +const POINT = sys.POINT; const HBRUSH = win32_types.HBRUSH; const HDC = win32_types.HDC; @@ -65,19 +66,6 @@ const PS_NULL: i32 = 5; const TRANSPARENT: i32 = 1; const NULL_BRUSH_INDEX: i32 = 5; -extern "gdi32" fn CreatePen(style: i32, width: i32, color: u32) callconv(.winapi) HPEN; -extern "gdi32" fn SelectObject(hdc: HDC, obj: HGDIOBJ) callconv(.winapi) HGDIOBJ; -extern "gdi32" fn DeleteObject(obj: HGDIOBJ) callconv(.winapi) i32; -extern "gdi32" fn MoveToEx(hdc: HDC, x: i32, y: i32, lp: ?*POINT) callconv(.winapi) i32; -extern "gdi32" fn LineTo(hdc: HDC, x: i32, y: i32) callconv(.winapi) i32; -extern "gdi32" fn Ellipse(hdc: HDC, x1: i32, y1: i32, x2: i32, y2: i32) callconv(.winapi) i32; -extern "gdi32" fn Rectangle(hdc: HDC, x1: i32, y1: i32, x2: i32, y2: i32) callconv(.winapi) i32; -extern "gdi32" fn Polygon(hdc: HDC, points: [*]const POINT, count: i32) callconv(.winapi) i32; -extern "gdi32" fn SetBkMode(hdc: HDC, mode: i32) callconv(.winapi) i32; -extern "gdi32" fn CreateSolidBrush(color: u32) callconv(.winapi) HBRUSH; -extern "gdi32" fn SetTextColor(hdc: HDC, color: u32) callconv(.winapi) u32; -extern "gdi32" fn GetStockObject(index: i32) callconv(.winapi) HGDIOBJ; - // -- Geometry helpers (pure -- no GDI) -------------------------------------- /// Inset `r` by `padding` on all four sides. Clamped to a zero-area @@ -142,41 +130,41 @@ const GdiState = struct { fn setupGdi(hdc: HDC, color: u32, is_hc: bool) ?GdiState { const stroke_w: i32 = if (is_hc) 1 else 2; - const pen = CreatePen(PS_SOLID, stroke_w, color) orelse return null; + const pen = sys.CreatePen(PS_SOLID, stroke_w, color) orelse return null; const brush = if (is_hc) - GetStockObject(NULL_BRUSH_INDEX) orelse { - _ = DeleteObject(pen); + sys.GetStockObject(NULL_BRUSH_INDEX) orelse { + _ = sys.DeleteObject(pen); return null; } else - CreateSolidBrush(color) orelse { - _ = DeleteObject(pen); + sys.CreateSolidBrush(color) orelse { + _ = sys.DeleteObject(pen); return null; }; - const old_pen = SelectObject(hdc, pen) orelse { - _ = DeleteObject(pen); - if (!is_hc) _ = DeleteObject(brush); + const old_pen = sys.SelectObject(hdc, pen) orelse { + _ = sys.DeleteObject(pen); + if (!is_hc) _ = sys.DeleteObject(brush); return null; }; - const old_brush = SelectObject(hdc, brush) orelse { - _ = SelectObject(hdc, old_pen); - _ = DeleteObject(pen); - if (!is_hc) _ = DeleteObject(brush); + const old_brush = sys.SelectObject(hdc, brush) orelse { + _ = sys.SelectObject(hdc, old_pen); + _ = sys.DeleteObject(pen); + if (!is_hc) _ = sys.DeleteObject(brush); return null; }; return .{ .pen = pen, .brush = brush, .old_pen = old_pen, .old_brush = old_brush }; } fn teardownGdi(hdc: HDC, gs: GdiState, is_hc: bool) void { - _ = SelectObject(hdc, gs.old_brush); - _ = SelectObject(hdc, gs.old_pen); - if (!is_hc) _ = DeleteObject(gs.brush); - _ = DeleteObject(gs.pen); + _ = sys.SelectObject(hdc, gs.old_brush); + _ = sys.SelectObject(hdc, gs.old_pen); + if (!is_hc) _ = sys.DeleteObject(gs.brush); + _ = sys.DeleteObject(gs.pen); } fn line(hdc: HDC, x1: i32, y1: i32, x2: i32, y2: i32) void { - _ = MoveToEx(hdc, x1, y1, null); - _ = LineTo(hdc, x2, y2); + _ = sys.MoveToEx(hdc, x1, y1, null); + _ = sys.LineTo(hdc, x2, y2); } // -- Per-icon draw routines ------------------------------------------------- @@ -212,21 +200,21 @@ fn drawArrowDown(hdc: HDC, r: Rect) void { fn drawSplitH(hdc: HDC, r: Rect) void { const mid = r.top + @divTrunc(r.height(), 2); - _ = Rectangle(hdc, r.left, r.top, r.right, mid - 1); - _ = Rectangle(hdc, r.left, mid + 1, r.right, r.bottom); + _ = sys.Rectangle(hdc, r.left, r.top, r.right, mid - 1); + _ = sys.Rectangle(hdc, r.left, mid + 1, r.right, r.bottom); } fn drawSplitV(hdc: HDC, r: Rect) void { const mid = r.left + @divTrunc(r.width(), 2); - _ = Rectangle(hdc, r.left, r.top, mid - 1, r.bottom); - _ = Rectangle(hdc, mid + 1, r.top, r.right, r.bottom); + _ = sys.Rectangle(hdc, r.left, r.top, mid - 1, r.bottom); + _ = sys.Rectangle(hdc, mid + 1, r.top, r.right, r.bottom); } fn drawPin(hdc: HDC, r: Rect) void { const cx = r.left + @divTrunc(r.width(), 2); const head_r = @max(@divTrunc(r.width(), 4), 2); // head circle - _ = Ellipse(hdc, cx - head_r, r.top, cx + head_r, r.top + head_r * 2); + _ = sys.Ellipse(hdc, cx - head_r, r.top, cx + head_r, r.top + head_r * 2); // shaft line(hdc, cx, r.top + head_r * 2, cx, r.bottom); } @@ -251,24 +239,24 @@ fn drawWarn(hdc: HDC, r: Rect) void { .{ .x = r.left, .y = r.bottom }, .{ .x = r.right, .y = r.bottom }, }; - _ = Polygon(hdc, &pts, 3); + _ = sys.Polygon(hdc, &pts, 3); // exclamation mark: vertical bar + dot const bang_top = r.top + @divTrunc(r.height(), 3); const bang_bot = r.bottom - @divTrunc(r.height(), 4); line(hdc, cx, bang_top, cx, bang_bot); const dot_y = r.bottom - @divTrunc(r.height(), 8); - _ = Ellipse(hdc, cx - 1, dot_y - 1, cx + 1, dot_y + 1); + _ = sys.Ellipse(hdc, cx - 1, dot_y - 1, cx + 1, dot_y + 1); } fn drawCircleGlyph(hdc: HDC, r: Rect, comptime glyph: enum { info, success, err_x }) void { - _ = Ellipse(hdc, r.left, r.top, r.right, r.bottom); + _ = sys.Ellipse(hdc, r.left, r.top, r.right, r.bottom); const cx = r.left + @divTrunc(r.width(), 2); const cy = r.top + @divTrunc(r.height(), 2); const qh = @divTrunc(r.height(), 4); switch (glyph) { .info => { // dot - _ = Ellipse(hdc, cx - 1, cy - qh - 1, cx + 1, cy - qh + 1); + _ = sys.Ellipse(hdc, cx - 1, cy - qh - 1, cx + 1, cy - qh + 1); // vertical stroke line(hdc, cx, cy - qh + 3, cx, cy + qh); }, @@ -302,21 +290,21 @@ fn drawMaximize(hdc: HDC, r: Rect) void { // `Rectangle` would FILL the rect (producing a solid-coloured // block, which the user sees as a "blank button"). Swap to the // stock NULL brush while we draw so only the outline renders. - const null_brush = GetStockObject(NULL_BRUSH_INDEX) orelse return; - const saved = SelectObject(hdc, null_brush) orelse return; - defer _ = SelectObject(hdc, saved); - _ = Rectangle(hdc, r.left, r.top, r.right, r.bottom); + const null_brush = sys.GetStockObject(NULL_BRUSH_INDEX) orelse return; + const saved = sys.SelectObject(hdc, null_brush) orelse return; + defer _ = sys.SelectObject(hdc, saved); + _ = sys.Rectangle(hdc, r.left, r.top, r.right, r.bottom); } fn drawRestore(hdc: HDC, r: Rect) void { - const null_brush = GetStockObject(NULL_BRUSH_INDEX) orelse return; - const saved = SelectObject(hdc, null_brush) orelse return; - defer _ = SelectObject(hdc, saved); + const null_brush = sys.GetStockObject(NULL_BRUSH_INDEX) orelse return; + const saved = sys.SelectObject(hdc, null_brush) orelse return; + defer _ = sys.SelectObject(hdc, saved); const off = @max(@divTrunc(r.width(), 4), 2); // back square (shifted right + up) - _ = Rectangle(hdc, r.left + off, r.top, r.right, r.bottom - off); + _ = sys.Rectangle(hdc, r.left + off, r.top, r.right, r.bottom - off); // front square (shifted left + down) - _ = Rectangle(hdc, r.left, r.top + off, r.right - off, r.bottom); + _ = sys.Rectangle(hdc, r.left, r.top + off, r.right - off, r.bottom); } fn drawSearch(hdc: HDC, r: Rect) void { @@ -324,7 +312,7 @@ fn drawSearch(hdc: HDC, r: Rect) void { const lens_r = @divTrunc(sz * 5, 12); const cx = r.left + lens_r; const cy = r.top + lens_r; - _ = Ellipse(hdc, cx - lens_r, cy - lens_r, cx + lens_r, cy + lens_r); + _ = sys.Ellipse(hdc, cx - lens_r, cy - lens_r, cx + lens_r, cy + lens_r); // handle: diagonal from bottom-right of circle toward bottom-right corner const hx = cx + @divTrunc(lens_r * 7, 10); const hy = cy + @divTrunc(lens_r * 7, 10); @@ -337,7 +325,7 @@ fn drawSettings(hdc: HDC, r: Rect) void { const outer = @divTrunc(@min(r.width(), r.height()), 2); const inner = @divTrunc(outer, 3); // centre dot - _ = Ellipse(hdc, cx - inner, cy - inner, cx + inner, cy + inner); + _ = sys.Ellipse(hdc, cx - inner, cy - inner, cx + inner, cy + inner); // six teeth at 60-degree intervals, rendered as short radial lines const angles = [6]struct { dx: i32, dy: i32 }{ .{ .dx = 0, .dy = -100 }, @@ -364,7 +352,7 @@ fn drawSettings(hdc: HDC, r: Rect) void { fn drawRegex(hdc: HDC, r: Rect) void { // dot const dot_y = r.bottom - 2; - _ = Ellipse(hdc, r.left, dot_y - 2, r.left + 3, dot_y + 1); + _ = sys.Ellipse(hdc, r.left, dot_y - 2, r.left + 3, dot_y + 1); // asterisk: two crossing lines const sx = r.left + @divTrunc(r.width(), 3); const mid_y = r.top + @divTrunc(r.height(), 2); @@ -385,7 +373,7 @@ fn drawCaseSens(hdc: HDC, r: Rect) void { const arx = r.left + @divTrunc(r.width() * 5, 8); const ar = @divTrunc(r.width(), 6); const acy = r.top + @divTrunc(r.height() * 2, 3); - _ = Ellipse(hdc, arx - ar, acy - ar, arx + ar, acy + ar); + _ = sys.Ellipse(hdc, arx - ar, acy - ar, arx + ar, acy + ar); line(hdc, arx + ar, r.top + @divTrunc(r.height(), 3), arx + ar, r.bottom); } @@ -394,13 +382,13 @@ fn drawWholeWord(hdc: HDC, r: Rect) void { const ar = @divTrunc(r.width(), 6); const acx = r.left + @divTrunc(r.width(), 4); const acy = r.top + @divTrunc(r.height() * 2, 3); - _ = Ellipse(hdc, acx - ar, acy - ar, acx + ar, acy + ar); + _ = sys.Ellipse(hdc, acx - ar, acy - ar, acx + ar, acy + ar); line(hdc, acx + ar, r.top + @divTrunc(r.height(), 3), acx + ar, r.bottom); // "b": vertical stroke + circle on the right const bx = r.left + @divTrunc(r.width() * 3, 5); line(hdc, bx, r.top, bx, r.bottom); const bcx = bx + ar; - _ = Ellipse(hdc, bcx - ar, acy - ar, bcx + ar, acy + ar); + _ = sys.Ellipse(hdc, bcx - ar, acy - ar, bcx + ar, acy + ar); } // -- Public draw entry point ------------------------------------------------ @@ -418,7 +406,7 @@ pub fn drawIcon( const content = iconContentRect(rect); if (!isDrawable(content, 4)) return; - _ = SetBkMode(hdc, TRANSPARENT); + _ = sys.SetBkMode(hdc, TRANSPARENT); const gs = setupGdi(hdc, color, is_hc) orelse return; defer teardownGdi(hdc, gs, is_hc); diff --git a/src/apprt/win32_ipc.zig b/src/apprt/win32_ipc.zig index e8ca578ae93..04c618b306e 100644 --- a/src/apprt/win32_ipc.zig +++ b/src/apprt/win32_ipc.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const apprt = @import("../apprt.zig"); +const sys = @import("win32/sys.zig"); const Allocator = std.mem.Allocator; const windows = std.os.windows; @@ -35,13 +36,6 @@ pub const PerformActionPayload = struct { action_text: []u8, }; -extern "kernel32" fn GetTickCount64() callconv(.winapi) u64; - -extern "kernel32" fn ConnectNamedPipe( - hNamedPipe: windows.HANDLE, - lpOverlapped: ?*anyopaque, -) callconv(.winapi) BOOL; - fn appendU32(dst: *std.ArrayList(u8), alloc: Allocator, value: u32) !void { var buf: [4]u8 = undefined; std.mem.writeInt(u32, &buf, value, .little); @@ -270,7 +264,7 @@ pub fn readDataResponseWithTimeout( pipe: windows.HANDLE, timeout_ms: u64, ) ![]u8 { - const deadline_ms = GetTickCount64() +| timeout_ms; + const deadline_ms = sys.GetTickCount64() +| timeout_ms; var header: [5]u8 = undefined; try readExactUntil(pipe, &header, deadline_ms); if (readU32(header[0..4]) != wire_version) return error.InvalidIpcResponse; @@ -309,11 +303,11 @@ pub fn readExactWithTimeout( dst: []u8, timeout_ms: u64, ) !void { - return readExactUntil(pipe, dst, GetTickCount64() +| timeout_ms); + return readExactUntil(pipe, dst, sys.GetTickCount64() +| timeout_ms); } fn deadline() u64 { - return GetTickCount64() +| io_timeout_ms; + return sys.GetTickCount64() +| io_timeout_ms; } fn pipeIoPending(err: windows.Win32Error) bool { @@ -327,7 +321,7 @@ fn readExactUntil( ) !void { var offset: usize = 0; while (offset < dst.len) { - if (GetTickCount64() >= deadline_ms) return error.IpcTimeout; + if (sys.GetTickCount64() >= deadline_ms) return error.IpcTimeout; var read_len: u32 = 0; if (windows.kernel32.ReadFile( pipe, @@ -351,7 +345,7 @@ fn readExactUntil( } pub fn writeAll(pipe: windows.HANDLE, src: []const u8) !void { - const started_at = GetTickCount64(); + const started_at = sys.GetTickCount64(); var offset: usize = 0; while (offset < src.len) { var write_len: u32 = 0; @@ -364,7 +358,7 @@ pub fn writeAll(pipe: windows.HANDLE, src: []const u8) !void { ) == 0) { const err = windows.kernel32.GetLastError(); if (pipeIoPending(err)) { - if (GetTickCount64() -| started_at >= io_timeout_ms) { + if (sys.GetTickCount64() -| started_at >= io_timeout_ms) { return error.IpcTimeout; } std.Thread.sleep(poll_interval_ns); @@ -383,7 +377,7 @@ test "win32 IPC silent client read is bounded" { const pipe_name_utf8 = try std.fmt.allocPrintSentinel( std.testing.allocator, "\\\\.\\pipe\\noctty-ipc-timeout-{d}", - .{GetTickCount64()}, + .{sys.GetTickCount64()}, 0, ); defer std.testing.allocator.free(pipe_name_utf8); @@ -406,7 +400,7 @@ test "win32 IPC silent client read is bounded" { try std.testing.expect(server != windows.INVALID_HANDLE_VALUE); defer _ = windows.CloseHandle(server); - try std.testing.expectEqual(@as(BOOL, 0), ConnectNamedPipe(server, null)); + try std.testing.expectEqual(@as(BOOL, 0), sys.ConnectNamedPipe(server, null)); try std.testing.expectEqual(windows.Win32Error.PIPE_LISTENING, windows.kernel32.GetLastError()); const client = windows.kernel32.CreateFileW( @@ -421,7 +415,7 @@ test "win32 IPC silent client read is bounded" { try std.testing.expect(client != windows.INVALID_HANDLE_VALUE); defer _ = windows.CloseHandle(client); - const connected = ConnectNamedPipe(server, null); + const connected = sys.ConnectNamedPipe(server, null); if (connected == 0) { try std.testing.expectEqual(windows.Win32Error.PIPE_CONNECTED, windows.kernel32.GetLastError()); } diff --git a/src/apprt/win32_job_object.zig b/src/apprt/win32_job_object.zig index 76c1f69eacb..730179e0b9a 100644 --- a/src/apprt/win32_job_object.zig +++ b/src/apprt/win32_job_object.zig @@ -10,6 +10,7 @@ const std = @import("std"); const windows = std.os.windows; const configpkg = @import("../config.zig"); const win32_types = @import("win32_types.zig"); +const sys = @import("win32/sys.zig"); pub const HANDLE = windows.HANDLE; pub const DWORD = windows.DWORD; @@ -24,12 +25,7 @@ pub const JOB_OBJECT_LIMIT_ACTIVE_PROCESS: DWORD = 0x00000008; pub const JOB_OBJECT_LIMIT_JOB_MEMORY: DWORD = 0x00000200; pub const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: DWORD = 0x00002000; -pub const JOBOBJECTINFOCLASS = enum(i32) { - basic_limit_information = 2, - basic_process_id_list = 3, - extended_limit_information = 9, - _, -}; +pub const JOBOBJECTINFOCLASS = sys.JOBOBJECTINFOCLASS; pub const IO_COUNTERS = extern struct { ReadOperationCount: u64 = 0, @@ -61,38 +57,12 @@ pub const JOBOBJECT_EXTENDED_LIMIT_INFORMATION = extern struct { PeakJobMemoryUsed: SIZE_T = 0, }; -pub extern "kernel32" fn CreateJobObjectW( - lpJobAttributes: ?*SECURITY_ATTRIBUTES, - lpName: ?LPCWSTR, -) callconv(.winapi) ?HANDLE; - -pub extern "kernel32" fn SetInformationJobObject( - hJob: HANDLE, - JobObjectInfoClass: JOBOBJECTINFOCLASS, - lpJobObjectInfo: *const anyopaque, - cbJobObjectInfoLength: DWORD, -) callconv(.winapi) BOOL; - -pub extern "kernel32" fn QueryInformationJobObject( - hJob: HANDLE, - JobObjectInfoClass: JOBOBJECTINFOCLASS, - lpJobObjectInfo: *anyopaque, - cbJobObjectInfoLength: DWORD, - lpReturnLength: ?*DWORD, -) callconv(.winapi) BOOL; - -pub extern "kernel32" fn AssignProcessToJobObject( - hJob: HANDLE, - hProcess: HANDLE, -) callconv(.winapi) BOOL; - -pub extern "kernel32" fn GetProcessId(Process: HANDLE) callconv(.winapi) DWORD; - -pub extern "kernel32" fn OpenProcess( - dwDesiredAccess: DWORD, - bInheritHandle: BOOL, - dwProcessId: DWORD, -) callconv(.winapi) ?HANDLE; +pub const CreateJobObjectW = sys.CreateJobObjectW; +pub const SetInformationJobObject = sys.SetInformationJobObject; +pub const QueryInformationJobObject = sys.QueryInformationJobObject; +pub const AssignProcessToJobObject = sys.AssignProcessToJobObject; +pub const GetProcessId = sys.GetProcessId; +pub const OpenProcess = sys.OpenProcess; pub const Mode = enum { never, diff --git a/src/apprt/win32_session_persistence.zig b/src/apprt/win32_session_persistence.zig index 718eb3c147c..3890742b939 100644 --- a/src/apprt/win32_session_persistence.zig +++ b/src/apprt/win32_session_persistence.zig @@ -1,6 +1,7 @@ //! Windows session-state file I/O and recovery policy. const std = @import("std"); +const sys = @import("win32/sys.zig"); const schema = @import("win32_session_state.zig"); const Allocator = std.mem.Allocator; @@ -111,17 +112,14 @@ fn replaceFileAtomicWindows(absolute_path: []const u8, temporary_path: []const u const temporary_w = try std.unicode.utf8ToUtf16LeAllocZ(std.heap.page_allocator, temporary_path); defer std.heap.page_allocator.free(temporary_w); - if (ReplaceFileW(target_w.ptr, temporary_w.ptr, null, 0, null, null) == 0 and - MoveFileExW(temporary_w.ptr, target_w.ptr, MOVEFILE_REPLACE_EXISTING) == 0) + if (sys.ReplaceFileW(target_w.ptr, temporary_w.ptr, null, 0, null, null) == 0 and + sys.MoveFileExW(temporary_w.ptr, target_w.ptr, MOVEFILE_REPLACE_EXISTING) == 0) { return windows.unexpectedError(windows.kernel32.GetLastError()); } } const MOVEFILE_REPLACE_EXISTING: u32 = 0x00000001; -extern "kernel32" fn ReplaceFileW([*:0]const u16, [*:0]const u16, ?[*:0]const u16, u32, ?*anyopaque, ?*anyopaque) callconv(.winapi) i32; -extern "kernel32" fn MoveFileExW([*:0]const u16, [*:0]const u16, u32) callconv(.winapi) i32; - test "win32 session persistence load distinguishes missing corrupt oversized and transient reads" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); diff --git a/src/apprt/win32_settings.zig b/src/apprt/win32_settings.zig index 671dd008bb0..95feb842d93 100644 --- a/src/apprt/win32_settings.zig +++ b/src/apprt/win32_settings.zig @@ -25,6 +25,8 @@ const win32_types = @import("win32_types.zig"); const win32_theme = @import("win32_theme.zig"); const win32_uia = @import("win32_uia/mod.zig"); const settings_transaction = @import("win32_settings_transaction.zig"); +const sys = @import("win32/sys.zig"); +const SetFocus = sys.SetFocus; /// Minimal set of Win32 aliases + externs we need here. Shared ABI structs /// live in `win32_types.zig` so this module stays free of an `*App` type @@ -49,7 +51,7 @@ const COLORREF = win32_types.COLORREF; const RECT = win32_types.RECT; const GUID = windows.GUID; const HRESULT = windows.HRESULT; -const HMONITOR = *anyopaque; +const HMONITOR = sys.HMONITOR; const WS_OVERLAPPEDWINDOW: u32 = 0x00CF0000; const WS_MAXIMIZEBOX: u32 = 0x00010000; @@ -505,89 +507,6 @@ fn comboIndexFromLinkPreviews(value: Config.LinkPreviews) usize { }; } -extern "user32" fn RegisterClassExW(lpwcx: *const WNDCLASSEXW) callconv(.winapi) ATOM; -extern "user32" fn CreateWindowExW( - dwExStyle: u32, - lpClassName: LPCWSTR, - lpWindowName: LPCWSTR, - dwStyle: u32, - X: i32, - Y: i32, - nWidth: i32, - nHeight: i32, - hWndParent: ?HWND, - hMenu: HMENU, - hInstance: HINSTANCE, - lpParam: ?*anyopaque, -) callconv(.winapi) ?HWND; -extern "user32" fn DefWindowProcW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; -extern "user32" fn CallWindowProcW(lpPrevWndFunc: ?*const anyopaque, hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; -extern "user32" fn ShowWindow(hWnd: HWND, nCmdShow: i32) callconv(.winapi) BOOL; -extern "user32" fn EnableWindow(hWnd: HWND, bEnable: BOOL) callconv(.winapi) BOOL; -extern "user32" fn SetForegroundWindow(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn SetFocus(hWnd: HWND) callconv(.winapi) ?HWND; -extern "user32" fn GetFocus() callconv(.winapi) ?HWND; -extern "user32" fn GetParent(hWnd: HWND) callconv(.winapi) ?HWND; -extern "user32" fn IsChild(hWndParent: HWND, hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn IsWindowVisible(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn IsWindowEnabled(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn PostMessageW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) BOOL; -extern "user32" fn DestroyWindow(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn GetClientRect(hWnd: HWND, lpRect: *RECT) callconv(.winapi) BOOL; -extern "user32" fn GetWindowRect(hWnd: HWND, lpRect: *RECT) callconv(.winapi) BOOL; -extern "user32" fn ScreenToClient(hWnd: HWND, lpPoint: *POINT) callconv(.winapi) BOOL; -extern "user32" fn GetDpiForWindow(hWnd: HWND) callconv(.winapi) UINT; -extern "user32" fn SetWindowPos(hWnd: HWND, hWndInsertAfter: ?HWND, X: i32, Y: i32, cx: i32, cy: i32, uFlags: UINT) callconv(.winapi) BOOL; -extern "user32" fn SystemParametersInfoW(uiAction: UINT, uiParam: UINT, pvParam: *RECT, fWinIni: UINT) callconv(.winapi) BOOL; -extern "user32" fn MonitorFromWindow(hwnd: HWND, dwFlags: u32) callconv(.winapi) ?HMONITOR; -extern "user32" fn GetMonitorInfoW(hMonitor: HMONITOR, lpmi: *MONITORINFO) callconv(.winapi) BOOL; -extern "user32" fn LoadCursorW(hInstance: ?HINSTANCE, lpCursorName: LPCWSTR) callconv(.winapi) HCURSOR; -extern "user32" fn SetWindowLongPtrW(hWnd: HWND, nIndex: i32, dwNewLong: LONG_PTR) callconv(.winapi) LONG_PTR; -extern "user32" fn GetWindowLongPtrW(hWnd: HWND, nIndex: i32) callconv(.winapi) LONG_PTR; -extern "user32" fn BeginPaint(hWnd: HWND, lpPaint: *PAINTSTRUCT) callconv(.winapi) HDC; -extern "user32" fn EndPaint(hWnd: HWND, lpPaint: *const PAINTSTRUCT) callconv(.winapi) BOOL; -extern "user32" fn GetDC(hWnd: HWND) callconv(.winapi) HDC; -extern "user32" fn ReleaseDC(hWnd: HWND, hDC: HDC) callconv(.winapi) i32; -extern "user32" fn DrawTextW(hDC: HDC, lpchText: [*:0]const u16, cchText: i32, lprc: *RECT, format: UINT) callconv(.winapi) i32; -extern "user32" fn IsWindow(hWnd: ?HWND) callconv(.winapi) BOOL; -extern "user32" fn IsIconic(hWnd: HWND) callconv(.winapi) BOOL; -extern "user32" fn InvalidateRect(hWnd: HWND, lpRect: ?*const RECT, bErase: BOOL) callconv(.winapi) BOOL; -extern "user32" fn GetWindowTextW(hWnd: HWND, lpString: [*]u16, nMaxCount: i32) callconv(.winapi) i32; -extern "user32" fn SendMessageW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT; -extern "user32" fn SetScrollRange(hWnd: HWND, nBar: c_int, nMinPos: c_int, nMaxPos: c_int, bRedraw: BOOL) callconv(.winapi) BOOL; -extern "user32" fn SetScrollPos(hWnd: HWND, nBar: c_int, nPos: c_int, bRedraw: BOOL) callconv(.winapi) c_int; -extern "user32" fn ShowScrollBar(hWnd: HWND, wBar: c_int, bShow: BOOL) callconv(.winapi) BOOL; -extern "user32" fn SetWindowRgn(hWnd: HWND, hRgn: ?HRGN, bRedraw: BOOL) callconv(.winapi) c_int; -extern "user32" fn GetSysColor(nIndex: c_int) callconv(.winapi) COLORREF; -extern "user32" fn NotifyWinEvent(event: u32, hwnd: HWND, idObject: i32, idChild: i32) callconv(.winapi) void; -extern "user32" fn EnumChildWindows(hWndParent: HWND, lpEnumFunc: *const fn (HWND, LPARAM) callconv(.winapi) BOOL, lParam: LPARAM) callconv(.winapi) BOOL; -extern "gdi32" fn CreateFontW( - cHeight: i32, - cWidth: i32, - cEscapement: i32, - cOrientation: i32, - cWeight: i32, - bItalic: u32, - bUnderline: u32, - bStrikeOut: u32, - iCharSet: u32, - iOutPrecision: u32, - iClipPrecision: u32, - iQuality: u32, - iPitchAndFamily: u32, - pszFaceName: LPCWSTR, -) callconv(.winapi) HGDIOBJ; -extern "gdi32" fn DeleteObject(ho: HGDIOBJ) callconv(.winapi) BOOL; -extern "gdi32" fn SelectObject(hdc: HDC, h: HGDIOBJ) callconv(.winapi) HGDIOBJ; -extern "gdi32" fn CreateRectRgn(x1: i32, y1: i32, x2: i32, y2: i32) callconv(.winapi) ?HRGN; -extern "kernel32" fn MulDiv(nNumber: i32, nNumerator: i32, nDenominator: i32) callconv(.winapi) i32; -extern "gdi32" fn FillRect(hdc: HDC, lprc: *const RECT, hbr: HBRUSH) callconv(.winapi) i32; -extern "gdi32" fn GetStockObject(i: i32) callconv(.winapi) HGDIOBJ; -extern "gdi32" fn SetDCBrushColor(hdc: HDC, color: COLORREF) callconv(.winapi) COLORREF; -extern "gdi32" fn CreateSolidBrush(color: COLORREF) callconv(.winapi) HBRUSH; -extern "gdi32" fn SetBkColor(hdc: HDC, color: COLORREF) callconv(.winapi) COLORREF; -extern "gdi32" fn SetTextColor(hdc: HDC, color: COLORREF) callconv(.winapi) COLORREF; -extern "gdi32" fn SetBkMode(hdc: HDC, mode: c_int) callconv(.winapi) c_int; const DC_BRUSH: i32 = 18; const TRANSPARENT: c_int = 1; const COLOR_WINDOW: c_int = 5; @@ -607,12 +526,7 @@ const MINMAXINFO = extern struct { ptMinTrackSize: POINT, ptMaxTrackSize: POINT, }; -const MONITORINFO = extern struct { - cbSize: u32, - rcMonitor: RECT, - rcWork: RECT, - dwFlags: u32, -}; +const MONITORINFO = sys.MONITORINFO; const class_name = std.unicode.utf8ToUtf16LeStringLiteral("noctty.win32.settings"); const edit_text_max_code_units: usize = 4096; @@ -749,10 +663,10 @@ const SettingsThemeAdapter = struct { } fn deleteBrushes(self: *SettingsThemeAdapter) void { - if (self.window_brush) |brush| _ = DeleteObject(brush); - if (self.rail_brush) |brush| _ = DeleteObject(brush); - if (self.edit_brush) |brush| _ = DeleteObject(brush); - if (self.button_brush) |brush| _ = DeleteObject(brush); + if (self.window_brush) |brush| _ = sys.DeleteObject(brush); + if (self.rail_brush) |brush| _ = sys.DeleteObject(brush); + if (self.edit_brush) |brush| _ = sys.DeleteObject(brush); + if (self.button_brush) |brush| _ = sys.DeleteObject(brush); self.window_brush = null; self.rail_brush = null; self.edit_brush = null; @@ -769,20 +683,20 @@ const SettingsThemeAdapter = struct { defer self.applying = false; self.deleteBrushes(); self.colors = colors; - self.window_brush = CreateSolidBrush(colors.window_bg); - self.rail_brush = CreateSolidBrush(colors.rail_bg); - self.edit_brush = CreateSolidBrush(colors.edit_bg); - self.button_brush = CreateSolidBrush(colors.button_bg); + self.window_brush = sys.CreateSolidBrush(colors.window_bg); + self.rail_brush = sys.CreateSolidBrush(colors.rail_bg); + self.edit_brush = sys.CreateSolidBrush(colors.edit_bg); + self.button_brush = sys.CreateSolidBrush(colors.button_bg); win32_theme.WindowThemeAdapter.applySettings(hwnd, colors); applyNativeControlTheme(hwnd, colors); - _ = EnumChildWindows( + _ = sys.EnumChildWindows( hwnd, applyNativeControlThemeToChild, @bitCast(@intFromPtr(&self.colors)), ); - _ = InvalidateRect(hwnd, null, 1); - _ = EnumChildWindows(hwnd, invalidateThemeChild, 0); + _ = sys.InvalidateRect(hwnd, null, 1); + _ = sys.EnumChildWindows(hwnd, invalidateThemeChild, 0); } fn controlBrush(self: *const SettingsThemeAdapter, message: UINT) HBRUSH { @@ -807,7 +721,7 @@ fn applyNativeControlThemeToChild(hwnd: HWND, lParam: LPARAM) callconv(.winapi) } fn invalidateThemeChild(hwnd: HWND, _: LPARAM) callconv(.winapi) BOOL { - _ = InvalidateRect(hwnd, null, 1); + _ = sys.InvalidateRect(hwnd, null, 1); return 1; } @@ -1029,15 +943,15 @@ pub const SettingsWindow = struct { } fn deleteUiFont(self: *SettingsWindow) void { - if (self.ui_font) |font| _ = DeleteObject(font); + if (self.ui_font) |font| _ = sys.DeleteObject(font); self.ui_font = null; - if (self.header_font) |font| _ = DeleteObject(font); + if (self.header_font) |font| _ = sys.DeleteObject(font); self.header_font = null; } pub fn deinit(self: *SettingsWindow) void { if (self.hwnd) |h| { - if (IsWindow(h) != 0) _ = DestroyWindow(h); + if (sys.IsWindow(h) != 0) _ = sys.DestroyWindow(h); } self.releaseSectionUiaProviders(); self.releaseTextUiaProviders(); @@ -1199,7 +1113,7 @@ pub const SettingsWindow = struct { !self.hasRawScalarEdits() and self.conflictCount() == 0 and self.hasPendingChanges(); - if (self.btn_save) |button| _ = EnableWindow(button, @intFromBool(enabled)); + if (self.btn_save) |button| _ = sys.EnableWindow(button, @intFromBool(enabled)); self.updateClosePromptActions(); } @@ -1290,7 +1204,7 @@ pub const SettingsWindow = struct { self.setStatus(self.raw_scalar_error[@intFromEnum(field)].?); if (focus) if (destination.hwnd) |control| { self.setActiveSection(destination.section); - _ = SetFocus(control); + _ = sys.SetFocus(control); self.ensureControlVisible(control); }; }, @@ -1299,7 +1213,7 @@ pub const SettingsWindow = struct { self.validation_control = control; self.setStatus(self.owned_validation_message orelse "A settings value is invalid."); if (focus) { - _ = SetFocus(control); + _ = sys.SetFocus(control); self.ensureControlVisible(control); } }, @@ -1428,10 +1342,10 @@ pub const SettingsWindow = struct { } fn moveFocusBeforeDisablingMutableControls(self: *SettingsWindow) void { - const focused = GetFocus() orelse return; + const focused = sys.GetFocus() orelse return; var should_move = false; for (self.mutableControls()) |control| { - if (control != null and (control.? == focused or IsChild(control.?, focused) != 0)) { + if (control != null and (control.? == focused or sys.IsChild(control.?, focused) != 0)) { should_move = true; break; } @@ -1453,12 +1367,12 @@ pub const SettingsWindow = struct { self.btn_close_keep_editing else self.sectionButton(self.active_section); - if (target) |hwnd| _ = SetFocus(hwnd); + if (target) |hwnd| _ = sys.SetFocus(hwnd); } fn setMutableControlsEnabled(self: *SettingsWindow, enabled: bool) void { for (self.mutableControls()) |control| { - if (control) |hwnd| _ = EnableWindow(hwnd, @intFromBool(enabled)); + if (control) |hwnd| _ = sys.EnableWindow(hwnd, @intFromBool(enabled)); } } @@ -1625,22 +1539,22 @@ pub const SettingsWindow = struct { } }; self.active_owned_conflict_field = next_owned; - const focused = GetFocus(); + const focused = sys.GetFocus(); const conflict_button_focused = focused != null and (focused == self.btn_conflict_keep or focused == self.btn_conflict_use_disk); const show: i32 = if (!self.close_prompt_visible and (next != null or next_owned != null)) SW_SHOWNORMAL else SW_HIDE; - if (self.btn_conflict_keep) |button| _ = ShowWindow(button, show); - if (self.btn_conflict_use_disk) |button| _ = ShowWindow(button, show); + if (self.btn_conflict_keep) |button| _ = sys.ShowWindow(button, show); + if (self.btn_conflict_use_disk) |button| _ = sys.ShowWindow(button, show); if (show == SW_HIDE and conflict_button_focused) self.focusAfterConflictResolution(); } fn focusAfterConflictResolution(self: *SettingsWindow) void { const validation = if (self.validation_control) |control| - if (IsWindowVisible(control) != 0 and IsWindowEnabled(control) != 0) control else null + if (sys.IsWindowVisible(control) != 0 and sys.IsWindowEnabled(control) != 0) control else null else null; const save_hwnd = if (self.btn_save) |button| - if (IsWindowVisible(button) != 0 and IsWindowEnabled(button) != 0) button else null + if (sys.IsWindowVisible(button) != 0 and sys.IsWindowEnabled(button) != 0) button else null else null; const section = self.sectionButton(self.active_section); @@ -1650,7 +1564,7 @@ pub const SettingsWindow = struct { .section => section, .none => null, }; - if (target) |control| _ = SetFocus(control); + if (target) |control| _ = sys.SetFocus(control); } fn resolveOwnedConflict(self: *SettingsWindow, field: OwnedSettingField, resolution: ConflictResolution) void { @@ -1816,7 +1730,7 @@ pub const SettingsWindow = struct { std.log.warn("settings text UIA provider unavailable index={} err={}", .{ index, err }); continue; }; - const previous = SetWindowLongPtrW( + const previous = sys.SetWindowLongPtrW( text, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(&settingsTextProc))), @@ -1828,7 +1742,7 @@ pub const SettingsWindow = struct { const proc: *const anyopaque = @ptrFromInt(@as(usize, @intCast(previous))); if (self.text_uia_prev_proc) |existing| { if (existing != proc) { - _ = SetWindowLongPtrW(text, GWLP_WNDPROC, previous); + _ = sys.SetWindowLongPtrW(text, GWLP_WNDPROC, previous); _ = win32_uia.SettingsControlProvider.Release(&provider.base); continue; } @@ -1932,7 +1846,7 @@ pub const SettingsWindow = struct { std.log.warn("settings control UIA provider unavailable index={} err={}", .{ index, err }); continue; }; - const previous = SetWindowLongPtrW( + const previous = sys.SetWindowLongPtrW( control, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(&settingsControlProc))), @@ -1996,7 +1910,7 @@ pub const SettingsWindow = struct { std.log.warn("settings section UIA provider unavailable section={s} err={}", .{ section.headerText(), err }); continue; }; - const previous = SetWindowLongPtrW( + const previous = sys.SetWindowLongPtrW( button, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(&settingsSectionButtonProc))), @@ -2008,7 +1922,7 @@ pub const SettingsWindow = struct { const proc: *const anyopaque = @ptrFromInt(@as(usize, @intCast(previous))); if (self.section_button_prev_proc) |existing| { if (existing != proc) { - _ = SetWindowLongPtrW(button, GWLP_WNDPROC, previous); + _ = sys.SetWindowLongPtrW(button, GWLP_WNDPROC, previous); _ = win32_uia.SettingsSectionProvider.Release(&provider.base); continue; } @@ -2031,7 +1945,7 @@ pub const SettingsWindow = struct { layoutChildren(self); if (changed) { if (self.sectionProvider(next)) |provider| provider.raiseSelected(); - if (self.hwnd) |h| _ = InvalidateRect(h, null, 1); + if (self.hwnd) |h| _ = sys.InvalidateRect(h, null, 1); } } @@ -2039,9 +1953,9 @@ pub const SettingsWindow = struct { const clamped = std.math.clamp(next, 0, self.content_scroll_max); if (clamped == self.content_scroll_y) return; self.content_scroll_y = clamped; - if (self.hwnd) |hwnd| _ = SetScrollPos(hwnd, SB_VERT, clamped, 1); + if (self.hwnd) |hwnd| _ = sys.SetScrollPos(hwnd, SB_VERT, clamped, 1); layoutChildren(self); - if (self.hwnd) |hwnd| _ = InvalidateRect(hwnd, null, 1); + if (self.hwnd) |hwnd| _ = sys.InvalidateRect(hwnd, null, 1); } fn refreshNativeSectionText(self: *SettingsWindow) void { @@ -2059,13 +1973,13 @@ pub const SettingsWindow = struct { fn setStatus(self: *SettingsWindow, text: []const u8) void { const status = self.text_status orelse return; if (!setWindowTextUtf8IfChanged(status, text)) return; - NotifyWinEvent(EVENT_OBJECT_NAMECHANGE, status, @bitCast(OBJID_CLIENT), @bitCast(CHILDID_SELF)); + sys.NotifyWinEvent(EVENT_OBJECT_NAMECHANGE, status, @bitCast(OBJID_CLIENT), @bitCast(CHILDID_SELF)); } fn recreateUiFont(self: *SettingsWindow) void { const hwnd = self.hwnd orelse return; - const next = CreateFontW( - -MulDiv(9, @intCast(self.dpi), 72), + const next = sys.CreateFontW( + -sys.MulDiv(9, @intCast(self.dpi), 72), 0, 0, 0, @@ -2080,8 +1994,8 @@ pub const SettingsWindow = struct { 0, std.unicode.utf8ToUtf16LeStringLiteral("Segoe UI"), ) orelse return; - const next_header = CreateFontW( - -MulDiv(12, @intCast(self.dpi), 72), + const next_header = sys.CreateFontW( + -sys.MulDiv(12, @intCast(self.dpi), 72), 0, 0, 0, @@ -2102,14 +2016,14 @@ pub const SettingsWindow = struct { self.header_font = next_header; self.close_prompt_measure_width = -1; self.close_prompt_measure_height = 0; - _ = EnumChildWindows(hwnd, applyChildFont, @bitCast(@intFromPtr(next))); + _ = sys.EnumChildWindows(hwnd, applyChildFont, @bitCast(@intFromPtr(next))); if (next_header) |font| { if (self.text_header) |header| { - _ = SendMessageW(header, WM_SETFONT, @intFromPtr(font), 1); + _ = sys.SendMessageW(header, WM_SETFONT, @intFromPtr(font), 1); } } - if (previous) |font| _ = DeleteObject(font); - if (previous_header) |font| _ = DeleteObject(font); + if (previous) |font| _ = sys.DeleteObject(font); + if (previous_header) |font| _ = sys.DeleteObject(font); } pub fn themeChanged(self: *SettingsWindow) void { @@ -2118,7 +2032,7 @@ pub const SettingsWindow = struct { } self.recreateUiFont(); layoutChildren(self); - if (self.hwnd) |hwnd| _ = InvalidateRect(hwnd, null, 1); + if (self.hwnd) |hwnd| _ = sys.InvalidateRect(hwnd, null, 1); } fn resolveThemeColors(self: *const SettingsWindow) win32_theme.SettingsColors { @@ -2137,11 +2051,11 @@ pub const SettingsWindow = struct { return win32_theme.settingsColors( theme, .{ - .window = GetSysColor(COLOR_WINDOW), - .window_text = GetSysColor(COLOR_WINDOWTEXT), - .button_face = GetSysColor(COLOR_BTNFACE), - .button_text = GetSysColor(COLOR_BTNTEXT), - .gray_text = GetSysColor(COLOR_GRAYTEXT), + .window = sys.GetSysColor(COLOR_WINDOW), + .window_text = sys.GetSysColor(COLOR_WINDOWTEXT), + .button_face = sys.GetSysColor(COLOR_BTNFACE), + .button_text = sys.GetSysColor(COLOR_BTNTEXT), + .gray_text = sys.GetSysColor(COLOR_GRAYTEXT), }, high_contrast, ); @@ -2151,10 +2065,10 @@ pub const SettingsWindow = struct { const hwnd = self.hwnd orelse return; var child_rect: RECT = undefined; var client_rect: RECT = undefined; - if (GetWindowRect(control, &child_rect) == 0 or - GetClientRect(hwnd, &client_rect) == 0) return; + if (sys.GetWindowRect(control, &child_rect) == 0 or + sys.GetClientRect(hwnd, &client_rect) == 0) return; var child_origin: POINT = .{ .x = child_rect.left, .y = child_rect.top }; - if (ScreenToClient(hwnd, &child_origin) == 0) return; + if (sys.ScreenToClient(hwnd, &child_origin) == 0) return; const top = child_origin.y; const bottom = top + child_rect.bottom - child_rect.top; const viewport_top = settingsContentViewportTop(self, client_rect); @@ -2177,47 +2091,47 @@ pub const SettingsWindow = struct { for (std.enums.values(Section)) |section| { if (self.sectionButton(section)) |button| { - _ = SendMessageW( + _ = sys.SendMessageW( button, BM_SETCHECK, if (section == self.active_section) BST_CHECKED else BST_UNCHECKED, 0, ); - const style: u32 = @truncate(@as(usize, @bitCast(GetWindowLongPtrW(button, GWL_STYLE)))); + const style: u32 = @truncate(@as(usize, @bitCast(sys.GetWindowLongPtrW(button, GWL_STYLE)))); const next_style = if (section == self.active_section) style | WS_TABSTOP else style & ~WS_TABSTOP; - if (next_style != style) _ = SetWindowLongPtrW(button, GWL_STYLE, @intCast(next_style)); + if (next_style != style) _ = sys.SetWindowLongPtrW(button, GWL_STYLE, @intCast(next_style)); } } - if (self.btn_open_editor) |btn| _ = ShowWindow(btn, show_advanced); - if (self.btn_keybindings_editor) |btn| _ = ShowWindow(btn, show_keybindings); - if (self.edit_scrollback) |e| _ = ShowWindow(e, show_terminal); - if (self.combo_confirm_close) |e| _ = ShowWindow(e, show_terminal); - if (self.combo_copy_on_select) |e| _ = ShowWindow(e, show_terminal); - if (self.chk_trim_trail) |e| _ = ShowWindow(e, show_terminal); - if (self.chk_desktop_notifications) |e| _ = ShowWindow(e, show_privacy); - if (self.chk_app_notify_clipboard) |e| _ = ShowWindow(e, show_privacy); - if (self.chk_app_notify_config) |e| _ = ShowWindow(e, show_privacy); - if (self.combo_clipboard_read) |e| _ = ShowWindow(e, show_privacy); - if (self.combo_clipboard_write) |e| _ = ShowWindow(e, show_privacy); - if (self.combo_link_url) |e| _ = ShowWindow(e, show_privacy); - if (self.combo_link_previews) |e| _ = ShowWindow(e, show_privacy); - if (self.edit_font_family) |e| _ = ShowWindow(e, show_appearance); - if (self.edit_font_size) |e| _ = ShowWindow(e, show_appearance); - if (self.edit_theme) |e| _ = ShowWindow(e, show_appearance); - if (self.edit_bg_opacity) |e| _ = ShowWindow(e, show_appearance); - if (self.combo_window_theme) |e| _ = ShowWindow(e, show_appearance); - if (self.combo_cursor_style) |e| _ = ShowWindow(e, show_appearance); - if (self.edit_pad_x) |e| _ = ShowWindow(e, show_appearance); - if (self.edit_pad_y) |e| _ = ShowWindow(e, show_appearance); - if (self.chk_bg_blur) |e| _ = ShowWindow(e, show_appearance); - if (self.combo_pad_balance) |e| _ = ShowWindow(e, show_appearance); - if (self.edit_command) |e| _ = ShowWindow(e, show_shell); - if (self.combo_shell_integ) |e| _ = ShowWindow(e, show_shell); - if (self.combo_auto_update) |e| _ = ShowWindow(e, show_updates); - if (self.combo_auto_update_channel) |e| _ = ShowWindow(e, show_updates); + if (self.btn_open_editor) |btn| _ = sys.ShowWindow(btn, show_advanced); + if (self.btn_keybindings_editor) |btn| _ = sys.ShowWindow(btn, show_keybindings); + if (self.edit_scrollback) |e| _ = sys.ShowWindow(e, show_terminal); + if (self.combo_confirm_close) |e| _ = sys.ShowWindow(e, show_terminal); + if (self.combo_copy_on_select) |e| _ = sys.ShowWindow(e, show_terminal); + if (self.chk_trim_trail) |e| _ = sys.ShowWindow(e, show_terminal); + if (self.chk_desktop_notifications) |e| _ = sys.ShowWindow(e, show_privacy); + if (self.chk_app_notify_clipboard) |e| _ = sys.ShowWindow(e, show_privacy); + if (self.chk_app_notify_config) |e| _ = sys.ShowWindow(e, show_privacy); + if (self.combo_clipboard_read) |e| _ = sys.ShowWindow(e, show_privacy); + if (self.combo_clipboard_write) |e| _ = sys.ShowWindow(e, show_privacy); + if (self.combo_link_url) |e| _ = sys.ShowWindow(e, show_privacy); + if (self.combo_link_previews) |e| _ = sys.ShowWindow(e, show_privacy); + if (self.edit_font_family) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.edit_font_size) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.edit_theme) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.edit_bg_opacity) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.combo_window_theme) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.combo_cursor_style) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.edit_pad_x) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.edit_pad_y) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.chk_bg_blur) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.combo_pad_balance) |e| _ = sys.ShowWindow(e, show_appearance); + if (self.edit_command) |e| _ = sys.ShowWindow(e, show_shell); + if (self.combo_shell_integ) |e| _ = sys.ShowWindow(e, show_shell); + if (self.combo_auto_update) |e| _ = sys.ShowWindow(e, show_updates); + if (self.combo_auto_update_channel) |e| _ = sys.ShowWindow(e, show_updates); for (settings_label_specs, self.field_labels) |spec, label| { - if (label) |hwnd| _ = ShowWindow(hwnd, if (spec.section == self.active_section) SW_SHOWNORMAL else SW_HIDE); + if (label) |hwnd| _ = sys.ShowWindow(hwnd, if (spec.section == self.active_section) SW_SHOWNORMAL else SW_HIDE); } } @@ -2233,7 +2147,7 @@ pub const SettingsWindow = struct { self.markRawScalarEdit(.scrollback_limit); var buf_w: [32]u16 = undefined; - const n = GetWindowTextW(edit, &buf_w, @intCast(buf_w.len)); + const n = sys.GetWindowTextW(edit, &buf_w, @intCast(buf_w.len)); if (n <= 0) return self.setRawScalarValidationError(.scrollback_limit, edit, "Scrollback limit is required."); var utf8_buf: [64]u8 = undefined; const utf8 = std.unicode.utf16LeToUtf8(&utf8_buf, buf_w[0..@intCast(n)]) catch @@ -2256,7 +2170,7 @@ pub const SettingsWindow = struct { var buf_w: [32]u16 = undefined; const w = utf8ToW(&buf_w, text); self.suppress_edit_events = true; - _ = SendMessageW(edit, WM_SETTEXT, 0, @bitCast(@intFromPtr(w))); + _ = sys.SendMessageW(edit, WM_SETTEXT, 0, @bitCast(@intFromPtr(w))); self.suppress_edit_events = false; } @@ -2292,7 +2206,7 @@ pub const SettingsWindow = struct { const edit = self.edit_font_size orelse return; self.markRawScalarEdit(.font_size); var buf_w: [32]u16 = undefined; - const n = GetWindowTextW(edit, &buf_w, @intCast(buf_w.len)); + const n = sys.GetWindowTextW(edit, &buf_w, @intCast(buf_w.len)); if (n <= 0) return self.setRawScalarValidationError(.font_size, edit, "Font size is required."); var utf8_buf: [64]u8 = undefined; const utf8 = std.unicode.utf16LeToUtf8(&utf8_buf, buf_w[0..@intCast(n)]) catch @@ -2318,7 +2232,7 @@ pub const SettingsWindow = struct { var buf_w: [32]u16 = undefined; const w = utf8ToW(&buf_w, text); self.suppress_edit_events = true; - _ = SendMessageW(edit, WM_SETTEXT, 0, @bitCast(@intFromPtr(w))); + _ = sys.SendMessageW(edit, WM_SETTEXT, 0, @bitCast(@intFromPtr(w))); self.suppress_edit_events = false; } @@ -2365,7 +2279,7 @@ pub const SettingsWindow = struct { const edit = self.edit_bg_opacity orelse return; self.markRawScalarEdit(.background_opacity); var buf_w: [32]u16 = undefined; - const n = GetWindowTextW(edit, &buf_w, @intCast(buf_w.len)); + const n = sys.GetWindowTextW(edit, &buf_w, @intCast(buf_w.len)); if (n <= 0) return self.setRawScalarValidationError(.background_opacity, edit, "Background opacity is required."); var utf8_buf: [64]u8 = undefined; const utf8 = std.unicode.utf16LeToUtf8(&utf8_buf, buf_w[0..@intCast(n)]) catch @@ -2389,7 +2303,7 @@ pub const SettingsWindow = struct { var buf_w: [32]u16 = undefined; const w = utf8ToW(&buf_w, text); self.suppress_edit_events = true; - _ = SendMessageW(edit, WM_SETTEXT, 0, @bitCast(@intFromPtr(w))); + _ = sys.SendMessageW(edit, WM_SETTEXT, 0, @bitCast(@intFromPtr(w))); self.suppress_edit_events = false; } @@ -2487,7 +2401,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const chk = self.chk_trim_trail orelse return; - const state = SendMessageW(chk, BM_GETCHECK, 0, 0); + const state = sys.SendMessageW(chk, BM_GETCHECK, 0, 0); p.*.@"clipboard-trim-trailing-spaces" = (state == BST_CHECKED); self.trackEdit(.trim_trailing_spaces, false); } @@ -2496,7 +2410,7 @@ pub const SettingsWindow = struct { const chk = self.chk_trim_trail orelse return; const p = self.pending orelse return; self.suppress_edit_events = true; - _ = SendMessageW( + _ = sys.SendMessageW( chk, BM_SETCHECK, if (p.@"clipboard-trim-trailing-spaces") BST_CHECKED else BST_UNCHECKED, @@ -2509,7 +2423,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const chk = self.chk_desktop_notifications orelse return; - p.*.@"desktop-notifications" = SendMessageW(chk, BM_GETCHECK, 0, 0) == BST_CHECKED; + p.*.@"desktop-notifications" = sys.SendMessageW(chk, BM_GETCHECK, 0, 0) == BST_CHECKED; self.trackEdit(.desktop_notifications, false); } @@ -2517,7 +2431,7 @@ pub const SettingsWindow = struct { const chk = self.chk_desktop_notifications orelse return; const p = self.pending orelse return; self.suppress_edit_events = true; - _ = SendMessageW( + _ = sys.SendMessageW( chk, BM_SETCHECK, if (p.@"desktop-notifications") BST_CHECKED else BST_UNCHECKED, @@ -2533,7 +2447,7 @@ pub const SettingsWindow = struct { .clipboard => self.chk_app_notify_clipboard, .config => self.chk_app_notify_config, } orelse return; - const enabled = SendMessageW(chk, BM_GETCHECK, 0, 0) == BST_CHECKED; + const enabled = sys.SendMessageW(chk, BM_GETCHECK, 0, 0) == BST_CHECKED; switch (field) { .clipboard => { p.*.@"app-notifications".@"clipboard-copy" = enabled; @@ -2557,7 +2471,7 @@ pub const SettingsWindow = struct { .config => p.@"app-notifications".@"config-reload", }; self.suppress_edit_events = true; - _ = SendMessageW(chk, BM_SETCHECK, if (enabled) BST_CHECKED else BST_UNCHECKED, 0); + _ = sys.SendMessageW(chk, BM_SETCHECK, if (enabled) BST_CHECKED else BST_UNCHECKED, 0); self.suppress_edit_events = false; } @@ -2568,7 +2482,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_confirm_close orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"confirm-close-surface" = switch (idx) { 0 => .false, @@ -2588,7 +2502,7 @@ pub const SettingsWindow = struct { .always => 2, }; self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2596,7 +2510,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_copy_on_select orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"copy-on-select" = switch (idx) { 0 => .false, @@ -2616,7 +2530,7 @@ pub const SettingsWindow = struct { .clipboard => 2, }; self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2624,7 +2538,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = combo_opt orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; @field(p.*, field_name) = clipboardAccessFromComboIndex(idx) orelse return; if (comptime std.mem.eql(u8, field_name, "clipboard-read")) { @@ -2639,7 +2553,7 @@ pub const SettingsWindow = struct { const p = self.pending orelse return; const idx = comboIndexFromClipboardAccess(@field(p, field_name)); self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2647,7 +2561,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_link_url orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"link-url" = linkUrlFromComboIndex(idx) orelse return; self.trackEdit(.link_url, false); @@ -2658,7 +2572,7 @@ pub const SettingsWindow = struct { const p = self.pending orelse return; const idx = comboIndexFromLinkUrl(p.@"link-url"); self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2666,7 +2580,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_link_previews orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"link-previews" = linkPreviewsFromComboIndex(idx) orelse return; self.trackEdit(.link_previews, false); @@ -2677,7 +2591,7 @@ pub const SettingsWindow = struct { const p = self.pending orelse return; const idx = comboIndexFromLinkPreviews(p.@"link-previews"); self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2685,7 +2599,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_window_theme orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"window-theme" = switch (idx) { 0 => .auto, @@ -2709,7 +2623,7 @@ pub const SettingsWindow = struct { .ghostty => 4, }; self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2717,7 +2631,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_shell_integ orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"shell-integration" = switch (idx) { 0 => .none, @@ -2745,7 +2659,7 @@ pub const SettingsWindow = struct { .zsh => 6, }; self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2753,7 +2667,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_cursor_style orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"cursor-style" = switch (idx) { 0 => .bar, @@ -2775,7 +2689,7 @@ pub const SettingsWindow = struct { .block_hollow => 3, }; self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2786,7 +2700,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const chk = self.chk_bg_blur orelse return; - const state = SendMessageW(chk, BM_GETCHECK, 0, 0); + const state = sys.SendMessageW(chk, BM_GETCHECK, 0, 0); p.*.@"background-blur" = backgroundBlurFromCheckbox( p.*.@"background-blur", state == BST_CHECKED, @@ -2799,7 +2713,7 @@ pub const SettingsWindow = struct { const p = self.pending orelse return; const enabled = p.@"background-blur".win32SystemBackdropEnabled(); self.suppress_edit_events = true; - _ = SendMessageW( + _ = sys.SendMessageW( chk, BM_SETCHECK, if (enabled) BST_CHECKED else BST_UNCHECKED, @@ -2812,7 +2726,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_pad_balance orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"window-padding-balance" = switch (idx) { 0 => .false, @@ -2832,7 +2746,7 @@ pub const SettingsWindow = struct { .equal => 2, }; self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2840,7 +2754,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_auto_update orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"auto-update" = switch (idx) { 0 => null, @@ -2861,7 +2775,7 @@ pub const SettingsWindow = struct { .download => 3, } else 0; self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2869,7 +2783,7 @@ pub const SettingsWindow = struct { if (self.suppress_edit_events) return; const p = &(self.pending orelse return); const combo = self.combo_auto_update_channel orelse return; - const idx = SendMessageW(combo, CB_GETCURSEL, 0, 0); + const idx = sys.SendMessageW(combo, CB_GETCURSEL, 0, 0); if (idx < 0) return; p.*.@"auto-update-channel" = switch (idx) { 0 => null, @@ -2888,7 +2802,7 @@ pub const SettingsWindow = struct { .tip => 2, } else 0; self.suppress_edit_events = true; - _ = SendMessageW(combo, CB_SETCURSEL, idx, 0); + _ = sys.SendMessageW(combo, CB_SETCURSEL, idx, 0); self.suppress_edit_events = false; } @@ -2933,7 +2847,7 @@ pub const SettingsWindow = struct { self.syncPaddingFromEdit(.x); self.syncPaddingFromEdit(.y); if (self.validation_control) |control| { - _ = SetFocus(control); + _ = sys.SetFocus(control); self.ensureControlVisible(control); return; } @@ -3006,10 +2920,10 @@ pub const SettingsWindow = struct { fn updateClosePromptActions(self: *SettingsWindow) void { const actions = closePromptActions(self.close_prompt_visible, self.save_in_flight, self.close_posted); - if (self.btn_close_save) |button| _ = EnableWindow(button, @intFromBool(actions.save)); - if (self.btn_close_discard) |button| _ = EnableWindow(button, @intFromBool(actions.discard)); + if (self.btn_close_save) |button| _ = sys.EnableWindow(button, @intFromBool(actions.save)); + if (self.btn_close_discard) |button| _ = sys.EnableWindow(button, @intFromBool(actions.discard)); if (self.btn_close_keep_editing) |button| { - _ = EnableWindow(button, @intFromBool(actions.keep_editing)); + _ = sys.EnableWindow(button, @intFromBool(actions.keep_editing)); } } @@ -3017,13 +2931,13 @@ pub const SettingsWindow = struct { const text = self.text_close_prompt orelse return; setWindowTextUtf8(text, closePromptText(self.save_in_flight)); if (announce) { - NotifyWinEvent(EVENT_OBJECT_NAMECHANGE, text, @bitCast(OBJID_CLIENT), @bitCast(CHILDID_SELF)); + sys.NotifyWinEvent(EVENT_OBJECT_NAMECHANGE, text, @bitCast(OBJID_CLIENT), @bitCast(CHILDID_SELF)); } } fn showClosePrompt(self: *SettingsWindow) void { if (self.close_prompt_visible) { - if (self.btn_close_keep_editing) |button| _ = SetFocus(button); + if (self.btn_close_keep_editing) |button| _ = sys.SetFocus(button); return; } self.close_prompt_visible = true; @@ -3031,21 +2945,21 @@ pub const SettingsWindow = struct { // carries close intent. The prompt remains visible so the user can // keep editing, but successful completion must honor the request. self.close_after_save = self.save_in_flight; - self.close_prior_focus = GetFocus(); - if (self.text_status) |text| _ = ShowWindow(text, SW_HIDE); - if (self.btn_conflict_keep) |button| _ = ShowWindow(button, SW_HIDE); - if (self.btn_conflict_use_disk) |button| _ = ShowWindow(button, SW_HIDE); - if (self.btn_save) |button| _ = ShowWindow(button, SW_HIDE); - if (self.text_close_prompt) |text| _ = ShowWindow(text, SW_SHOWNORMAL); - if (self.btn_close_save) |button| _ = ShowWindow(button, SW_SHOWNORMAL); - if (self.btn_close_discard) |button| _ = ShowWindow(button, SW_SHOWNORMAL); - if (self.btn_close_keep_editing) |button| _ = ShowWindow(button, SW_SHOWNORMAL); + self.close_prior_focus = sys.GetFocus(); + if (self.text_status) |text| _ = sys.ShowWindow(text, SW_HIDE); + if (self.btn_conflict_keep) |button| _ = sys.ShowWindow(button, SW_HIDE); + if (self.btn_conflict_use_disk) |button| _ = sys.ShowWindow(button, SW_HIDE); + if (self.btn_save) |button| _ = sys.ShowWindow(button, SW_HIDE); + if (self.text_close_prompt) |text| _ = sys.ShowWindow(text, SW_SHOWNORMAL); + if (self.btn_close_save) |button| _ = sys.ShowWindow(button, SW_SHOWNORMAL); + if (self.btn_close_discard) |button| _ = sys.ShowWindow(button, SW_SHOWNORMAL); + if (self.btn_close_keep_editing) |button| _ = sys.ShowWindow(button, SW_SHOWNORMAL); self.syncClosePromptText(true); self.updateClosePromptActions(); layoutChildren(self); // Conservative default: closing must never be the result of an // accidental Enter after the window-close gesture. - if (self.btn_close_keep_editing) |button| _ = SetFocus(button); + if (self.btn_close_keep_editing) |button| _ = sys.SetFocus(button); } fn cancelClosePrompt(self: *SettingsWindow) void { @@ -3054,27 +2968,27 @@ pub const SettingsWindow = struct { self.close_prompt_visible = false; self.close_after_save = false; self.close_prior_focus = null; - if (self.text_close_prompt) |text| _ = ShowWindow(text, SW_HIDE); - if (self.btn_close_save) |button| _ = ShowWindow(button, SW_HIDE); - if (self.btn_close_discard) |button| _ = ShowWindow(button, SW_HIDE); - if (self.btn_close_keep_editing) |button| _ = ShowWindow(button, SW_HIDE); - if (self.text_status) |text| _ = ShowWindow(text, SW_SHOWNORMAL); - if (self.btn_save) |button| _ = ShowWindow(button, SW_SHOWNORMAL); + if (self.text_close_prompt) |text| _ = sys.ShowWindow(text, SW_HIDE); + if (self.btn_close_save) |button| _ = sys.ShowWindow(button, SW_HIDE); + if (self.btn_close_discard) |button| _ = sys.ShowWindow(button, SW_HIDE); + if (self.btn_close_keep_editing) |button| _ = sys.ShowWindow(button, SW_HIDE); + if (self.text_status) |text| _ = sys.ShowWindow(text, SW_SHOWNORMAL); + if (self.btn_save) |button| _ = sys.ShowWindow(button, SW_SHOWNORMAL); self.syncConflictControls(); self.updateSaveEnabled(); layoutChildren(self); if (prior_focus) |control| { const belongs_to_settings = if (self.hwnd) |settings_hwnd| - control == settings_hwnd or IsChild(settings_hwnd, control) != 0 + control == settings_hwnd or sys.IsChild(settings_hwnd, control) != 0 else false; - if (belongs_to_settings and IsWindow(control) != 0 and IsWindowVisible(control) != 0 and IsWindowEnabled(control) != 0) { - _ = SetFocus(control); + if (belongs_to_settings and sys.IsWindow(control) != 0 and sys.IsWindowVisible(control) != 0 and sys.IsWindowEnabled(control) != 0) { + _ = sys.SetFocus(control); return; } } - if (self.sectionButton(self.active_section)) |button| _ = SetFocus(button); + if (self.sectionButton(self.active_section)) |button| _ = sys.SetFocus(button); } fn saveAndClose(self: *SettingsWindow) void { @@ -3108,7 +3022,7 @@ pub const SettingsWindow = struct { const hwnd = self.hwnd orelse return; self.close_posted = true; self.updateClosePromptActions(); - if (PostMessageW(hwnd, WM_SETTINGS_CLOSE_NOW, 0, 0) == 0) { + if (sys.PostMessageW(hwnd, WM_SETTINGS_CLOSE_NOW, 0, 0) == 0) { self.close_posted = false; self.updateClosePromptActions(); self.setStatus("Could not close the settings window; your edits are preserved."); @@ -3119,11 +3033,11 @@ pub const SettingsWindow = struct { fn closeNow(self: *SettingsWindow, hwnd: HWND) void { if (self.close_notified) return; self.close_notified = true; - _ = ShowWindow(hwnd, SW_HIDE); - if (DestroyWindow(hwnd) == 0) { + _ = sys.ShowWindow(hwnd, SW_HIDE); + if (sys.DestroyWindow(hwnd) == 0) { self.close_notified = false; self.close_posted = false; - _ = ShowWindow(hwnd, SW_SHOWNORMAL); + _ = sys.ShowWindow(hwnd, SW_SHOWNORMAL); self.updateClosePromptActions(); self.setStatus("Could not close the settings window; your edits are preserved."); self.cancelClosePrompt(); @@ -3260,7 +3174,7 @@ pub const SettingsWindow = struct { .auto_update_channel => .{ .section = .updates, .hwnd = self.combo_auto_update_channel }, }; self.setActiveSection(destination.section); - if (destination.hwnd) |control| _ = SetFocus(control); + if (destination.hwnd) |control| _ = sys.SetFocus(control); } /// Bring the settings window up. Idempotent: a subsequent open @@ -3268,7 +3182,7 @@ pub const SettingsWindow = struct { /// instead of duplicating it. pub fn open(self: *SettingsWindow) !void { if (self.hwnd) |h| { - if (IsWindow(h) != 0) { + if (sys.IsWindow(h) != 0) { switch (pendingCloseReopenAction(self.close_posted, self.close_after_save)) { .none => {}, .cancel_saved_close => { @@ -3285,8 +3199,8 @@ pub const SettingsWindow = struct { }, } self.cancelClosePrompt(); - if (IsIconic(h) != 0) _ = ShowWindow(h, SW_RESTORE) else _ = ShowWindow(h, SW_SHOWNORMAL); - _ = SetForegroundWindow(h); + if (sys.IsIconic(h) != 0) _ = sys.ShowWindow(h, SW_RESTORE) else _ = sys.ShowWindow(h, SW_SHOWNORMAL); + _ = sys.SetForegroundWindow(h); return; } self.hwnd = null; @@ -3307,13 +3221,13 @@ pub const SettingsWindow = struct { .cbWndExtra = 0, .hInstance = self.handle.hinstance, .hIcon = null, - .hCursor = LoadCursorW(null, @ptrFromInt(IDC_ARROW)), + .hCursor = sys.LoadCursorW(null, @ptrFromInt(IDC_ARROW)), .hbrBackground = null, .lpszMenuName = null, .lpszClassName = class_name, .hIconSm = null, }; - self.class_atom = RegisterClassExW(&wc); + self.class_atom = sys.RegisterClassExW(&wc); if (self.class_atom == 0) { return windows.unexpectedError(windows.kernel32.GetLastError()); } @@ -3321,7 +3235,7 @@ pub const SettingsWindow = struct { const title = std.unicode.utf8ToUtf16LeStringLiteral("noctty settings"); const owner_hwnd = if (self.handle.ownerWindow) |get_owner| get_owner(self.handle.ctx) else null; - const hwnd = CreateWindowExW( + const hwnd = sys.CreateWindowExW( WS_EX_APPWINDOW, class_name, title, @@ -3340,32 +3254,32 @@ pub const SettingsWindow = struct { self, ) orelse return windows.unexpectedError(windows.kernel32.GetLastError()); self.hwnd = hwnd; - self.dpi = normalizedDpi(GetDpiForWindow(hwnd)); + self.dpi = normalizedDpi(sys.GetDpiForWindow(hwnd)); // PerMonitorV2 coordinates are physical pixels. Start at the intended // logical size, capped to the current work area, so a high-DPI monitor // does not receive a tiny and immediately overlapping first frame. var work_area: RECT = undefined; var have_work_area = false; - if (owner_hwnd) |owner| if (MonitorFromWindow(owner, MONITOR_DEFAULTTONEAREST)) |monitor| { + if (owner_hwnd) |owner| if (sys.MonitorFromWindow(owner, MONITOR_DEFAULTTONEAREST)) |monitor| { var info: MONITORINFO = .{ .cbSize = @sizeOf(MONITORINFO), .rcMonitor = undefined, .rcWork = undefined, .dwFlags = 0, }; - if (GetMonitorInfoW(monitor, &info) != 0) { + if (sys.GetMonitorInfoW(monitor, &info) != 0) { work_area = info.rcWork; have_work_area = true; } }; - if (!have_work_area) have_work_area = SystemParametersInfoW(SPI_GETWORKAREA, 0, &work_area, 0) != 0; + if (!have_work_area) have_work_area = sys.SystemParametersInfoW(SPI_GETWORKAREA, 0, &work_area, 0) != 0; if (have_work_area) { const work_width = work_area.right - work_area.left; const work_height = work_area.bottom - work_area.top; const width = @min(self.px(960), @divTrunc(work_width * 9, 10)); const height = @min(self.px(840), @divTrunc(work_height * 9, 10)); - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, null, work_area.left + @divTrunc(work_width - width, 2), @@ -3381,7 +3295,7 @@ pub const SettingsWindow = struct { self.text_summary = makeStatic(hwnd, self.handle.hinstance, Section.appearance.placeholderText()); self.text_status = makeStatic(hwnd, self.handle.hinstance, ""); self.text_close_prompt = makeStatic(hwnd, self.handle.hinstance, "Save changes before closing?"); - if (self.text_close_prompt) |text| _ = ShowWindow(text, SW_HIDE); + if (self.text_close_prompt) |text| _ = sys.ShowWindow(text, SW_HIDE); self.text_help = makeStatic(hwnd, self.handle.hinstance, ""); for (settings_label_specs, 0..) |spec, i| { self.field_labels[i] = makeStatic(hwnd, self.handle.hinstance, spec.text); @@ -3405,7 +3319,7 @@ pub const SettingsWindow = struct { // in the Advanced section; hidden when another section is // active. const btn_label = std.unicode.utf8ToUtf16LeStringLiteral("Open in default editor"); - self.btn_open_editor = CreateWindowExW( + self.btn_open_editor = sys.CreateWindowExW( 0, btn_class, btn_label, @@ -3423,7 +3337,7 @@ pub const SettingsWindow = struct { // and fires a hard reload. Save errors are logged and the draft // remains in memory for retry. const btn_save_label = std.unicode.utf8ToUtf16LeStringLiteral("Save"); - self.btn_save = CreateWindowExW( + self.btn_save = sys.CreateWindowExW( 0, btn_class, btn_save_label, @@ -3437,7 +3351,7 @@ pub const SettingsWindow = struct { self.handle.hinstance, null, ); - self.btn_conflict_keep = CreateWindowExW( + self.btn_conflict_keep = sys.CreateWindowExW( 0, btn_class, std.unicode.utf8ToUtf16LeStringLiteral("Keep mine"), @@ -3451,7 +3365,7 @@ pub const SettingsWindow = struct { self.handle.hinstance, null, ); - self.btn_conflict_use_disk = CreateWindowExW( + self.btn_conflict_use_disk = sys.CreateWindowExW( 0, btn_class, std.unicode.utf8ToUtf16LeStringLiteral("Use disk"), @@ -3465,7 +3379,7 @@ pub const SettingsWindow = struct { self.handle.hinstance, null, ); - self.btn_close_save = CreateWindowExW( + self.btn_close_save = sys.CreateWindowExW( 0, btn_class, std.unicode.utf8ToUtf16LeStringLiteral("Save and close"), @@ -3479,7 +3393,7 @@ pub const SettingsWindow = struct { self.handle.hinstance, null, ); - self.btn_close_discard = CreateWindowExW( + self.btn_close_discard = sys.CreateWindowExW( 0, btn_class, std.unicode.utf8ToUtf16LeStringLiteral("Discard changes"), @@ -3493,7 +3407,7 @@ pub const SettingsWindow = struct { self.handle.hinstance, null, ); - self.btn_close_keep_editing = CreateWindowExW( + self.btn_close_keep_editing = sys.CreateWindowExW( 0, btn_class, std.unicode.utf8ToUtf16LeStringLiteral("Keep editing"), @@ -3509,7 +3423,7 @@ pub const SettingsWindow = struct { ); const btn_keybind_label = std.unicode.utf8ToUtf16LeStringLiteral("Open config for keybinds"); - self.btn_keybindings_editor = CreateWindowExW( + self.btn_keybindings_editor = sys.CreateWindowExW( 0, btn_class, btn_keybind_label, @@ -3700,20 +3614,12 @@ pub const SettingsWindow = struct { layoutChildren(self); self.applySectionVisibility(); - _ = ShowWindow(hwnd, SW_SHOWNORMAL); - _ = SetForegroundWindow(hwnd); - if (self.sectionButton(self.active_section)) |button| _ = SetFocus(button); + _ = sys.ShowWindow(hwnd, SW_SHOWNORMAL); + _ = sys.SetForegroundWindow(hwnd); + if (self.sectionButton(self.active_section)) |button| _ = sys.SetFocus(button); } }; -extern "ole32" fn CoCreateInstance( - class_id: *const GUID, - outer: ?*anyopaque, - context: u32, - interface_id: *const GUID, - result: *?*anyopaque, -) callconv(.winapi) HRESULT; - const IAccPropServicesVtbl = extern struct { QueryInterface: *const fn (*anyopaque, *const GUID, *?*anyopaque) callconv(.winapi) HRESULT, AddRef: *const fn (*anyopaque) callconv(.winapi) u32, @@ -3757,7 +3663,7 @@ const IAccPropServices = extern struct { fn createAccPropServices() ?*IAccPropServices { var raw: ?*anyopaque = null; - const hr = CoCreateInstance( + const hr = sys.CoCreateInstance( &CLSID_ACC_PROP_SERVICES, null, CLSCTX_INPROC_SERVER, @@ -3813,17 +3719,17 @@ fn annotateAccessibleControls(self: *SettingsWindow) void { fn populateCombo(combo_opt: ?HWND, items: []const []const u8) void { const combo = combo_opt orelse return; - _ = SendMessageW(combo, CB_RESETCONTENT, 0, 0); + _ = sys.SendMessageW(combo, CB_RESETCONTENT, 0, 0); for (items) |item| { var buf_w: [64]u16 = undefined; const w = utf8ToW(&buf_w, item); - _ = SendMessageW(combo, CB_ADDSTRING, 0, @bitCast(@intFromPtr(w))); + _ = sys.SendMessageW(combo, CB_ADDSTRING, 0, @bitCast(@intFromPtr(w))); } } fn makeStatic(parent: HWND, hinstance: HINSTANCE, text: []const u8) ?HWND { var text_w: [2048]u16 = undefined; - return CreateWindowExW( + return sys.CreateWindowExW( 0, std.unicode.utf8ToUtf16LeStringLiteral("STATIC"), utf8ToW(&text_w, text), @@ -3842,22 +3748,22 @@ fn makeStatic(parent: HWND, hinstance: HINSTANCE, text: []const u8) ?HWND { fn setWindowTextUtf8(hwnd_opt: ?HWND, text: []const u8) void { const hwnd = hwnd_opt orelse return; var text_w: [2048]u16 = undefined; - _ = SendMessageW(hwnd, WM_SETTEXT, 0, @bitCast(@intFromPtr(utf8ToW(&text_w, text)))); + _ = sys.SendMessageW(hwnd, WM_SETTEXT, 0, @bitCast(@intFromPtr(utf8ToW(&text_w, text)))); } fn setWindowTextUtf8IfChanged(hwnd: HWND, text: []const u8) bool { var current: [2048]u16 = undefined; - const current_len: usize = @intCast(@max(0, GetWindowTextW(hwnd, ¤t, @intCast(current.len)))); + const current_len: usize = @intCast(@max(0, sys.GetWindowTextW(hwnd, ¤t, @intCast(current.len)))); var desired: [2048]u16 = undefined; const desired_z = utf8ToW(&desired, text); const desired_slice = std.mem.span(desired_z); if (std.mem.eql(u16, current[0..current_len], desired_slice)) return false; - _ = SendMessageW(hwnd, WM_SETTEXT, 0, @bitCast(@intFromPtr(desired_z))); + _ = sys.SendMessageW(hwnd, WM_SETTEXT, 0, @bitCast(@intFromPtr(desired_z))); return true; } fn applyChildFont(hwnd: HWND, lParam: LPARAM) callconv(.winapi) BOOL { - _ = SendMessageW(hwnd, WM_SETFONT, @as(WPARAM, @bitCast(lParam)), 1); + _ = sys.SendMessageW(hwnd, WM_SETFONT, @as(WPARAM, @bitCast(lParam)), 1); return 1; } @@ -3869,7 +3775,7 @@ fn makeEdit( extra_style: u32, ) ?HWND { const edit_class = std.unicode.utf8ToUtf16LeStringLiteral("EDIT"); - const edit = CreateWindowExW( + const edit = sys.CreateWindowExW( 0, edit_class, std.unicode.utf8ToUtf16LeStringLiteral(""), @@ -3883,7 +3789,7 @@ fn makeEdit( hinstance, null, ); - if (edit) |e| _ = SendMessageW(e, EM_LIMITTEXT, edit_text_max_code_units - 1, 0); + if (edit) |e| _ = sys.SendMessageW(e, EM_LIMITTEXT, edit_text_max_code_units - 1, 0); return edit; } @@ -3895,7 +3801,7 @@ fn makeCheckbox( width: i32, ) ?HWND { const btn_class = std.unicode.utf8ToUtf16LeStringLiteral("BUTTON"); - return CreateWindowExW( + return sys.CreateWindowExW( 0, btn_class, label, @@ -3919,7 +3825,7 @@ fn makeCombo( items: []const []const u8, ) ?HWND { const combo_class = std.unicode.utf8ToUtf16LeStringLiteral("COMBOBOX"); - const combo = CreateWindowExW( + const combo = sys.CreateWindowExW( 0, combo_class, std.unicode.utf8ToUtf16LeStringLiteral(""), @@ -3953,7 +3859,7 @@ fn makeSectionButton( .advanced => BTN_SECTION_ADVANCED, }; const group_style: u32 = if (section == .appearance) WS_GROUP | WS_TABSTOP else 0; - return CreateWindowExW( + return sys.CreateWindowExW( 0, class, section.label(), @@ -4201,19 +4107,19 @@ test "settings save command rechecks dispatch state" { } fn windowWorkArea(hwnd: HWND, work_area: *RECT) bool { - if (MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)) |monitor| { + if (sys.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)) |monitor| { var info: MONITORINFO = .{ .cbSize = @sizeOf(MONITORINFO), .rcMonitor = undefined, .rcWork = undefined, .dwFlags = 0, }; - if (GetMonitorInfoW(monitor, &info) != 0) { + if (sys.GetMonitorInfoW(monitor, &info) != 0) { work_area.* = info.rcWork; return true; } } - return SystemParametersInfoW(SPI_GETWORKAREA, 0, work_area, 0) != 0; + return sys.SystemParametersInfoW(SPI_GETWORKAREA, 0, work_area, 0) != 0; } fn cappedMinimum(logical: i32, dpi: u32, work_extent: i32) i32 { @@ -4452,21 +4358,21 @@ test "settings custom UIA providers require confirmed STA initialization" { fn clipChildToViewport(parent: HWND, child: ?HWND, viewport_top: i32, viewport_bottom: i32) bool { const hwnd = child orelse return true; var rect: RECT = undefined; - if (GetWindowRect(hwnd, &rect) == 0) return false; + if (sys.GetWindowRect(hwnd, &rect) == 0) return false; var origin: POINT = .{ .x = rect.left, .y = rect.top }; - if (ScreenToClient(parent, &origin) == 0) return false; + if (sys.ScreenToClient(parent, &origin) == 0) return false; const width = @max(0, rect.right - rect.left); const height = @max(0, rect.bottom - rect.top); const clip_top = std.math.clamp(viewport_top - origin.y, 0, height); const clip_bottom = std.math.clamp(viewport_bottom - origin.y, clip_top, height); if (clip_top == 0 and clip_bottom == height) { - _ = SetWindowRgn(hwnd, null, 1); + _ = sys.SetWindowRgn(hwnd, null, 1); return false; } const fully_clipped = viewportRegionIsFullyClipped(width, clip_top, clip_bottom); - const region = CreateRectRgn(0, clip_top, width, clip_bottom) orelse return false; - if (SetWindowRgn(hwnd, region, 1) == 0) { - _ = DeleteObject(region); + const region = sys.CreateRectRgn(0, clip_top, width, clip_bottom) orelse return false; + if (sys.SetWindowRgn(hwnd, region, 1) == 0) { + _ = sys.DeleteObject(region); return false; } return fully_clipped; @@ -4490,11 +4396,11 @@ fn closePromptMeasuredHeight(self: *SettingsWindow, pane_width: i32) i32 { return self.close_prompt_measure_height; } const hwnd = self.hwnd orelse return minimum; - const hdc = GetDC(hwnd) orelse return minimum; - defer _ = ReleaseDC(hwnd, hdc); - const previous_font = if (self.ui_font) |font| SelectObject(hdc, font) else null; + const hdc = sys.GetDC(hwnd) orelse return minimum; + defer _ = sys.ReleaseDC(hwnd, hdc); + const previous_font = if (self.ui_font) |font| sys.SelectObject(hdc, font) else null; defer { - if (previous_font) |font| _ = SelectObject(hdc, font); + if (previous_font) |font| _ = sys.SelectObject(hdc, font); } var text_w: [256]u16 = undefined; @@ -4505,7 +4411,7 @@ fn closePromptMeasuredHeight(self: *SettingsWindow, pane_width: i32) i32 { .right = @max(1, pane_width), .bottom = 0, }; - _ = DrawTextW( + _ = sys.DrawTextW( hdc, text.ptr, @intCast(text.len), @@ -4561,7 +4467,7 @@ fn settingsContentViewportTop(self: *SettingsWindow, client_rect: RECT) i32 { fn layoutChildren(self: *SettingsWindow) void { const hwnd = self.hwnd orelse return; var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) return; + if (sys.GetClientRect(hwnd, &rect) == 0) return; // Left rail — stack section buttons top-down. const rail_width = self.px(left_rail_width); @@ -4582,7 +4488,7 @@ fn layoutChildren(self: *SettingsWindow) void { self.btn_section_advanced, }) |btn_opt| { if (btn_opt) |btn| { - _ = MoveWindow(btn, btn_x, y, btn_w, button_height, 1); + _ = sys.MoveWindow(btn, btn_x, y, btn_w, button_height, 1); } y += button_height + button_gap; } @@ -4606,22 +4512,22 @@ fn layoutChildren(self: *SettingsWindow) void { const content_bottom = pane_top + stack_top + (content_rows - 1) * row_gap + control_height; self.content_scroll_max = @max(0, content_bottom - (rect.bottom - side)); self.content_scroll_y = std.math.clamp(self.content_scroll_y, 0, self.content_scroll_max); - _ = SetScrollRange(hwnd, SB_VERT, 0, self.content_scroll_max, 1); - _ = SetScrollPos(hwnd, SB_VERT, self.content_scroll_y, 1); - _ = ShowScrollBar(hwnd, SB_VERT, @intFromBool(self.content_scroll_max > 0)); + _ = sys.SetScrollRange(hwnd, SB_VERT, 0, self.content_scroll_max, 1); + _ = sys.SetScrollPos(hwnd, SB_VERT, self.content_scroll_y, 1); + _ = sys.ShowScrollBar(hwnd, SB_VERT, @intFromBool(self.content_scroll_max > 0)); const content_top = pane_top + stack_top - self.content_scroll_y; const header_right = @max(pane_left, rect.right - side - self.px(110)); - if (self.text_header) |text| _ = MoveWindow(text, pane_left, pane_top, header_right - pane_left, self.px(28), 1); - if (self.text_summary) |text| _ = MoveWindow(text, pane_left, pane_top + self.px(34), header_right - pane_left, self.px(40), 1); - if (self.text_status) |text| _ = MoveWindow(text, pane_left, pane_top + self.px(76), pane_width, self.px(24), 1); - if (self.text_close_prompt) |text| _ = MoveWindow(text, pane_left, pane_top + self.px(76), pane_width, prompt_height, 1); - if (self.btn_conflict_keep) |button| _ = MoveWindow(button, pane_left, pane_top + self.px(104), self.px(110), self.px(28), 1); - if (self.btn_conflict_use_disk) |button| _ = MoveWindow(button, pane_left + self.px(120), pane_top + self.px(104), self.px(110), self.px(28), 1); + if (self.text_header) |text| _ = sys.MoveWindow(text, pane_left, pane_top, header_right - pane_left, self.px(28), 1); + if (self.text_summary) |text| _ = sys.MoveWindow(text, pane_left, pane_top + self.px(34), header_right - pane_left, self.px(40), 1); + if (self.text_status) |text| _ = sys.MoveWindow(text, pane_left, pane_top + self.px(76), pane_width, self.px(24), 1); + if (self.text_close_prompt) |text| _ = sys.MoveWindow(text, pane_left, pane_top + self.px(76), pane_width, prompt_height, 1); + if (self.btn_conflict_keep) |button| _ = sys.MoveWindow(button, pane_left, pane_top + self.px(104), self.px(110), self.px(28), 1); + if (self.btn_conflict_use_disk) |button| _ = sys.MoveWindow(button, pane_left + self.px(120), pane_top + self.px(104), self.px(110), self.px(28), 1); const close_action_top = pane_top + self.px(76) + prompt_height + self.px(8); - if (self.btn_close_save) |button| _ = MoveWindow(button, pane_left, close_action_top, close_actions.first_width, self.px(32), 1); - if (self.btn_close_discard) |button| _ = MoveWindow(button, pane_left + close_actions.second_x, close_action_top + close_actions.second_y, close_actions.second_width, self.px(32), 1); - if (self.btn_close_keep_editing) |button| _ = MoveWindow(button, pane_left + close_actions.third_x, close_action_top + close_actions.third_y, close_actions.third_width, self.px(32), 1); + if (self.btn_close_save) |button| _ = sys.MoveWindow(button, pane_left, close_action_top, close_actions.first_width, self.px(32), 1); + if (self.btn_close_discard) |button| _ = sys.MoveWindow(button, pane_left + close_actions.second_x, close_action_top + close_actions.second_y, close_actions.second_width, self.px(32), 1); + if (self.btn_close_keep_editing) |button| _ = sys.MoveWindow(button, pane_left + close_actions.third_x, close_action_top + close_actions.third_y, close_actions.third_width, self.px(32), 1); var active_label_index: usize = 0; for (settings_label_specs, self.field_labels) |spec, label_opt| { @@ -4632,7 +4538,7 @@ fn layoutChildren(self: *SettingsWindow) void { const column_width = @divTrunc(pane_width - (if (columns == 2) column_gap else 0), @as(i32, @intCast(columns))); const column: i32 = @intCast(active_label_index % columns); const row: i32 = @intCast(active_label_index / columns); - _ = MoveWindow( + _ = sys.MoveWindow( label, pane_left + column * (column_width + column_gap), content_top + row * row_gap - self.px(field_label_offset), @@ -4643,7 +4549,7 @@ fn layoutChildren(self: *SettingsWindow) void { active_label_index += 1; } if (self.text_help) |text| { - _ = MoveWindow(text, pane_left, content_top + row_gap, pane_width, @max(self.px(240), rect.bottom - content_top - row_gap - side), 1); + _ = sys.MoveWindow(text, pane_left, content_top + row_gap, pane_width, @max(self.px(240), rect.bottom - content_top - row_gap - side), 1); } // Terminal section stack. All rows share this section and are @@ -4653,19 +4559,19 @@ fn layoutChildren(self: *SettingsWindow) void { { var ty: i32 = content_top; if (self.edit_scrollback) |e| { - _ = MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), control_height, 1); + _ = sys.MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), control_height, 1); ty += row_gap; } if (self.combo_confirm_close) |e| { - _ = MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(160), 1); + _ = sys.MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(160), 1); ty += row_gap; } if (self.combo_copy_on_select) |e| { - _ = MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(160), 1); + _ = sys.MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(160), 1); ty += row_gap; } if (self.chk_trim_trail) |e| { - _ = MoveWindow(e, pane_left, ty, @min(self.px(260), pane_width), checkbox_height, 1); + _ = sys.MoveWindow(e, pane_left, ty, @min(self.px(260), pane_width), checkbox_height, 1); } } @@ -4687,7 +4593,7 @@ fn layoutChildren(self: *SettingsWindow) void { const control_hwnd = control.hwnd orelse continue; const column: i32 = @intCast(i % columns); const row: i32 = @intCast(i / columns); - _ = MoveWindow( + _ = sys.MoveWindow( control_hwnd, pane_left + column * (column_width + column_gap), content_top + row * row_gap, @@ -4719,7 +4625,7 @@ fn layoutChildren(self: *SettingsWindow) void { const control_hwnd = control.hwnd orelse continue; const column: i32 = @intCast(i % columns); const row: i32 = @intCast(i / columns); - _ = MoveWindow( + _ = sys.MoveWindow( control_hwnd, pane_left + column * (column_width + column_gap), content_top + row * row_gap, @@ -4734,32 +4640,32 @@ fn layoutChildren(self: *SettingsWindow) void { { var ty: i32 = content_top; if (self.edit_command) |e| { - _ = MoveWindow(e, pane_left, ty, @min(self.px(360), pane_width), control_height, 1); + _ = sys.MoveWindow(e, pane_left, ty, @min(self.px(360), pane_width), control_height, 1); ty += row_gap; } if (self.combo_shell_integ) |e| { - _ = MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(200), 1); + _ = sys.MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(200), 1); } } if (self.btn_keybindings_editor) |btn| { - _ = MoveWindow(btn, pane_left, content_top, @min(self.px(220), pane_width), self.px(32), 1); + _ = sys.MoveWindow(btn, pane_left, content_top, @min(self.px(220), pane_width), self.px(32), 1); } // Updates section. { var ty: i32 = content_top; if (self.combo_auto_update) |e| { - _ = MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(160), 1); + _ = sys.MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(160), 1); ty += row_gap; } if (self.combo_auto_update_channel) |e| { - _ = MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(140), 1); + _ = sys.MoveWindow(e, pane_left, ty, @min(self.px(200), pane_width), self.px(140), 1); } } if (self.btn_open_editor) |btn| { - _ = MoveWindow(btn, pane_left, content_top, @min(self.px(220), pane_width), self.px(32), 1); + _ = sys.MoveWindow(btn, pane_left, content_top, @min(self.px(220), pane_width), self.px(32), 1); } // Save button — always-visible primary action in the header. Keeping it @@ -4767,7 +4673,7 @@ fn layoutChildren(self: *SettingsWindow) void { if (self.btn_save) |btn| { const w = self.px(90); const h = self.px(32); - _ = MoveWindow( + _ = sys.MoveWindow( btn, rect.right - w - side, pane_top, @@ -4792,17 +4698,8 @@ fn layoutChildren(self: *SettingsWindow) void { } } -extern "user32" fn MoveWindow( - hWnd: HWND, - X: i32, - Y: i32, - nWidth: i32, - nHeight: i32, - bRepaint: BOOL, -) callconv(.winapi) BOOL; - fn settingsTextProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { - const owner = if (GetParent(hwnd)) |parent| recoverOwner(parent) else null; + const owner = if (sys.GetParent(hwnd)) |parent| recoverOwner(parent) else null; if (owner) |settings| { const previous = settings.text_uia_prev_proc; if (settings.textIndex(hwnd)) |index| { @@ -4814,18 +4711,18 @@ fn settingsTextProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callc if (msg == WM_NCDESTROY) { if (settings.text_uia_providers[index]) |provider| provider.detach(); if (previous) |proc| { - _ = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(proc)))); - return CallWindowProcW(proc, hwnd, msg, wParam, lParam); + _ = sys.SetWindowLongPtrW(hwnd, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(proc)))); + return sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); } } } - if (previous) |proc| return CallWindowProcW(proc, hwnd, msg, wParam, lParam); + if (previous) |proc| return sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); } fn settingsControlProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { - const owner = if (GetParent(hwnd)) |parent| recoverOwner(parent) else null; + const owner = if (sys.GetParent(hwnd)) |parent| recoverOwner(parent) else null; if (owner) |settings| { if (settings.controlIndex(hwnd)) |index| { const previous = settings.control_uia_prev_procs[index]; @@ -4837,12 +4734,12 @@ fn settingsControlProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) ca if (msg == WM_NCDESTROY) { if (settings.control_uia_providers[index]) |provider| provider.detach(); if (previous) |proc| { - _ = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(proc)))); - return CallWindowProcW(proc, hwnd, msg, wParam, lParam); + _ = sys.SetWindowLongPtrW(hwnd, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(proc)))); + return sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); } } if (previous) |proc| { - const result = CallWindowProcW(proc, hwnd, msg, wParam, lParam); + const result = sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); if (msg == WM_SETFOCUS) { if (settings.control_uia_providers[index]) |provider| provider.raiseFocusChanged(); } @@ -4850,11 +4747,11 @@ fn settingsControlProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) ca } } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); } fn settingsSectionButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { - const owner = if (GetParent(hwnd)) |parent| recoverOwner(parent) else null; + const owner = if (sys.GetParent(hwnd)) |parent| recoverOwner(parent) else null; if (owner) |settings| { const previous = settings.section_button_prev_proc; if (settings.sectionForButton(hwnd)) |section| { @@ -4866,13 +4763,13 @@ fn settingsSectionButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPAR if (msg == WM_NCDESTROY) { if (settings.sectionProvider(section)) |provider| provider.detach(); if (previous) |proc| { - _ = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(proc)))); - return CallWindowProcW(proc, hwnd, msg, wParam, lParam); + _ = sys.SetWindowLongPtrW(hwnd, GWLP_WNDPROC, @as(LONG_PTR, @intCast(@intFromPtr(proc)))); + return sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); } } } if (previous) |proc| { - const result = CallWindowProcW(proc, hwnd, msg, wParam, lParam); + const result = sys.CallWindowProcW(proc, hwnd, msg, wParam, lParam); if (msg == WM_SETFOCUS) { if (settings.sectionForButton(hwnd)) |section| { if (settings.sectionProvider(section)) |provider| provider.raiseFocusChanged(); @@ -4881,14 +4778,14 @@ fn settingsSectionButtonProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPAR return result; } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); } fn wndProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.winapi) LRESULT { if (msg == WM_NCCREATE) { const cs: *const CREATESTRUCTW = @ptrFromInt(@as(usize, @bitCast(lParam))); if (cs.lpCreateParams) |ptr| { - _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, @intCast(@intFromPtr(ptr))); + _ = sys.SetWindowLongPtrW(hwnd, GWLP_USERDATA, @intCast(@intFromPtr(ptr))); } } @@ -4900,7 +4797,7 @@ fn wndProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.wina if (win32_uia.returnSettingsSectionGroupProvider(hwnd, wParam, lParam, provider)) |result| return result; } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, WM_ERASEBKGND => return 1, WM_PAINT => { @@ -4927,7 +4824,7 @@ fn wndProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.wina else colors.window_bg; const foreground = if (child) |control| - if (IsWindowEnabled(control) == 0) + if (sys.IsWindowEnabled(control) == 0) colors.disabled_text else if (edit_like) colors.edit_text @@ -4937,20 +4834,20 @@ fn wndProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.wina colors.text else colors.text; - _ = SetTextColor(hdc, foreground); - _ = SetBkColor(hdc, background); - if (msg == WM_CTLCOLORSTATIC) _ = SetBkMode(hdc, TRANSPARENT); + _ = sys.SetTextColor(hdc, foreground); + _ = sys.SetBkColor(hdc, background); + if (msg == WM_CTLCOLORSTATIC) _ = sys.SetBkMode(hdc, TRANSPARENT); if (o.theme_adapter.controlBrush(msg)) |brush| { return pointerLresult(brush); } } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, WM_DPICHANGED => { if (owner) |o| { o.dpi = normalizedDpi(@intCast(wParam & 0xFFFF)); const suggested: *const RECT = @ptrFromInt(@as(usize, @bitCast(lParam))); - _ = SetWindowPos( + _ = sys.SetWindowPos( hwnd, null, suggested.left, @@ -4961,7 +4858,7 @@ fn wndProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.wina ); o.recreateUiFont(); layoutChildren(o); - _ = InvalidateRect(hwnd, null, 1); + _ = sys.InvalidateRect(hwnd, null, 1); } return 0; }, @@ -5039,8 +4936,8 @@ fn wndProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.wina } if (clickedButton(id, notify, BTN_SAVE)) { if (owner) |o| { - const button_enabled = if (o.btn_save) |button| IsWindowEnabled(button) != 0 else false; - const button_visible = if (o.btn_save) |button| IsWindowVisible(button) != 0 else false; + const button_enabled = if (o.btn_save) |button| sys.IsWindowEnabled(button) != 0 else false; + const button_visible = if (o.btn_save) |button| sys.IsWindowVisible(button) != 0 else false; if (saveCommandCanDispatch( o.close_prompt_visible, o.save_in_flight, @@ -5214,7 +5111,7 @@ fn wndProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.wina } return 0; } - return DefWindowProcW(hwnd, msg, wParam, lParam); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, WM_CLOSE => { if (owner) |o| { @@ -5241,15 +5138,15 @@ fn wndProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.wina // the HWND without the user closing it) doesn't re-enter // the quit-timer path during teardown. if (owner) |o| o.clearChildRefs(); - _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); - return DefWindowProcW(hwnd, msg, wParam, lParam); + _ = sys.SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + return sys.DefWindowProcW(hwnd, msg, wParam, lParam); }, - else => return DefWindowProcW(hwnd, msg, wParam, lParam), + else => return sys.DefWindowProcW(hwnd, msg, wParam, lParam), } } fn recoverOwner(hwnd: HWND) ?*SettingsWindow { - const raw = GetWindowLongPtrW(hwnd, GWLP_USERDATA); + const raw = sys.GetWindowLongPtrW(hwnd, GWLP_USERDATA); if (raw == 0) return null; return @ptrFromInt(@as(usize, @intCast(raw))); } @@ -5283,32 +5180,32 @@ fn settingsControlRelease(ctx: *anyopaque) void { fn paint(hwnd: HWND, owner: *SettingsWindow) void { var ps: PAINTSTRUCT = undefined; - const hdc = BeginPaint(hwnd, &ps); - defer _ = EndPaint(hwnd, &ps); + const hdc = sys.BeginPaint(hwnd, &ps); + defer _ = sys.EndPaint(hwnd, &ps); var rect: RECT = undefined; - if (GetClientRect(hwnd, &rect) == 0) return; + if (sys.GetClientRect(hwnd, &rect) == 0) return; const colors = owner.theme_adapter.colors; - const fallback_brush = GetStockObject(DC_BRUSH); + const fallback_brush = sys.GetStockObject(DC_BRUSH); const window_brush = owner.theme_adapter.window_brush orelse fallback_brush; if (owner.theme_adapter.window_brush == null) { - _ = SetDCBrushColor(hdc, colors.window_bg); + _ = sys.SetDCBrushColor(hdc, colors.window_bg); } - _ = FillRect(hdc, &rect, window_brush); + _ = sys.FillRect(hdc, &rect, window_brush); var rail_rect = rect; rail_rect.right = owner.px(left_rail_width); const rail_brush = owner.theme_adapter.rail_brush orelse fallback_brush; if (owner.theme_adapter.rail_brush == null) { - _ = SetDCBrushColor(hdc, colors.rail_bg); + _ = sys.SetDCBrushColor(hdc, colors.rail_bg); } - _ = FillRect(hdc, &rail_rect, rail_brush); + _ = sys.FillRect(hdc, &rail_rect, rail_brush); } fn readEditUtf8(edit: HWND, buf: []u8) ?[]const u8 { var buf_w: [edit_text_max_code_units]u16 = undefined; - const n = GetWindowTextW(edit, &buf_w, @intCast(buf_w.len)); + const n = sys.GetWindowTextW(edit, &buf_w, @intCast(buf_w.len)); const written = std.unicode.utf16LeToUtf8(buf, buf_w[0..@intCast(n)]) catch return null; return buf[0..written]; } @@ -5317,7 +5214,7 @@ fn setEditText(edit: HWND, text: []const u8, suppress: *bool) void { var buf_w: [edit_text_max_code_units]u16 = undefined; const w = utf8ToW(&buf_w, text); suppress.* = true; - _ = SendMessageW(edit, WM_SETTEXT, 0, @bitCast(@intFromPtr(w))); + _ = sys.SendMessageW(edit, WM_SETTEXT, 0, @bitCast(@intFromPtr(w))); suppress.* = false; } diff --git a/src/apprt/win32_taskbar_progress.zig b/src/apprt/win32_taskbar_progress.zig index 296e0a3f59f..a37814d8d31 100644 --- a/src/apprt/win32_taskbar_progress.zig +++ b/src/apprt/win32_taskbar_progress.zig @@ -2,6 +2,7 @@ const std = @import("std"); const windows = std.os.windows; const progress_report = @import("../progress_report.zig"); const win32_types = @import("win32_types.zig"); +const sys = @import("win32/sys.zig"); const HRESULT = windows.HRESULT; const GUID = windows.GUID; @@ -31,14 +32,6 @@ const CLSID_TaskbarList = GUID.parse("{56FDF344-FD6D-11d0-958A-006097C9A090}"); const IID_ITaskbarList3 = GUID.parse("{EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF}"); const CO_E_NOTINITIALIZED: HRESULT = @bitCast(@as(u32, 0x800401F0)); -extern "ole32" fn CoCreateInstance( - rclsid: *const GUID, - pUnkOuter: ?*anyopaque, - dwClsContext: DWORD, - riid: *const GUID, - ppv: *?*anyopaque, -) callconv(.winapi) HRESULT; - const ITaskbarList3Vtbl = extern struct { QueryInterface: *const fn (*anyopaque, *const GUID, *?*anyopaque) callconv(.winapi) HRESULT, AddRef: *const fn (*anyopaque) callconv(.winapi) u32, @@ -138,7 +131,7 @@ pub const TaskbarProgress = struct { pub fn init() InitError!TaskbarProgress { var raw: ?*anyopaque = null; - const create_hr = CoCreateInstance( + const create_hr = sys.CoCreateInstance( &CLSID_TaskbarList, null, CLSCTX_INPROC_SERVER, diff --git a/src/apprt/win32_terminal_accessibility.zig b/src/apprt/win32_terminal_accessibility.zig index a69ac0e3995..6c0fd1f787d 100644 --- a/src/apprt/win32_terminal_accessibility.zig +++ b/src/apprt/win32_terminal_accessibility.zig @@ -7,6 +7,7 @@ const std = @import("std"); const win32_types = @import("win32_types.zig"); +const sys = @import("win32/sys.zig"); const win32_uia = @import("win32_uia/mod.zig"); const Allocator = std.mem.Allocator; @@ -31,20 +32,6 @@ const AnnouncementNormalizer = struct { has_text: bool = false, }; -extern "kernel32" fn GetTickCount64() callconv(.winapi) u64; -extern "user32" fn PostMessageW(HWND, UINT, WPARAM, LPARAM) callconv(.winapi) BOOL; -extern "user32" fn SendMessageTimeoutW( - HWND, - UINT, - WPARAM, - LPARAM, - UINT, - UINT, - *usize, -) callconv(.winapi) isize; -extern "user32" fn SetTimer(?HWND, usize, UINT, ?*const anyopaque) callconv(.winapi) usize; -extern "user32" fn KillTimer(?HWND, usize) callconv(.winapi) BOOL; - pub const Capture = struct { snapshot: win32_uia.AccessibleTextSnapshot, cell_width: f64, @@ -236,7 +223,7 @@ pub const TerminalAccessibilitySession = struct { &self.recent_input_complete, input, )) { - self.recent_input_ms = GetTickCount64(); + self.recent_input_ms = sys.GetTickCount64(); } } @@ -247,7 +234,7 @@ pub const TerminalAccessibilitySession = struct { self.mutex.lock(); defer self.mutex.unlock(); if (!self.attached) return; - const without_echo = self.stripRecentInputEcho(output, GetTickCount64()); + const without_echo = self.stripRecentInputEcho(output, sys.GetTickCount64()); const candidate = self.normalizeSemanticOutputLocked( without_echo.held_prefix, without_echo.output, @@ -329,7 +316,7 @@ pub const TerminalAccessibilitySession = struct { requested_speech_mode: SpeechMode, ) void { const provider = self.provider orelse return; - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); const policy = publishPolicy( win32_uia.events.clientsAreListening(), queryRecentlyActive(self.last_query_ms.load(.acquire), now_ms), @@ -338,7 +325,7 @@ pub const TerminalAccessibilitySession = struct { if (!force and !refreshDue(self.last_refresh_ms, now_ms)) { if (!self.refresh_timer_active) { const hwnd = self.ops.hwnd(self.ops.ctx) orelse return; - if (SetTimer(hwnd, self.timer_id, refreshDelay(self.last_refresh_ms, now_ms), null) != 0) { + if (sys.SetTimer(hwnd, self.timer_id, refreshDelay(self.last_refresh_ms, now_ms), null) != 0) { self.refresh_timer_active = true; } } @@ -356,7 +343,7 @@ pub const TerminalAccessibilitySession = struct { else .discard; const result = self.refresh(speech_mode, true); - const completed_ms = GetTickCount64(); + const completed_ms = sys.GetTickCount64(); self.last_refresh_ms = completed_ms; if (snapshotWasSlow(started_ms, completed_ms)) { std.log.warn("win32 terminal UIA snapshot slow elapsed_ms={d}", .{completed_ms -| started_ms}); @@ -382,7 +369,7 @@ pub const TerminalAccessibilitySession = struct { } if (refresh_result.announcement_pending and !self.refresh_timer_active) { const hwnd = self.ops.hwnd(self.ops.ctx) orelse return; - if (SetTimer(hwnd, self.timer_id, refresh_interval_ms, null) != 0) { + if (sys.SetTimer(hwnd, self.timer_id, refresh_interval_ms, null) != 0) { self.refresh_timer_active = true; } } @@ -448,7 +435,7 @@ pub const TerminalAccessibilitySession = struct { self.alloc.free(visible_text); self.alloc.free(cells); self.alloc.free(text); - if (update_refresh_time) self.last_refresh_ms = GetTickCount64(); + if (update_refresh_time) self.last_refresh_ms = sys.GetTickCount64(); return .{ .change = .unchanged, .announcement = announcement, @@ -476,7 +463,7 @@ pub const TerminalAccessibilitySession = struct { self.cell_height = capture.cell_height; self.origin_x = capture.origin_x; self.origin_y = capture.origin_y; - if (update_refresh_time) self.last_refresh_ms = GetTickCount64(); + if (update_refresh_time) self.last_refresh_ms = sys.GetTickCount64(); return .{ .change = if (text_changed and caret_changed) .text_and_caret @@ -561,7 +548,7 @@ pub const TerminalAccessibilitySession = struct { fn armAnnouncementTimer(self: *TerminalAccessibilitySession) void { if (!self.hasPendingAnnouncement() or self.refresh_timer_active) return; const hwnd = self.ops.hwnd(self.ops.ctx) orelse return; - if (SetTimer(hwnd, self.timer_id, 1, null) != 0) self.refresh_timer_active = true; + if (sys.SetTimer(hwnd, self.timer_id, 1, null) != 0) self.refresh_timer_active = true; } fn enqueuePendingAnnouncements( @@ -593,7 +580,7 @@ pub const TerminalAccessibilitySession = struct { fn cancelTimer(self: *TerminalAccessibilitySession) void { if (!self.refresh_timer_active) return; - if (self.ops.hwnd(self.ops.ctx)) |hwnd| _ = KillTimer(hwnd, self.timer_id); + if (self.ops.hwnd(self.ops.ctx)) |hwnd| _ = sys.KillTimer(hwnd, self.timer_id); self.refresh_timer_active = false; } @@ -679,7 +666,7 @@ pub const TerminalAccessibilitySession = struct { } fn noteTextQuery(self: *TerminalAccessibilitySession) void { - const now_ms = GetTickCount64(); + const now_ms = sys.GetTickCount64(); const previous_query_ms = self.last_query_ms.swap(now_ms, .acq_rel); self.mutex.lock(); const hwnd = if (self.attached) self.ops.hwnd(self.ops.ctx) else null; @@ -687,7 +674,7 @@ pub const TerminalAccessibilitySession = struct { if (hwnd == null) return; if (queryNeedsSynchronousRefresh(previous_query_ms, now_ms)) { var ignored: usize = 0; - if (SendMessageTimeoutW( + if (sys.SendMessageTimeoutW( hwnd.?, self.query_message, 1, @@ -698,7 +685,7 @@ pub const TerminalAccessibilitySession = struct { ) != 0) return; } if (self.query_refresh_post_pending.cmpxchgStrong(false, true, .acq_rel, .acquire) != null) return; - if (PostMessageW(hwnd.?, self.query_message, 0, 0) == 0) { + if (sys.PostMessageW(hwnd.?, self.query_message, 0, 0) == 0) { self.query_refresh_post_pending.store(false, .release); } } diff --git a/src/apprt/win32_uia/widgets.zig b/src/apprt/win32_uia/widgets.zig index 5f807fd1e96..f63273e25d2 100644 --- a/src/apprt/win32_uia/widgets.zig +++ b/src/apprt/win32_uia/widgets.zig @@ -5677,6 +5677,788 @@ test "SettingsControlProvider exposes InvokeProvider only for buttons" { try std.testing.expect(invoke == null); } +test "settings provider behavior invoke dispatch is asynchronous and source preserving" { + const win32 = struct { + const MSG = extern struct { + hwnd: ?com.HWND, + message: u32, + wParam: com.WPARAM, + lParam: com.LPARAM, + time: u32, + pt: POINT, + lPrivate: u32, + }; + + extern "user32" fn CreateWindowExW( + dwExStyle: u32, + lpClassName: [*:0]const u16, + lpWindowName: [*:0]const u16, + dwStyle: u32, + x: i32, + y: i32, + nWidth: i32, + nHeight: i32, + hWndParent: ?com.HWND, + hMenu: ?*anyopaque, + hInstance: ?*anyopaque, + lpParam: ?*anyopaque, + ) callconv(.winapi) ?com.HWND; + extern "user32" fn DefWindowProcW( + hWnd: com.HWND, + Msg: u32, + wParam: com.WPARAM, + lParam: com.LPARAM, + ) callconv(.winapi) com.LRESULT; + extern "user32" fn DestroyWindow(hWnd: com.HWND) callconv(.winapi) com.BOOL; + extern "user32" fn SetWindowLongPtrW( + hWnd: com.HWND, + nIndex: i32, + dwNewLong: isize, + ) callconv(.winapi) isize; + extern "user32" fn PeekMessageW( + lpMsg: *MSG, + hWnd: ?com.HWND, + wMsgFilterMin: u32, + wMsgFilterMax: u32, + wRemoveMsg: u32, + ) callconv(.winapi) com.BOOL; + extern "user32" fn DispatchMessageW(lpMsg: *const MSG) callconv(.winapi) com.LRESULT; + }; + const fixture = struct { + var command_calls: usize = 0; + var last_wparam: com.WPARAM = 0; + var last_lparam: com.LPARAM = 0; + + fn wndProc( + hwnd: com.HWND, + message: u32, + wparam: com.WPARAM, + lparam: com.LPARAM, + ) callconv(.winapi) com.LRESULT { + if (message == WM_COMMAND) { + command_calls += 1; + last_wparam = wparam; + last_lparam = lparam; + return 1; + } + return win32.DefWindowProcW(hwnd, message, wparam, lparam); + } + }; + + fixture.command_calls = 0; + fixture.last_wparam = 0; + fixture.last_lparam = 0; + + const static_class = std.unicode.utf8ToUtf16LeStringLiteral("STATIC"); + const button_class = std.unicode.utf8ToUtf16LeStringLiteral("BUTTON"); + const empty_title = std.unicode.utf8ToUtf16LeStringLiteral(""); + const parent = win32.CreateWindowExW( + 0, + static_class, + empty_title, + 0, + 0, + 0, + 100, + 100, + null, + null, + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(parent); + try std.testing.expect(win32.SetWindowLongPtrW( + parent, + -4, + @intCast(@intFromPtr(&fixture.wndProc)), + ) != 0); + + const child = win32.CreateWindowExW( + 0, + button_class, + empty_title, + 0x40000000, + 0, + 0, + 80, + 24, + parent, + @ptrFromInt(41), + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(child); + const provider = try SettingsControlProvider.create( + std.testing.allocator, + child, + .button, + "Save", + ); + defer _ = SettingsControlProvider.Release(&provider.base); + + try std.testing.expectEqual( + com.S_OK, + SettingsControlProvider.Invoke(&provider.invoke_iface), + ); + try std.testing.expectEqual(@as(usize, 0), fixture.command_calls); + + var message: win32.MSG = undefined; + try std.testing.expect(win32.PeekMessageW( + &message, + parent, + WM_COMMAND, + WM_COMMAND, + 1, + ) != 0); + _ = win32.DispatchMessageW(&message); + try std.testing.expectEqual(@as(usize, 1), fixture.command_calls); + try std.testing.expectEqual(@as(com.WPARAM, 41), fixture.last_wparam); + try std.testing.expectEqual( + @as(com.LPARAM, @bitCast(@intFromPtr(child))), + fixture.last_lparam, + ); +} + +test "settings provider behavior selection dispatch is synchronous and postcondition checked" { + const win32 = struct { + extern "user32" fn CreateWindowExW( + dwExStyle: u32, + lpClassName: [*:0]const u16, + lpWindowName: [*:0]const u16, + dwStyle: u32, + x: i32, + y: i32, + nWidth: i32, + nHeight: i32, + hWndParent: ?com.HWND, + hMenu: ?*anyopaque, + hInstance: ?*anyopaque, + lpParam: ?*anyopaque, + ) callconv(.winapi) ?com.HWND; + extern "user32" fn DefWindowProcW( + hWnd: com.HWND, + Msg: u32, + wParam: com.WPARAM, + lParam: com.LPARAM, + ) callconv(.winapi) com.LRESULT; + extern "user32" fn DestroyWindow(hWnd: com.HWND) callconv(.winapi) com.BOOL; + extern "user32" fn SetWindowLongPtrW( + hWnd: com.HWND, + nIndex: i32, + dwNewLong: isize, + ) callconv(.winapi) isize; + }; + const fixture = struct { + var group: ?*SettingsSectionGroupProvider = null; + var update_selection = false; + var command_calls: usize = 0; + var last_wparam: com.WPARAM = 0; + var last_lparam: com.LPARAM = 0; + + fn wndProc( + hwnd: com.HWND, + message: u32, + wparam: com.WPARAM, + lparam: com.LPARAM, + ) callconv(.winapi) com.LRESULT { + if (message == WM_COMMAND) { + command_calls += 1; + last_wparam = wparam; + last_lparam = lparam; + if (update_selection) group.?.setSelected(0); + return 1; + } + return win32.DefWindowProcW(hwnd, message, wparam, lparam); + } + }; + + fixture.group = null; + fixture.update_selection = false; + fixture.command_calls = 0; + fixture.last_wparam = 0; + fixture.last_lparam = 0; + + const static_class = std.unicode.utf8ToUtf16LeStringLiteral("STATIC"); + const button_class = std.unicode.utf8ToUtf16LeStringLiteral("BUTTON"); + const empty_title = std.unicode.utf8ToUtf16LeStringLiteral(""); + const parent = win32.CreateWindowExW( + 0, + static_class, + empty_title, + 0, + 0, + 0, + 100, + 100, + null, + null, + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(parent); + try std.testing.expect(win32.SetWindowLongPtrW( + parent, + -4, + @intCast(@intFromPtr(&fixture.wndProc)), + ) != 0); + + const child = win32.CreateWindowExW( + 0, + button_class, + empty_title, + 0x40000000, + 0, + 0, + 80, + 24, + parent, + @ptrFromInt(42), + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(child); + + const group = try SettingsSectionGroupProvider.create(std.testing.allocator, parent); + const section = try SettingsSectionProvider.create( + std.testing.allocator, + child, + "Appearance", + 0, + group, + ); + group.setSection(0, section); + fixture.group = group; + defer { + fixture.group = null; + group.detach(); + section.detach(); + _ = SettingsSectionProvider.Release(§ion.base); + _ = SettingsSectionGroupProvider.Release(&group.base); + } + + fixture.update_selection = true; + try std.testing.expectEqual( + com.S_OK, + SettingsSectionProvider.Select(§ion.selection_iface), + ); + try std.testing.expect(section.isSelected()); + try std.testing.expectEqual(@as(usize, 1), fixture.command_calls); + try std.testing.expectEqual(@as(com.WPARAM, 42), fixture.last_wparam); + try std.testing.expectEqual( + @as(com.LPARAM, @bitCast(@intFromPtr(child))), + fixture.last_lparam, + ); + + group.setSelected(1); + fixture.update_selection = false; + try std.testing.expectEqual( + com.UIA_E_INVALIDOPERATION, + SettingsSectionProvider.Select(§ion.selection_iface), + ); + try std.testing.expect(!section.isSelected()); + try std.testing.expectEqual(@as(usize, 2), fixture.command_calls); +} + +test "settings provider behavior selection dispatch is bounded for a hung owner thread" { + const win32 = struct { + const MSG = extern struct { + hwnd: ?com.HWND, + message: u32, + wParam: com.WPARAM, + lParam: com.LPARAM, + time: u32, + pt: POINT, + lPrivate: u32, + }; + + const WAIT_OBJECT_0: u32 = 0; + const WAIT_TIMEOUT: u32 = 258; + const INFINITE: u32 = 0xFFFFFFFF; + const PM_REMOVE: u32 = 0x0001; + + extern "kernel32" fn CreateEventW( + lpEventAttributes: ?*anyopaque, + bManualReset: com.BOOL, + bInitialState: com.BOOL, + lpName: ?[*:0]const u16, + ) callconv(.winapi) ?*anyopaque; + extern "kernel32" fn SetEvent(hEvent: *anyopaque) callconv(.winapi) com.BOOL; + extern "kernel32" fn WaitForSingleObject( + hHandle: *anyopaque, + dwMilliseconds: u32, + ) callconv(.winapi) u32; + extern "kernel32" fn CloseHandle(hObject: *anyopaque) callconv(.winapi) com.BOOL; + extern "user32" fn CreateWindowExW( + dwExStyle: u32, + lpClassName: [*:0]const u16, + lpWindowName: [*:0]const u16, + dwStyle: u32, + x: i32, + y: i32, + nWidth: i32, + nHeight: i32, + hWndParent: ?com.HWND, + hMenu: ?*anyopaque, + hInstance: ?*anyopaque, + lpParam: ?*anyopaque, + ) callconv(.winapi) ?com.HWND; + extern "user32" fn DefWindowProcW( + hWnd: com.HWND, + Msg: u32, + wParam: com.WPARAM, + lParam: com.LPARAM, + ) callconv(.winapi) com.LRESULT; + extern "user32" fn DestroyWindow(hWnd: com.HWND) callconv(.winapi) com.BOOL; + extern "user32" fn SetWindowLongPtrW( + hWnd: com.HWND, + nIndex: i32, + dwNewLong: isize, + ) callconv(.winapi) isize; + extern "user32" fn PeekMessageW( + lpMsg: *MSG, + hWnd: ?com.HWND, + wMsgFilterMin: u32, + wMsgFilterMax: u32, + wRemoveMsg: u32, + ) callconv(.winapi) com.BOOL; + }; + const Fixture = struct { + var active: ?*@This() = null; + + ready_event: *anyopaque, + release_event: *anyopaque, + owner_released_event: *anyopaque, + destroy_event: *anyopaque, + select_done_event: *anyopaque, + parent: ?com.HWND, + child: ?com.HWND, + section: ?*SettingsSectionProvider, + owner_setup_ok: std.atomic.Value(bool), + owner_thread_calls: std.atomic.Value(usize), + select_calls: std.atomic.Value(usize), + command_calls: std.atomic.Value(usize), + select_result: std.atomic.Value(com.HRESULT), + + fn wndProc( + hwnd: com.HWND, + message: u32, + wparam: com.WPARAM, + lparam: com.LPARAM, + ) callconv(.winapi) com.LRESULT { + const self = active orelse return win32.DefWindowProcW(hwnd, message, wparam, lparam); + if (message == WM_COMMAND) { + _ = self.command_calls.fetchAdd(1, .acq_rel); + return 1; + } + return win32.DefWindowProcW(hwnd, message, wparam, lparam); + } + + fn ownerMain(self: *@This()) void { + _ = self.owner_thread_calls.fetchAdd(1, .acq_rel); + const static_class = std.unicode.utf8ToUtf16LeStringLiteral("STATIC"); + const button_class = std.unicode.utf8ToUtf16LeStringLiteral("BUTTON"); + const empty_title = std.unicode.utf8ToUtf16LeStringLiteral(""); + const parent = win32.CreateWindowExW( + 0, + static_class, + empty_title, + 0, + 0, + 0, + 100, + 100, + null, + null, + null, + null, + ) orelse { + _ = win32.SetEvent(self.ready_event); + return; + }; + self.parent = parent; + if (win32.SetWindowLongPtrW( + parent, + -4, + @intCast(@intFromPtr(&wndProc)), + ) == 0) { + _ = win32.DestroyWindow(parent); + self.parent = null; + _ = win32.SetEvent(self.ready_event); + return; + } + const child = win32.CreateWindowExW( + 0, + button_class, + empty_title, + 0x40000000, + 0, + 0, + 80, + 24, + parent, + @ptrFromInt(44), + null, + null, + ) orelse { + _ = win32.DestroyWindow(parent); + self.parent = null; + _ = win32.SetEvent(self.ready_event); + return; + }; + self.child = child; + self.owner_setup_ok.store(true, .release); + _ = win32.SetEvent(self.ready_event); + + // Deliberately do not pump this owner thread until the watchdog + // releases it. PeekMessage then drains any mutation-induced + // unbounded synchronous send so the fixture can shut down safely. + _ = win32.WaitForSingleObject(self.release_event, win32.INFINITE); + var message: win32.MSG = undefined; + _ = win32.PeekMessageW(&message, parent, WM_COMMAND, WM_COMMAND, win32.PM_REMOVE); + _ = win32.SetEvent(self.owner_released_event); + _ = win32.WaitForSingleObject(self.destroy_event, win32.INFINITE); + _ = win32.DestroyWindow(child); + _ = win32.DestroyWindow(parent); + } + + fn selectMain(self: *@This()) void { + _ = self.select_calls.fetchAdd(1, .acq_rel); + const result = SettingsSectionProvider.Select(&self.section.?.selection_iface); + self.select_result.store(result, .release); + _ = win32.SetEvent(self.select_done_event); + } + }; + + const ready_event = win32.CreateEventW(null, 1, 0, null) orelse return error.TestUnexpectedResult; + defer _ = win32.CloseHandle(ready_event); + const release_event = win32.CreateEventW(null, 1, 0, null) orelse return error.TestUnexpectedResult; + defer _ = win32.CloseHandle(release_event); + const owner_released_event = win32.CreateEventW(null, 1, 0, null) orelse return error.TestUnexpectedResult; + defer _ = win32.CloseHandle(owner_released_event); + const destroy_event = win32.CreateEventW(null, 1, 0, null) orelse return error.TestUnexpectedResult; + defer _ = win32.CloseHandle(destroy_event); + const select_done_event = win32.CreateEventW(null, 1, 0, null) orelse return error.TestUnexpectedResult; + defer _ = win32.CloseHandle(select_done_event); + + var fixture: Fixture = .{ + .ready_event = ready_event, + .release_event = release_event, + .owner_released_event = owner_released_event, + .destroy_event = destroy_event, + .select_done_event = select_done_event, + .parent = null, + .child = null, + .section = null, + .owner_setup_ok = std.atomic.Value(bool).init(false), + .owner_thread_calls = std.atomic.Value(usize).init(0), + .select_calls = std.atomic.Value(usize).init(0), + .command_calls = std.atomic.Value(usize).init(0), + .select_result = std.atomic.Value(com.HRESULT).init(com.E_NOTIMPL), + }; + Fixture.active = &fixture; + defer Fixture.active = null; + const owner_thread = try std.Thread.spawn(.{}, Fixture.ownerMain, .{&fixture}); + var owner_joined = false; + defer if (!owner_joined) { + _ = win32.SetEvent(release_event); + _ = win32.SetEvent(destroy_event); + owner_thread.join(); + }; + + try std.testing.expectEqual( + win32.WAIT_OBJECT_0, + win32.WaitForSingleObject(ready_event, win32.INFINITE), + ); + try std.testing.expect(fixture.owner_setup_ok.load(.acquire)); + const parent = fixture.parent orelse return error.TestUnexpectedResult; + const child = fixture.child orelse return error.TestUnexpectedResult; + const group = try SettingsSectionGroupProvider.create(std.testing.allocator, parent); + const section = try SettingsSectionProvider.create( + std.testing.allocator, + child, + "Keyboard shortcuts", + 0, + group, + ); + group.setSection(0, section); + fixture.section = section; + defer { + fixture.section = null; + group.detach(); + section.detach(); + _ = SettingsSectionProvider.Release(§ion.base); + _ = SettingsSectionGroupProvider.Release(&group.base); + } + + const select_thread = try std.Thread.spawn(.{}, Fixture.selectMain, .{&fixture}); + defer select_thread.join(); + const select_wait = win32.WaitForSingleObject(select_done_event, 4000); + try std.testing.expect(select_wait == win32.WAIT_OBJECT_0 or select_wait == win32.WAIT_TIMEOUT); + + _ = win32.SetEvent(release_event); + try std.testing.expectEqual( + win32.WAIT_OBJECT_0, + win32.WaitForSingleObject(owner_released_event, win32.INFINITE), + ); + if (select_wait == win32.WAIT_TIMEOUT) { + _ = win32.WaitForSingleObject(select_done_event, win32.INFINITE); + } + _ = win32.SetEvent(destroy_event); + owner_thread.join(); + owner_joined = true; + + try std.testing.expectEqual(win32.WAIT_OBJECT_0, select_wait); + try std.testing.expectEqual(com.UIA_E_ELEMENTNOTAVAILABLE, fixture.select_result.load(.acquire)); + try std.testing.expectEqual(@as(usize, 1), fixture.owner_thread_calls.load(.acquire)); + try std.testing.expectEqual(@as(usize, 1), fixture.select_calls.load(.acquire)); + try std.testing.expectEqual(@as(usize, 0), fixture.command_calls.load(.acquire)); +} + +test "settings provider behavior keyboard focus includes descendants" { + const win32 = struct { + extern "user32" fn CreateWindowExW( + dwExStyle: u32, + lpClassName: [*:0]const u16, + lpWindowName: [*:0]const u16, + dwStyle: u32, + x: i32, + y: i32, + nWidth: i32, + nHeight: i32, + hWndParent: ?com.HWND, + hMenu: ?*anyopaque, + hInstance: ?*anyopaque, + lpParam: ?*anyopaque, + ) callconv(.winapi) ?com.HWND; + extern "user32" fn DestroyWindow(hWnd: com.HWND) callconv(.winapi) com.BOOL; + extern "user32" fn SetFocus(hWnd: com.HWND) callconv(.winapi) ?com.HWND; + }; + + const static_class = std.unicode.utf8ToUtf16LeStringLiteral("STATIC"); + const edit_class = std.unicode.utf8ToUtf16LeStringLiteral("EDIT"); + const empty_title = std.unicode.utf8ToUtf16LeStringLiteral(""); + const parent = win32.CreateWindowExW( + 0, + static_class, + empty_title, + 0, + 0, + 0, + 100, + 100, + null, + null, + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(parent); + const child = win32.CreateWindowExW( + 0, + edit_class, + empty_title, + 0x40000000, + 0, + 0, + 80, + 24, + parent, + @ptrFromInt(43), + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(child); + const peer = win32.CreateWindowExW( + 0, + static_class, + empty_title, + 0, + 0, + 0, + 100, + 100, + null, + null, + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(peer); + + _ = win32.SetFocus(child); + try std.testing.expect(hwndHasKeyboardFocus(child)); + try std.testing.expect(hwndHasKeyboardFocus(parent)); + try std.testing.expect(!hwndHasKeyboardFocus(peer)); + + _ = win32.SetFocus(peer); + try std.testing.expect(hwndHasKeyboardFocus(peer)); + try std.testing.expect(!hwndHasKeyboardFocus(child)); + try std.testing.expect(!hwndHasKeyboardFocus(parent)); +} + +test "settings provider behavior focus properties are provider routed and thread correct" { + const win32 = struct { + extern "user32" fn CreateWindowExW( + dwExStyle: u32, + lpClassName: [*:0]const u16, + lpWindowName: [*:0]const u16, + dwStyle: u32, + x: i32, + y: i32, + nWidth: i32, + nHeight: i32, + hWndParent: ?com.HWND, + hMenu: ?*anyopaque, + hInstance: ?*anyopaque, + lpParam: ?*anyopaque, + ) callconv(.winapi) ?com.HWND; + extern "user32" fn DestroyWindow(hWnd: com.HWND) callconv(.winapi) com.BOOL; + extern "user32" fn SetFocus(hWnd: com.HWND) callconv(.winapi) ?com.HWND; + }; + const fixture = struct { + var control_property_queries: usize = 0; + var section_property_queries: usize = 0; + var focus_event_calls: usize = 0; + + fn queryControl(provider: *SettingsControlProvider) !bool { + control_property_queries += 1; + var value = com.VARIANT.empty(); + try std.testing.expectEqual( + com.S_OK, + provider.base.vtbl.GetPropertyValue( + &provider.base, + constants.UIA_HasKeyboardFocusPropertyId, + &value, + ), + ); + try std.testing.expectEqual(com.VT_BOOL, value.vt); + return value.value.bool_val == com.VARIANT_TRUE; + } + + fn querySection(provider: *SettingsSectionProvider) !bool { + section_property_queries += 1; + var value = com.VARIANT.empty(); + try std.testing.expectEqual( + com.S_OK, + provider.base.vtbl.GetPropertyValue( + &provider.base, + constants.UIA_HasKeyboardFocusPropertyId, + &value, + ), + ); + try std.testing.expectEqual(com.VT_BOOL, value.vt); + return value.value.bool_val == com.VARIANT_TRUE; + } + + fn raiseControl(provider: *SettingsControlProvider) void { + focus_event_calls += 1; + provider.raiseFocusChanged(); + } + + fn raiseSection(provider: *SettingsSectionProvider) void { + focus_event_calls += 1; + provider.raiseFocusChanged(); + } + }; + + fixture.control_property_queries = 0; + fixture.section_property_queries = 0; + fixture.focus_event_calls = 0; + + const static_class = std.unicode.utf8ToUtf16LeStringLiteral("STATIC"); + const button_class = std.unicode.utf8ToUtf16LeStringLiteral("BUTTON"); + const empty_title = std.unicode.utf8ToUtf16LeStringLiteral(""); + const parent = win32.CreateWindowExW( + 0, + static_class, + empty_title, + 0, + 0, + 0, + 100, + 100, + null, + null, + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(parent); + const child = win32.CreateWindowExW( + 0, + button_class, + empty_title, + 0x40000000, + 0, + 0, + 80, + 24, + parent, + @ptrFromInt(45), + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(child); + const peer = win32.CreateWindowExW( + 0, + static_class, + empty_title, + 0, + 0, + 0, + 100, + 100, + null, + null, + null, + null, + ) orelse return error.TestUnexpectedResult; + defer _ = win32.DestroyWindow(peer); + + const control = try SettingsControlProvider.create( + std.testing.allocator, + parent, + .button, + "Settings", + ); + defer { + _ = control.disconnect(); + _ = SettingsControlProvider.Release(&control.base); + } + const group = try SettingsSectionGroupProvider.create(std.testing.allocator, parent); + const section = try SettingsSectionProvider.create( + std.testing.allocator, + child, + "Appearance", + 0, + group, + ); + group.setSection(0, section); + defer { + _ = section.disconnect(); + _ = group.disconnect(); + _ = SettingsSectionProvider.Release(§ion.base); + _ = SettingsSectionGroupProvider.Release(&group.base); + } + + var disconnected_all = false; + defer if (!disconnected_all) { + _ = com.UiaDisconnectAllProviders(); + }; + _ = win32.SetFocus(child); + try std.testing.expect(try fixture.queryControl(control)); + try std.testing.expect(try fixture.querySection(section)); + fixture.raiseControl(control); + fixture.raiseSection(section); + + _ = win32.SetFocus(peer); + try std.testing.expect(!try fixture.queryControl(control)); + try std.testing.expect(!try fixture.querySection(section)); + try std.testing.expectEqual(com.S_OK, com.UiaDisconnectAllProviders()); + disconnected_all = true; + + try std.testing.expectEqual(@as(usize, 2), fixture.control_property_queries); + try std.testing.expectEqual(@as(usize, 2), fixture.section_property_queries); + try std.testing.expectEqual(@as(usize, 2), fixture.focus_event_calls); +} + test "SettingsSectionProvider preserves required single-selection semantics" { try std.testing.expectEqual(com.S_OK, settingsSectionAddToSelectionResult(true)); try std.testing.expectEqual(com.UIA_E_INVALIDOPERATION, settingsSectionAddToSelectionResult(false)); diff --git a/test/windows/PSScriptAnalyzerSettings.psd1 b/test/windows/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 00000000000..31448b144a5 --- /dev/null +++ b/test/windows/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,9 @@ +@{ + Severity = @('Error', 'Warning') + IncludeRules = @( + 'PSAvoidUsingCmdletAliases' + 'PSAvoidUsingInvokeExpression' + 'PSAvoidUsingPlainTextForPassword' + 'PSUseDeclaredVarsMoreThanAssignments' + ) +} diff --git a/test/windows/README.md b/test/windows/README.md index 94f66850d02..acf111d8ad5 100644 --- a/test/windows/README.md +++ b/test/windows/README.md @@ -7,6 +7,128 @@ Each interactive Win11 harness uses its own repo-local sandbox under only that harness's logs/temp state instead of tearing down sibling validators. +## interactive-win11-accessibility.ps1 + +Full interactive UI Automation story for terminal text, focus, notifications, +Settings, themes, High Contrast, and accessibility idle/stress behavior. + +```powershell +pwsh -NoProfile -File .\interactive-win11-accessibility.ps1 -ResetState +``` + +## interactive-win11-key-input.ps1 + +Validates Win32 keyboard and Unicode input delivery plus focus ownership across +the supported key scenarios. + +```powershell +pwsh -NoProfile -File .\interactive-win11-key-input.ps1 -ResetState -Key a +``` + +## interactive-win11-new-tab.ps1 + +Validates new-tab creation, focus, and preservation of maximized host-window +state while inactive tabs initialize. + +```powershell +pwsh -NoProfile -File .\interactive-win11-new-tab.ps1 -ResetState +``` + +## interactive-win11-configured-size.ps1 + +Launches a terminal with configured row and column dimensions and verifies the +resulting console and native-window size evidence. + +```powershell +pwsh -NoProfile -File .\interactive-win11-configured-size.ps1 -ResetState +``` + +## interactive-win11-shell-command.ps1 + +Validates configured `cmd.exe` and PowerShell shell-command launch paths, +captured output, exit status, and failure diagnostics. + +```powershell +pwsh -NoProfile -File .\interactive-win11-shell-command.ps1 -ResetState +``` + +## interactive-win11-shell-command-live.ps1 + +Types a Noctty CLI action into a live terminal and validates focus, screen +capture, and command output. + +```powershell +pwsh -NoProfile -File .\interactive-win11-shell-command-live.ps1 -ResetState -CliAction +help +``` + +## interactive-win11-pr-smoke.ps1 + +Serial PR smoke entry point for startup, input, tabs, resize, undo, +accessibility, palette-theme, and session-restore harnesses. + +```powershell +pwsh -NoProfile -File .\interactive-win11-pr-smoke.ps1 -ResetState +``` + +## interactive-win11-stateful-lib.ps1 + +Dot-sourced helpers for stateful window discovery, Win32 messaging, pixel +sampling, process launch, and cleanup. It is not a standalone harness. + +## cli-shell-command.ps1 + +Runs a Noctty CLI action through `cmd.exe` or PowerShell and verifies its +exit code plus expected redirected text. + +```powershell +pwsh -NoProfile -File .\cli-shell-command.ps1 -Shell cmd -Arguments +help -ExpectedText 'Usage: noctty' +``` + +## cli-redirected-text-action.ps1 + +Runs a GUI-subsystem CLI action with redirected stdout/stderr and verifies +clean, bounded text-mode completion. Wired into `package-portable-cli.ps1`. + +```powershell +pwsh -NoProfile -File .\cli-redirected-text-action.ps1 -Action +help -ExpectedText 'Usage: noctty' +``` + +## cli-detached-action.ps1 + +Validates a detached CLI action, its resource discovery, timeout behavior, and +absence of the native CLI-failure dialog. + +```powershell +pwsh -NoProfile -File .\cli-detached-action.ps1 -Action +version +``` + +## package-portable-cli.ps1 + +Packages or reuses a portable build, then runs shell, redirected-text, and +detached-action checks against the staged artifacts. + +```powershell +pwsh -NoProfile -File .\package-portable-cli.ps1 -Version 0.0.1 +``` + +## assert-interactive-runner.ps1 + +GitHub Actions preflight for the dedicated interactive Windows runner. It +checks OS/session/runner provenance and can write the evidence JSON path. + +```powershell +pwsh -NoProfile -File .\assert-interactive-runner.ps1 -OutputPath .\runner-evidence.json +``` + +## powershell-shell-integration.ps1 + +Manual check for PowerShell shell-integration prompt hooks, OSC output, URI +escaping, feature flags, and cleanup. It is not wired into an automated runner. + +```powershell +pwsh -NoProfile -File .\powershell-shell-integration.ps1 +``` + ## interactive-win11-validate.ps1 Composite Win11 validator. It runs launch-helper checks and startup smoke, @@ -202,7 +324,7 @@ powershell.exe -ExecutionPolicy Bypass -File .\interactive-win11-session-restore ## ..\..\scripts\interactive-win11.ps1 Generic repo-local Win11 launcher for ad hoc debugging. It uses the -same sandbox/bootstrap logic as the focused harnesses and can either +same sandbox setup logic as the focused harnesses and can either launch `noctty` directly or open a shell with the sandbox environment applied. @@ -212,6 +334,16 @@ Run with: powershell.exe -ExecutionPolicy Bypass -File ..\..\scripts\interactive-win11.ps1 -ResetState ``` +## Style + +- Define reusable helpers as `function Verb-Noun { ... }` with a `param(...)` + block and typed, PascalCase parameters where practical. +- Resolve `$repoRoot` once, then dot-source shared code with + `. (Join-Path $repoRoot 'scripts\name.ps1')`. +- Aim for roughly 120 columns. Wrap multi-parameter invocations with splatting + or aligned continuations; keep indivisible diagnostic strings and pinned + regular expressions intact. + ## test_dll_init.c Regression test for the DLL CRT initialization fix. Loads ghostty.dll diff --git a/test/windows/cli-redirected-text-action.ps1 b/test/windows/cli-redirected-text-action.ps1 index 667cc27da10..e4bbb3f7594 100644 --- a/test/windows/cli-redirected-text-action.ps1 +++ b/test/windows/cli-redirected-text-action.ps1 @@ -5,6 +5,8 @@ param( [Parameter(Mandatory = $true)] [string] $ExpectedText, + [string] $ExePath = $null, + [int] $TimeoutSeconds = 5 ) @@ -16,7 +18,9 @@ if ($TimeoutSeconds -le 0) { $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) . (Join-Path $repoRoot 'scripts\interactive-win11-lib.ps1') -$exePath = Join-Path $repoRoot 'zig-out\bin\noctty.exe' +if ([string]::IsNullOrWhiteSpace($ExePath)) { + $ExePath = Join-Path $repoRoot 'zig-out\bin\noctty.exe' +} $scratchDir = Join-Path $repoRoot 'zig-out\cli-redirected' $actionSlug = $Action.TrimStart('+') $actionSlug = $actionSlug -replace '[\\/:*?"<>|]', '_' @@ -27,27 +31,31 @@ if ([string]::IsNullOrWhiteSpace($actionSlug)) { $stdoutPath = Join-Path $scratchDir "$actionSlug.stdout.txt" $stderrPath = Join-Path $scratchDir "$actionSlug.stderr.txt" -if (-not (Test-Path $exePath)) { - throw "Missing built executable: $exePath. Run `zig build -Demit-exe=true` first." +if (-not (Test-Path $ExePath)) { + throw "Missing built executable: $ExePath. Run `zig build -Demit-exe=true` first." } New-Item -ItemType Directory -Force -Path $scratchDir | Out-Null Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue $process = Start-Process ` - -FilePath $exePath ` + -FilePath $ExePath ` -ArgumentList $Action ` -RedirectStandardOutput $stdoutPath ` -RedirectStandardError $stderrPath ` -WindowStyle Hidden ` -PassThru +# Capture the handle while the process is still alive. Reading it after exit +# can yield null, and the exit-code helper needs a real handle. +$processHandle = $process.Handle + try { if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { throw "Redirected CLI action did not exit within ${TimeoutSeconds}s. A dialog or hung child likely blocked completion." } - $exitCode = Get-InteractiveWin11ProcessExitCode -Process $process -ProcessHandle $process.Handle + $exitCode = Get-InteractiveWin11ProcessExitCode -Process $process -ProcessHandle $processHandle if ($exitCode -ne 0) { throw "Redirected CLI action should exit with code 0, got $exitCode." } diff --git a/test/windows/flagship/README.md b/test/windows/flagship/README.md index c6629d4a39e..25e7bfe0727 100644 --- a/test/windows/flagship/README.md +++ b/test/windows/flagship/README.md @@ -8,13 +8,63 @@ Versioned, machine-readable contracts for the flagship runtime rewrite. - `scenarios/`: workflow-runnable scenario definitions. - `examples/`: format examples; hashes are placeholders, not frozen evidence. - `baselines/`: immutable manifests tied to a base commit and verified hashes. +- `Test-VerificationContracts.ps1`: stable entry point and aggregate-failure + runner. +- `contracts/ContractHelpers.ps1`: shared assertions, AST helpers, and the + table-contract failure collector. +- `contracts/Contracts.*.ps1`: ordered per-target contract fragments. A thrown + code assertion stops only its fragment; table rows continue and report every + failed row. Validate the contracts: ```powershell -pwsh -File test/windows/flagship/Test-VerificationContracts.ps1 +pwsh -NoProfile -File test/windows/flagship/Test-VerificationContracts.ps1 +./test/windows/flagship/Test-VerificationContracts.ps1 ``` +Both forms resolve fragments relative to the entry point, independent of the +caller's current directory. + +## Regenerating protected workflow digests + +`contracts/Contracts.80-Release.ps1` freezes eight canonical SHA-256 values: +the full release and readiness workflows, three protected step envelopes, and +their three literal `run` bodies. A legitimate workflow edit requires semantic +review of the full file and protected steps before replacing any digest. + +From the repository root, calculate the candidate values with the same +canonicalization and extraction helpers used by the contracts: + +```powershell +$repoRoot = (Resolve-Path '.').Path +. (Join-Path $repoRoot 'test/windows/flagship/contracts/ContractHelpers.ps1') + +$releasePath = Join-Path $repoRoot '.github/workflows/release.yml' +$readinessPath = Join-Path $repoRoot '.github/workflows/release-readiness.yml' +$release = Get-Content -LiteralPath $releasePath -Raw +$readiness = Get-Content -LiteralPath $readinessPath -Raw +$releasePreflight = Get-YamlStepBlock -Content $release -Name 'Release preflight' -Source $releasePath +$readinessPreflight = Get-YamlStepBlock -Content $readiness -Name 'Validate release configuration' -Source $readinessPath +$interactiveEvidence = Get-YamlStepBlock -Content $release -Name 'Require successful Test workflow for release SHA' -Source $releasePath + +$pins = [ordered]@{ + releasePreflightScriptSha256 = (Get-CanonicalTextSha256 -Text (Get-YamlLiteralRunScript -Content $releasePreflight -Source 'Release preflight')) + readinessPreflightScriptSha256 = (Get-CanonicalTextSha256 -Text (Get-YamlLiteralRunScript -Content $readinessPreflight -Source 'Validate release configuration')) + releaseInteractiveEvidenceScriptSha256 = (Get-CanonicalTextSha256 -Text (Get-YamlLiteralRunScript -Content $interactiveEvidence -Source 'Require successful Test workflow for release SHA')) + releaseInteractiveEvidenceStepSha256 = (Get-CanonicalTextSha256 -Text $interactiveEvidence) + releasePreflightStepSha256 = (Get-CanonicalTextSha256 -Text $releasePreflight) + readinessPreflightStepSha256 = (Get-CanonicalTextSha256 -Text $readinessPreflight) + releaseWorkflowSha256 = (Get-CanonicalTextSha256 -Text $release) + readinessWorkflowSha256 = (Get-CanonicalTextSha256 -Text $readiness) +} +$pins.GetEnumerator() | ForEach-Object { '{0} = {1}' -f $_.Key, $_.Value } +``` + +Update only the corresponding values near the top of +`Contracts.80-Release.ps1`, then rerun the contract entry point. The mutation +tests must remain unchanged and green. + Hosted Windows CI runs the x64 portable smoke through `Invoke-PortableSmoke.ps1`. The full composite runs only on opted-in manual dispatches (`run_interactive_win11=true`) on the unlocked diff --git a/test/windows/flagship/Test-VerificationContracts.ps1 b/test/windows/flagship/Test-VerificationContracts.ps1 index d2c0fa4d88a..6f95726fd5d 100644 --- a/test/windows/flagship/Test-VerificationContracts.ps1 +++ b/test/windows/flagship/Test-VerificationContracts.ps1 @@ -6,9296 +6,57 @@ param() $ErrorActionPreference = 'Stop' $root = $PSScriptRoot $repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $root)) +$contractsRoot = Join-Path $root 'contracts' -function Assert-JsonDocument { - param( - [Parameter(Mandatory)] [string] $Path, - [Parameter(Mandatory)] [string] $SchemaPath - ) - - $json = Get-Content -LiteralPath $Path -Raw - if (Get-Command Test-Json -ErrorAction SilentlyContinue) { - if (-not ($json | Test-Json -SchemaFile $SchemaPath -ErrorAction Stop)) { - throw "JSON contract validation failed: $Path" - } - return - } - - try { - Add-Type -AssemblyName System.Web.Extensions - $parser = New-Object System.Web.Script.Serialization.JavaScriptSerializer - $parser.MaxJsonLength = [int]::MaxValue - [void]$parser.DeserializeObject($json) - [void]$parser.DeserializeObject((Get-Content -LiteralPath $SchemaPath -Raw)) - } catch { - throw "JSON syntax validation failed: $Path ($($_.Exception.Message))" - } -} - -function Assert-PassArtifactsBound { - param([Parameter(Mandatory)] [string] $Path) - - $result = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($result.status -eq 'pass' -and @($result.artifacts | Where-Object { -not $_.sha256 }).Count -ne 0) { - throw "Passing verification result has an unbound artifact hash: $Path" - } -} - -function Assert-WorkflowContract { - param( - [Parameter(Mandatory)] [string] $Path, - [Parameter(Mandatory)] [string] $Pattern, - [Parameter(Mandatory)] [string] $Description - ) - - $content = Get-Content -LiteralPath $Path -Raw - if ($content -notmatch $Pattern) { - throw "Workflow contract missing: $Description ($Path)" - } -} - -function Assert-WorkflowContractAbsent { - param( - [Parameter(Mandatory)] [string] $Path, - [Parameter(Mandatory)] [string] $Pattern, - [Parameter(Mandatory)] [string] $Description - ) - - $content = Get-Content -LiteralPath $Path -Raw - if ($content -match $Pattern) { - throw "Workflow contract forbidden: $Description ($Path)" - } -} - -function Assert-TextContract { - param( - [Parameter(Mandatory)] [string] $Content, - [Parameter(Mandatory)] [string] $Pattern, - [Parameter(Mandatory)] [string] $Description, - [Parameter(Mandatory)] [string] $Context - ) - - if ($Content -notmatch $Pattern) { throw "Contract missing: $Description ($Context)" } -} - -function Assert-ZigTestsDiscoveredAndRun { - param( - [Parameter(Mandatory)] [string] $RepoRoot, - [Parameter(Mandatory)] [hashtable] $Sources, - [Parameter(Mandatory)] [string[]] $ExpectedNames, - [Parameter(Mandatory)] [string] $Filter - ) - - $declarations = [Collections.Generic.List[object]]::new() - foreach ($entry in $Sources.GetEnumerator()) { - foreach ($match in [regex]::Matches( - $entry.Value, - '(?m)^test "(?[^"\r\n]+)" \{$' - )) { - [void]$declarations.Add([pscustomobject]@{ - Path = $entry.Key - Name = $match.Groups['name'].Value - }) - } - } - foreach ($expectedName in $ExpectedNames) { - $declMatches = @($declarations | Where-Object { $_.Name -ceq $expectedName }) - if ($declMatches.Count -ne 1) { - throw "Expected exactly one Zig test declaration '$expectedName'; found $($declMatches.Count)." - } - } - - Invoke-ZigFixture ` - -RepoRoot $RepoRoot ` - -Filter $Filter -} - -function Get-YamlJobText { - param( - [Parameter(Mandatory)] [string] $Content, - [Parameter(Mandatory)] [string] $Name, - [Parameter(Mandatory)] [string] $Source - ) - - $pattern = '(?ms)^ ' + [regex]::Escape($Name) + ':\s*(?:#.*)?\r?\n.*?(?=^ \S[^\r\n]*:\s*(?:#.*)?$|\z)' - $match = [regex]::Match($Content, $pattern) - if (-not $match.Success) { throw "Workflow job not found: $Name ($Source)" } - $match.Value -} - -function Get-YamlStepBlock { - param( - [Parameter(Mandatory)] [string] $Content, - [Parameter(Mandatory)] [string] $Name, - [Parameter(Mandatory)] [string] $Source - ) - - $pattern = '(?ms)^ - name:[ \t]+' + [regex]::Escape($Name) + '[ \t]*\r?\n.*?(?=^ -[ \t]+|^ \S[^\r\n]*:\s*(?:#.*)?$|^ \S[^\r\n]*:\s*(?:#.*)?$|\z)' - $matches = [regex]::Matches($Content, $pattern) - if ($matches.Count -ne 1) { - throw "Expected exactly one workflow step '$Name'; found $($matches.Count): $Source" - } - $matches[0].Value -} - -function Assert-DeferredZigFixtureExecution { - param( - [Parameter(Mandatory)] [string] $WorkflowText, - [Parameter(Mandatory)] [string] $Source - ) - - $windowsJob = Get-YamlJobText ` - -Content $WorkflowText ` - -Name 'windows' ` - -Source $Source - $flagshipStep = Get-YamlStepBlock ` - -Content $windowsJob ` - -Name 'Flagship verification contract checks' ` - -Source "$Source :: windows" - $setupStep = Get-YamlStepBlock ` - -Content $windowsJob ` - -Name 'Setup Zig' ` - -Source "$Source :: windows" - $fullSuiteStep = Get-YamlStepBlock ` - -Content $windowsJob ` - -Name 'Full Zig test suite' ` - -Source "$Source :: windows" - $conditionalStepPattern = '(?m)^ (?:if|continue-on-error):' - $flagshipIndex = $windowsJob.IndexOf($flagshipStep, [StringComparison]::Ordinal) - $setupIndex = $windowsJob.IndexOf($setupStep, [StringComparison]::Ordinal) - $fullSuiteIndex = $windowsJob.IndexOf($fullSuiteStep, [StringComparison]::Ordinal) - - if ($flagshipStep -notmatch - '(?m)^ run: \./test/windows/flagship/Test-VerificationContracts\.ps1\s*$' -or - $setupStep -notmatch - '(?m)^ uses: mlugg/setup-zig@[^\s]+(?:\s+#.*)?\s*$' -or - $setupStep -match $conditionalStepPattern -or - $fullSuiteStep -notmatch - '(?m)^ run: zig build test -Demit-test-exe=true\s*$' -or - $fullSuiteStep -match $conditionalStepPattern -or - $flagshipIndex -lt 0 -or - $setupIndex -le $flagshipIndex -or - $fullSuiteIndex -le $setupIndex) { - throw 'The early flagship contract must defer unavailable fixture execution to the same Windows job''s mandatory Setup Zig and full Zig test suite.' - } -} - -function Test-FlagshipZigFixtureExecutionEnabled { - return $env:GITHUB_ACTIONS -ne 'true' -} - -function Invoke-ZigFixture { - param( - [Parameter(Mandatory)] [string] $RepoRoot, - [Parameter(Mandatory)] [string] $Filter - ) - - if (-not (Test-FlagshipZigFixtureExecutionEnabled)) { - Write-Host "ZIG-FIXTURE-EXECUTION-SKIPPED [$Filter]: deferred to mandatory full Zig suite in the same Windows job." - return - } - $filterArgument = "-Dtest-filter=$Filter" - & (Join-Path $RepoRoot 'scripts\dev-windows.cmd') ` - zig build test $filterArgument - if ($LASTEXITCODE -ne 0) { - throw "Zig semantic fixture '$Filter' failed with exit code $LASTEXITCODE." - } -} - -function Get-YamlLiteralRunScript { - param( - [Parameter(Mandatory)] [string] $Content, - [Parameter(Mandatory)] [string] $Source - ) - - $match = [regex]::Match($Content, '(?ms)^ run:[ \t]*\|[ \t]*\r?\n(?.*)\z') - if (-not $match.Success) { throw "Literal workflow run block not found: $Source" } - $scriptLines = @( - foreach ($line in ($match.Groups['body'].Value -split '\r?\n')) { - if ($line.Length -eq 0) { ''; continue } - if (-not $line.StartsWith(' ', [StringComparison]::Ordinal)) { - throw "Literal workflow run block has unexpected indentation: $Source" - } - $line.Substring(10) - } - ) - ($scriptLines -join "`n").TrimEnd([char[]]"`n") -} +. (Join-Path $contractsRoot 'ContractHelpers.ps1') +Initialize-ContractFailureCollection +$script:FlagshipScenarioIds = $null -function ConvertTo-CanonicalText { - param([Parameter(Mandatory)] [AllowEmptyString()] [string] $Text) - - (($Text -replace "`r`n", "`n") -replace "`r", "`n"). - TrimEnd([char[]]"`n") +$fragments = @(Get-ChildItem -LiteralPath $contractsRoot -Filter 'Contracts.*.ps1' | + Sort-Object Name) +if ($fragments.Count -eq 0) { + Add-ContractFailure ` + -Description 'No verification contract fragments were found.' ` + -SourceFragment 'Test-VerificationContracts.ps1' } -function Get-CanonicalTextSha256 { - param([Parameter(Mandatory)] [AllowEmptyString()] [string] $Text) - - $normalized = ConvertTo-CanonicalText -Text $Text - $sha256 = [Security.Cryptography.SHA256]::Create() +foreach ($fragment in $fragments) { + $script:CurrentContractFragment = $fragment.Name try { - $bytes = [Text.UTF8Encoding]::new($false).GetBytes($normalized) - return (([BitConverter]::ToString($sha256.ComputeHash($bytes))) -replace - '-', '').ToLowerInvariant() - } - finally { - $sha256.Dispose() - } -} - -function Test-YamlStepEnvelopeDigest { - param( - [Parameter(Mandatory)] [string] $StepText, - [Parameter(Mandatory)] [string] $ExpectedSha256 - ) - - (Get-CanonicalTextSha256 -Text $StepText) -ceq $ExpectedSha256 -} - -function Get-NamedVariableAssignments { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [string] $Name - ) - - return @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.Left.VariablePath.UserPath -split ':')[-1]) -ceq $Name - }, $true)) -} - -function Test-ExactVariableUseSet { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [string] $Name, - [Parameter(Mandatory)] - [System.Management.Automation.Language.Ast[]] $ExpectedContainers - ) - - $actualUses = @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.VariablePath.UserPath -split ':')[-1]) -ceq $Name - }, $true)) - $expectedUses = @( - foreach ($container in $ExpectedContainers) { - $container.FindAll({ - param($node) - $node -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.VariablePath.UserPath -split ':')[-1]) -ceq $Name - }, $true) - } - ) - if ($actualUses.Count -ne $expectedUses.Count) { return $false } - foreach ($expected in $expectedUses) { - if (@($actualUses | Where-Object { - [object]::ReferenceEquals($_, $expected) - }).Count -ne 1) { - return $false - } - } - return $true -} - -function Test-NamedReleasePreflightSplat { - param( - [Parameter(Mandatory)] [string] $ScriptText, - [Parameter(Mandatory)] [hashtable] $ExpectedExpressions, - [Parameter(Mandatory)] [string] $ExpectedScriptSha256 - ) - - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput( - $ScriptText, - [ref]$tokens, - [ref]$errors - ) - if ($errors.Count -ne 0) { return $false } - if (@($ast.FindAll({ - param($node) - Test-ForbiddenScriptMutationNode ` - -Node $node ` - -StrictReleaseContract - }, $true)).Count -ne 0) { - return $false - } - if (@($ast.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.CommandAst]) { - return $false - } - $name = $node.GetCommandName() - return $null -ne $name -and - ($name -split '\\')[-1] -in @( - 'New-Item', 'ni', - 'Get-ChildItem', 'dir', 'gci', 'ls', - 'Get-Content', 'cat', 'gc', 'type' - ) - }, $true)).Count -ne 0) { - return $false - } - - $commands = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - (($node.GetCommandName() -replace '\\', '/') -eq - './scripts/release-preflight.ps1') - }, $true)) - if ($commands.Count -ne 1 -or $commands[0].CommandElements.Count -ne 2) { - return $false - } - $splat = $commands[0].CommandElements[1] - if ($splat -isnot - [System.Management.Automation.Language.VariableExpressionAst] -or - -not $splat.Splatted) { - return $false - } - $splatName = ($splat.VariablePath.UserPath -split ':')[-1] - - $assignments = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.Left.VariablePath.UserPath -split ':')[-1]) -eq $splatName - }, $true)) - if ($assignments.Count -ne 1 -or - $assignments[0].Right -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - $assignments[0].Right.Expression -isnot - [System.Management.Automation.Language.HashtableAst]) { - return $false + . $fragment.FullName } - - $actualExpressions = @{} - foreach ($pair in $assignments[0].Right.Expression.KeyValuePairs) { - if ($pair.Item1 -isnot - [System.Management.Automation.Language.StringConstantExpressionAst]) { - return $false - } - $key = [string]$pair.Item1.Value - if ($actualExpressions.ContainsKey($key)) { return $false } - $actualExpressions[$key] = $pair.Item2.Extent.Text.Trim() - } - if ($actualExpressions.Count -ne $ExpectedExpressions.Count) { - return $false - } - foreach ($key in $ExpectedExpressions.Keys) { - if (-not $actualExpressions.ContainsKey($key) -or - $actualExpressions[$key] -cne $ExpectedExpressions[$key]) { - return $false + catch { + $detail = $_.Exception.Message + if (-not [string]::IsNullOrWhiteSpace($_.InvocationInfo.PositionMessage)) { + $detail += "`n$($_.InvocationInfo.PositionMessage.Trim())" } + Add-ContractFailure ` + -Description $_.Exception.Message ` + -SourceFragment $fragment.Name ` + -Detail $detail } - - $variableUses = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.VariablePath.UserPath -split ':')[-1]) -eq $splatName - }, $true)) - $assignmentUses = @($variableUses | Where-Object { - [object]::ReferenceEquals($_, $assignments[0].Left) - }) - $invocationUses = @($variableUses | Where-Object { - [object]::ReferenceEquals($_, $splat) - }) - $allSplats = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.VariableExpressionAst] -and - $node.Splatted - }, $true)) - return $variableUses.Count -eq 2 -and - $assignmentUses.Count -eq 1 -and - $invocationUses.Count -eq 1 -and - $allSplats.Count -eq 1 -and - [object]::ReferenceEquals($allSplats[0], $splat) -and - (Get-CanonicalTextSha256 -Text $ScriptText) -ceq - $ExpectedScriptSha256 -} - -function Assert-NamedReleasePreflightSplat { - param( - [Parameter(Mandatory)] [string] $StepText, - [Parameter(Mandatory)] [hashtable] $ExpectedExpressions, - [Parameter(Mandatory)] [string] $ExpectedScriptSha256, - [Parameter(Mandatory)] [string] $Context - ) - - $scriptText = Get-YamlLiteralRunScript ` - -Content $StepText ` - -Source $Context - if (-not (Test-NamedReleasePreflightSplat ` - -ScriptText $scriptText ` - -ExpectedExpressions $ExpectedExpressions ` - -ExpectedScriptSha256 $ExpectedScriptSha256)) { - throw "Release preflight workflow must use one immutable exact named splat: $Context" - } -} - -function Test-ReleaseInteractiveResultSelectionContract { - param([Parameter(Mandatory)] [string] $ScriptText) - - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput( - $ScriptText, - [ref]$tokens, - [ref]$errors - ) - if ($errors.Count -ne 0) { return $false } - - $runLoops = @( - $ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ForEachStatementAst] -and - $node.Variable.Extent.Text -ceq '$run' -and - $node.Condition.Extent.Text.Trim() -ceq '$matching' - }, $true) | - Where-Object { - Test-DirectNamedBlockChild ` - -Node $_ ` - -NamedBlock $ast.EndBlock - } - ) - if ($runLoops.Count -ne 1) { return $false } - $runBody = $runLoops[0].Body - - $resultFileAssignments = @( - $runBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left.Extent.Text -ceq '$resultFiles' - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody - } - ) - if ($resultFileAssignments.Count -ne 1 -or - $resultFileAssignments[0].Right -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - $resultFileAssignments[0].Right.Expression -isnot - [System.Management.Automation.Language.ArrayExpressionAst]) { - return $false - } - $resultFileAssignment = $resultFileAssignments[0] - $getChildCommands = @($resultFileAssignment.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Get-ChildItem' - }, $true)) - $whereCommands = @($resultFileAssignment.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Where-Object' - }, $true)) - if ($getChildCommands.Count -ne 1 -or $whereCommands.Count -ne 1 -or - -not [object]::ReferenceEquals( - $getChildCommands[0].Parent, - $whereCommands[0].Parent - ) -or - $getChildCommands[0].Parent -isnot - [System.Management.Automation.Language.PipelineAst] -or - $getChildCommands[0].Parent.PipelineElements.Count -ne 2 -or - -not [object]::ReferenceEquals( - $getChildCommands[0].Parent.PipelineElements[0], - $getChildCommands[0] - ) -or - -not [object]::ReferenceEquals( - $getChildCommands[0].Parent.PipelineElements[1], - $whereCommands[0] - )) { - return $false - } - $getChildElements = @( - $getChildCommands[0].CommandElements | - ForEach-Object { $_.Extent.Text.Trim() } - ) - if (-not (Test-CommandArgumentPair ` - -Elements $getChildElements ` - -Name '-LiteralPath' ` - -Value '$artifactRoot') -or - -not (Test-CommandArgumentPair ` - -Elements $getChildElements ` - -Name '-Filter' ` - -Value 'result.json') -or - @($getChildElements | Where-Object { $_ -ceq '-File' }).Count -ne 1 -or - @($getChildElements | Where-Object { $_ -ceq '-Recurse' }).Count -ne 1 -or - $whereCommands[0].CommandElements.Count -ne 2 -or - $whereCommands[0].CommandElements[1] -isnot - [System.Management.Automation.Language.ScriptBlockExpressionAst]) { - return $false - } - - $filterBlock = $whereCommands[0].CommandElements[1].ScriptBlock - $filterStatements = @($filterBlock.EndBlock.Statements) - if ($filterStatements.Count -ne 1 -or - $filterStatements[0] -isnot - [System.Management.Automation.Language.TryStatementAst]) { - return $false - } - $filterTry = $filterStatements[0] - if ($filterTry.Body.Statements.Count -ne 1 -or - $filterTry.CatchClauses.Count -ne 1 -or - $null -ne $filterTry.Finally -or - $filterTry.CatchClauses[0].Body.Statements.Count -ne 1) { - return $false - } - $filterExpression = Get-SinglePipelineExpression ` - -Node $filterTry.Body.Statements[0] - $catchExpression = Get-SinglePipelineExpression ` - -Node $filterTry.CatchClauses[0].Body.Statements[0] - if ($null -eq $filterExpression -or - (($filterExpression.Extent.Text -replace '\s+', ' ').Trim()) -cne - '(Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json).scenario_id -eq ''windows.interactive-win11.composite''' -or - $null -eq $catchExpression -or - $catchExpression.Extent.Text.Trim() -cne '$false') { - return $false - } - - $countGates = @( - $runBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -and - (($node.Extent.Text -replace '\s+', ' ').Trim()) -ceq - 'if ($resultFiles.Count -ne 1) { continue }' - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody - } - ) - $resultPathAssignments = @( - $runBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left.Extent.Text -ceq '$resultPath' -and - $node.Right.Extent.Text.Trim() -ceq '$resultFiles[0].FullName' - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody - } - ) - $resultAssignments = @( - $runBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left.Extent.Text -ceq '$result' -and - (($node.Right.Extent.Text -replace '\s+', ' ').Trim()) -ceq - 'Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json' - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody - } - ) - $arbitraryFirst = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Select-Object' -and - @($node.CommandElements | Where-Object { - $_.Extent.Text -ceq '-First' - }).Count -gt 0 - }, $true)) - return $countGates.Count -eq 1 -and - $resultPathAssignments.Count -eq 1 -and - $resultAssignments.Count -eq 1 -and - $arbitraryFirst.Count -eq 0 -and - $resultFileAssignment.Extent.EndOffset -lt - $countGates[0].Extent.StartOffset -and - $countGates[0].Extent.EndOffset -lt - $resultPathAssignments[0].Extent.StartOffset -and - $resultPathAssignments[0].Extent.EndOffset -lt - $resultAssignments[0].Extent.StartOffset -} - -function Get-LogicalAndLeaves { - param([Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node) - - if ($Node -is [System.Management.Automation.Language.BinaryExpressionAst] -and - $Node.Operator -eq [System.Management.Automation.Language.TokenKind]::And) { - Get-LogicalAndLeaves -Node $Node.Left - Get-LogicalAndLeaves -Node $Node.Right - return - } - return ,$Node -} - -function Get-SinglePipelineExpression { - param([System.Management.Automation.Language.Ast] $Node) - - if ($Node -isnot [System.Management.Automation.Language.PipelineAst] -or - $Node.PipelineElements.Count -ne 1 -or - $Node.PipelineElements[0] -isnot - [System.Management.Automation.Language.CommandExpressionAst]) { - return $null - } - return $Node.PipelineElements[0].Expression -} - -function Test-ExactExpressionSet { - param( - [System.Management.Automation.Language.Ast] $Root, - [Parameter(Mandatory)] [string[]] $Expected - ) - - if ($null -eq $Root) { return $false } - $actual = @( - Get-LogicalAndLeaves -Node $Root | - ForEach-Object { ($_.Extent.Text -replace '\s+', ' ').Trim() } - ) - return $actual.Count -eq $Expected.Count -and - @($Expected | Where-Object { $actual -cnotcontains $_ }).Count -eq 0 } -function Test-CommandArgumentPair { - param( - [Parameter(Mandatory)] [string[]] $Elements, - [Parameter(Mandatory)] [string] $Name, - [Parameter(Mandatory)] [string] $Value - ) - - $matchCount = 0 - for ($index = 0; $index -lt ($Elements.Count - 1); $index++) { - if ($Elements[$index] -ceq $Name -and - $Elements[$index + 1] -ceq $Value) { - $matchCount++ - } - } - return $matchCount -eq 1 -and - @($Elements | Where-Object { $_ -ceq $Name }).Count -eq 1 +if ($null -eq $script:FlagshipScenarioIds) { + Add-ContractFailure ` + -Description 'Foundation contract did not publish FlagshipScenarioIds.' ` + -SourceFragment 'Test-VerificationContracts.ps1' } -function Test-ReleaseInteractiveSuccessPredicates { - param( - [Parameter(Mandatory)] [string] $ScriptText, - [Parameter(Mandatory)] [string] $ExpectedScriptSha256 - ) - - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput( - $ScriptText, - [ref]$tokens, - [ref]$errors - ) - if ($errors.Count -ne 0) { return $false } - if (@($ast.FindAll({ - param($node) - Test-ForbiddenScriptMutationNode ` - -Node $node ` - -StrictReleaseContract - }, $true)).Count -ne 0) { - return $false - } - if (@($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.VariableExpressionAst] -and - $node.Splatted - }, $true)).Count -ne 0) { - return $false - } - - $shaAssignments = @(Get-NamedVariableAssignments -Ast $ast -Name 'sha') - if ($shaAssignments.Count -ne 1 -or - -not (Test-DirectNamedBlockChild ` - -Node $shaAssignments[0] ` - -NamedBlock $ast.EndBlock) -or - (($shaAssignments[0].Right.Extent.Text -replace '\s+', ' ').Trim()) -cne - '(git rev-parse HEAD).Trim()') { - return $false - } - $shaAssignment = $shaAssignments[0] - - $runListCommands = @($ast.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.CommandAst] -or - $node.GetCommandName() -ne 'gh' -or - $node.CommandElements.Count -lt 3) { - return $false - } - return $node.CommandElements[1].Extent.Text -ceq 'run' -and - $node.CommandElements[2].Extent.Text -ceq 'list' - }, $true)) - if ($runListCommands.Count -ne 1) { return $false } - $runList = $runListCommands[0] - $runListAssignment = $runList - while ($null -ne $runListAssignment -and - $runListAssignment -isnot - [System.Management.Automation.Language.AssignmentStatementAst]) { - $runListAssignment = $runListAssignment.Parent - } - if ($null -eq $runListAssignment -or - $runListAssignment.Left -isnot - [System.Management.Automation.Language.VariableExpressionAst] -or - (($runListAssignment.Left.VariablePath.UserPath -split ':')[-1]) -cne - 'runsJson' -or - -not (Test-DirectNamedBlockChild ` - -Node $runListAssignment ` - -NamedBlock $ast.EndBlock)) { - return $false - } - $runsJsonAssignments = @( - $ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.Left.VariablePath.UserPath -split ':')[-1]) -ceq - 'runsJson' - }, $true) - ) - if ($runsJsonAssignments.Count -ne 1 -or - -not [object]::ReferenceEquals( - $runsJsonAssignments[0], - $runListAssignment - )) { - return $false - } - $runListElements = @( - $runList.CommandElements | - ForEach-Object { $_.Extent.Text.Trim() } - ) - foreach ($pair in @( - @('--workflow', 'Test'), - @('--commit', '$sha'), - @('--status', 'success') - )) { - if (-not (Test-CommandArgumentPair ` - -Elements $runListElements ` - -Name $pair[0] ` - -Value $pair[1])) { - return $false - } - } - - $runsAssignments = @( - $ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.Left.VariablePath.UserPath -split ':')[-1]) -ceq 'runs' - }, $true) - ) - if ($runsAssignments.Count -ne 1 -or - -not (Test-DirectNamedBlockChild ` - -Node $runsAssignments[0] ` - -NamedBlock $ast.EndBlock) -or - $runsAssignments[0].Right -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - $runsAssignments[0].Right.Expression -isnot - [System.Management.Automation.Language.ArrayExpressionAst]) { - return $false - } - $runsAssignment = $runsAssignments[0] - $runsStatements = @( - $runsAssignment.Right.Expression.SubExpression.Statements - ) - if ($runsStatements.Count -ne 1 -or - $runsStatements[0] -isnot - [System.Management.Automation.Language.PipelineAst]) { - return $false - } - $runsPipeline = $runsStatements[0] - if ($runsPipeline.PipelineElements.Count -ne 2 -or - $runsPipeline.PipelineElements[0] -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - $runsPipeline.PipelineElements[0].Expression -isnot - [System.Management.Automation.Language.VariableExpressionAst] -or - $runsPipeline.PipelineElements[0].Expression.Extent.Text -cne - '$runsJson' -or - $runsPipeline.PipelineElements[1] -isnot - [System.Management.Automation.Language.CommandAst] -or - $runsPipeline.PipelineElements[1].GetCommandName() -ne - 'ConvertFrom-Json' -or - $runsPipeline.PipelineElements[1].CommandElements.Count -ne 1) { - return $false - } - - $matchingAssignments = @( - $ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.Left.VariablePath.UserPath -split ':')[-1]) -ceq - 'matching' - }, $true) - ) - if ($matchingAssignments.Count -ne 1 -or - -not (Test-DirectNamedBlockChild ` - -Node $matchingAssignments[0] ` - -NamedBlock $ast.EndBlock) -or - $matchingAssignments[0].Right -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - $matchingAssignments[0].Right.Expression -isnot - [System.Management.Automation.Language.ArrayExpressionAst]) { - return $false - } - $matchingAssignment = $matchingAssignments[0] - $matchingStatements = @( - $matchingAssignment.Right.Expression.SubExpression.Statements - ) - if ($matchingStatements.Count -ne 1 -or - $matchingStatements[0] -isnot - [System.Management.Automation.Language.PipelineAst]) { - return $false - } - $matchingPipeline = $matchingStatements[0] - $whereCommands = @($matchingAssignment.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Where-Object' - }, $true)) - if ($whereCommands.Count -ne 1 -or - $matchingPipeline.PipelineElements.Count -ne 2 -or - $matchingPipeline.PipelineElements[0] -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - $matchingPipeline.PipelineElements[0].Expression -isnot - [System.Management.Automation.Language.VariableExpressionAst] -or - $matchingPipeline.PipelineElements[0].Expression.Extent.Text -cne - '$runs' -or - -not [object]::ReferenceEquals( - $matchingPipeline.PipelineElements[1], - $whereCommands[0] - ) -or - -not [object]::ReferenceEquals( - $whereCommands[0].Parent, - $matchingPipeline - ) -or - $whereCommands[0].CommandElements.Count -ne 2 -or - $whereCommands[0].CommandElements[1] -isnot - [System.Management.Automation.Language.ScriptBlockExpressionAst]) { - return $false - } - if ($runListAssignment.Extent.EndOffset -ge - $runsAssignment.Extent.StartOffset -or - $runsAssignment.Extent.EndOffset -ge - $matchingAssignment.Extent.StartOffset) { - return $false - } - $runListToRunsStatements = @( - $ast.EndBlock.Statements | - Where-Object { - $_.Extent.StartOffset -ge - $runListAssignment.Extent.EndOffset -and - $_.Extent.EndOffset -le - $runsAssignment.Extent.StartOffset - } - ) - $runsToMatchingStatements = @( - $ast.EndBlock.Statements | - Where-Object { - $_.Extent.StartOffset -ge $runsAssignment.Extent.EndOffset -and - $_.Extent.EndOffset -le - $matchingAssignment.Extent.StartOffset - } - ) - if ($runListToRunsStatements.Count -ne 1 -or - $runListToRunsStatements[0] -isnot - [System.Management.Automation.Language.IfStatementAst] -or - $runListToRunsStatements[0].Clauses.Count -ne 1 -or - $null -ne $runListToRunsStatements[0].ElseClause -or - $runListToRunsStatements[0].Clauses[0].Item2.Statements.Count -ne 1 -or - $runListToRunsStatements[0].Clauses[0].Item2.Statements[0] -isnot - [System.Management.Automation.Language.ThrowStatementAst] -or - $runsToMatchingStatements.Count -ne 0) { - return $false - } - $runListFailureCondition = Get-SinglePipelineExpression ` - -Node $runListToRunsStatements[0].Clauses[0].Item1 - if ($null -eq $runListFailureCondition -or - (($runListFailureCondition.Extent.Text -replace '\s+', ' ').Trim()) -cne - '$LASTEXITCODE -ne 0') { - return $false - } - $filterBlock = $whereCommands[0].CommandElements[1].ScriptBlock - $filterStatements = @($filterBlock.EndBlock.Statements) - if ($filterStatements.Count -ne 1 -or - -not (Test-ExactExpressionSet ` - -Root (Get-SinglePipelineExpression -Node $filterStatements[0]) ` - -Expected @( - '$_.headSha -eq $sha', - '$_.status -eq "completed"', - '$_.conclusion -eq "success"' - ))) { - return $false - } - - $matchingCountIfs = @( - $ast.FindAll({ - param($node) - if ($node -isnot - [System.Management.Automation.Language.IfStatementAst] -or - $node.Clauses.Count -ne 1) { - return $false - } - $condition = Get-SinglePipelineExpression ` - -Node $node.Clauses[0].Item1 - return $null -ne $condition -and - (($condition.Extent.Text -replace '\s+', ' ').Trim()) -ceq - '$matching.Count -eq 0' - }, $true) | - Where-Object { - Test-DirectNamedBlockChild ` - -Node $_ ` - -NamedBlock $ast.EndBlock - } - ) - if ($matchingCountIfs.Count -ne 1) { return $false } - $matchingCountUses = @( - $matchingCountIfs[0].Clauses[0].Item1.FindAll({ - param($node) - $node -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.VariablePath.UserPath -split ':')[-1]) -ceq - 'matching' - }, $true) - ) - if ($matchingCountUses.Count -ne 1) { return $false } - - $matchingLoops = @( - $ast.FindAll({ - param($node) - if ($node -isnot - [System.Management.Automation.Language.ForEachStatementAst] -or - $node.Variable.Extent.Text -cne '$run') { - return $false - } - $source = Get-SinglePipelineExpression -Node $node.Condition - return $source -is - [System.Management.Automation.Language.VariableExpressionAst] -and - $source.Extent.Text -ceq '$matching' - }, $true) | - Where-Object { - Test-DirectNamedBlockChild ` - -Node $_ ` - -NamedBlock $ast.EndBlock - } - ) - if ($matchingLoops.Count -ne 1) { return $false } - $runBody = $matchingLoops[0].Body - $matchingLoopSource = Get-SinglePipelineExpression ` - -Node $matchingLoops[0].Condition - $newItemCommands = @($ast.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.CommandAst]) { - return $false - } - $name = $node.GetCommandName() - return $null -ne $name -and - ($name -split '\\')[-1] -in @('New-Item', 'ni') - }, $true)) - if ($newItemCommands.Count -ne 1 -or - $newItemCommands[0].GetCommandName() -cne 'New-Item' -or - (($newItemCommands[0].Extent.Text -replace '\s+', ' ').Trim()) -cne - 'New-Item -ItemType Directory -Force -Path $artifactRoot' -or - -not (Test-DirectStatementBlockChild ` - -Node $newItemCommands[0].Parent ` - -StatementBlock $runBody)) { - return $false - } - - $chainVariableContracts = @( - [pscustomobject]@{ - Name = 'runsJson' - Expected = @( - $runListAssignment.Left, - $runsPipeline.PipelineElements[0].Expression - ) - }, - [pscustomobject]@{ - Name = 'runs' - Expected = @( - $runsAssignment.Left, - $matchingPipeline.PipelineElements[0].Expression - ) - }, - [pscustomobject]@{ - Name = 'matching' - Expected = @( - $matchingAssignment.Left, - $matchingCountUses[0], - $matchingLoopSource - ) - } - ) - foreach ($contract in $chainVariableContracts) { - $uses = @( - $ast.FindAll({ - param($node) - $node -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (($node.VariablePath.UserPath -split ':')[-1]) -ceq - $contract.Name - }, $true) - ) - if ($uses.Count -ne $contract.Expected.Count) { return $false } - foreach ($expected in $contract.Expected) { - if (@($uses | Where-Object { - [object]::ReferenceEquals($_, $expected) - }).Count -ne 1) { - return $false - } - } - } - - $evidenceIfs = @( - $runBody.FindAll({ - param($node) - if ($node -isnot - [System.Management.Automation.Language.IfStatementAst] -or - $node.Clauses.Count -ne 1) { - return $false - } - $body = $node.Clauses[0].Item2 - return @($body.FindAll({ - param($child) - $child -is - [System.Management.Automation.Language.AssignmentStatementAst] -and - $child.Left.Extent.Text -ceq '$evidenceRun' -and - $child.Right.Extent.Text -ceq '$run' - }, $true) | Where-Object { - Test-DirectStatementBlockChild -Node $_ -StatementBlock $body - }).Count -eq 1 - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody - } - ) - if ($evidenceIfs.Count -ne 1) { return $false } - $evidenceIf = $evidenceIfs[0] - if (-not (Test-ExactExpressionSet ` - -Root (Get-SinglePipelineExpression -Node $evidenceIf.Clauses[0].Item1) ` - -Expected @( - '$result.status -eq ''pass''', - '$result.implementation_commit -eq $sha', - '[string]$result.workflow_run_id -eq [string]$run.databaseId', - '$hashesBound' - ))) { - return $false - } - $evidenceBodyStatements = @($evidenceIf.Clauses[0].Item2.Statements) - if ($evidenceBodyStatements.Count -ne 2 -or - $evidenceBodyStatements[0] -isnot - [System.Management.Automation.Language.AssignmentStatementAst] -or - $evidenceBodyStatements[0].Left.Extent.Text -cne '$evidenceRun' -or - $evidenceBodyStatements[0].Right.Extent.Text -cne '$run' -or - $evidenceBodyStatements[1] -isnot - [System.Management.Automation.Language.BreakStatementAst]) { - return $false - } - $evidenceSuccessAssignment = $evidenceBodyStatements[0] - - $resultFilesAssignments = @( - Get-NamedVariableAssignments -Ast $ast -Name 'resultFiles' - ) - $resultPathAssignments = @( - Get-NamedVariableAssignments -Ast $ast -Name 'resultPath' - ) - $resultAssignments = @( - Get-NamedVariableAssignments -Ast $ast -Name 'result' - ) - $resultDirAssignments = @( - Get-NamedVariableAssignments -Ast $ast -Name 'resultDir' - ) - if ($resultFilesAssignments.Count -ne 1 -or - $resultPathAssignments.Count -ne 1 -or - $resultAssignments.Count -ne 1 -or - $resultDirAssignments.Count -ne 1 -or - -not (Test-DirectStatementBlockChild ` - -Node $resultFilesAssignments[0] ` - -StatementBlock $runBody) -or - -not (Test-DirectStatementBlockChild ` - -Node $resultPathAssignments[0] ` - -StatementBlock $runBody) -or - -not (Test-DirectStatementBlockChild ` - -Node $resultAssignments[0] ` - -StatementBlock $runBody) -or - -not (Test-DirectStatementBlockChild ` - -Node $resultDirAssignments[0] ` - -StatementBlock $runBody) -or - $resultPathAssignments[0].Right.Extent.Text.Trim() -cne - '$resultFiles[0].FullName' -or - (($resultAssignments[0].Right.Extent.Text -replace '\s+', ' ').Trim()) -cne - 'Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json' -or - (($resultDirAssignments[0].Right.Extent.Text -replace '\s+', ' ').Trim()) -cne - 'Split-Path -Parent $resultPath') { - return $false - } - $resultFilesAssignment = $resultFilesAssignments[0] - $resultPathAssignment = $resultPathAssignments[0] - $resultAssignment = $resultAssignments[0] - $resultDirAssignment = $resultDirAssignments[0] - $getChildItemCommands = @($ast.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.CommandAst]) { - return $false - } - $name = $node.GetCommandName() - return $null -ne $name -and - ($name -split '\\')[-1] -in @( - 'Get-ChildItem', 'dir', 'gci', 'ls' - ) - }, $true)) - if ($getChildItemCommands.Count -ne 1 -or - $getChildItemCommands[0].GetCommandName() -cne 'Get-ChildItem' -or - (($getChildItemCommands[0].Extent.Text -replace '\s+', ' ').Trim()) -cne - 'Get-ChildItem -LiteralPath $artifactRoot -Filter result.json -File -Recurse' -or - $getChildItemCommands[0].Extent.StartOffset -le - $resultFilesAssignment.Extent.StartOffset -or - $getChildItemCommands[0].Extent.EndOffset -ge - $resultFilesAssignment.Extent.EndOffset) { - return $false - } - $getContentCommands = @($ast.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.CommandAst]) { - return $false - } - $name = $node.GetCommandName() - return $null -ne $name -and - ($name -split '\\')[-1] -in @( - 'Get-Content', 'cat', 'gc', 'type' - ) - }, $true)) - $resultFileContentCommands = @($getContentCommands | Where-Object { - $_.GetCommandName() -ceq 'Get-Content' -and - (($_.Extent.Text -replace '\s+', ' ').Trim()) -ceq - 'Get-Content -LiteralPath $_.FullName -Raw' -and - $_.Extent.StartOffset -gt - $resultFilesAssignment.Extent.StartOffset -and - $_.Extent.EndOffset -lt - $resultFilesAssignment.Extent.EndOffset - }) - $resultContentCommands = @($getContentCommands | Where-Object { - $_.GetCommandName() -ceq 'Get-Content' -and - (($_.Extent.Text -replace '\s+', ' ').Trim()) -ceq - 'Get-Content -LiteralPath $resultPath -Raw' -and - $_.Extent.StartOffset -gt $resultAssignment.Extent.StartOffset -and - $_.Extent.EndOffset -lt $resultAssignment.Extent.EndOffset - }) - if ($getContentCommands.Count -ne 2 -or - $resultFileContentCommands.Count -ne 1 -or - $resultContentCommands.Count -ne 1) { - return $false - } - foreach ($command in @( - $resultFileContentCommands[0], - $resultContentCommands[0] - )) { - if (@($getContentCommands | Where-Object { - [object]::ReferenceEquals($_, $command) - }).Count -ne 1) { - return $false - } - } - - $resultFilesCountGates = @( - $runBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -and - (($node.Extent.Text -replace '\s+', ' ').Trim()) -ceq - 'if ($resultFiles.Count -ne 1) { continue }' - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody - } - ) - $artifactCountGates = @( - $runBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -and - (($node.Extent.Text -replace '\s+', ' ').Trim()) -ceq - 'if (@($result.artifacts).Count -eq 0) { continue }' - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody - } - ) - $artifactLoops = @( - $runBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ForEachStatementAst] -and - $node.Variable.Extent.Text -ceq '$artifact' -and - (($node.Condition.Extent.Text -replace '\s+', ' ').Trim()) -ceq - '@($result.artifacts)' - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody - } - ) - if ($resultFilesCountGates.Count -ne 1 -or - $artifactCountGates.Count -ne 1 -or - $artifactLoops.Count -ne 1 -or - $resultFilesAssignment.Extent.EndOffset -ge - $resultFilesCountGates[0].Extent.StartOffset -or - $resultFilesCountGates[0].Extent.EndOffset -ge - $resultPathAssignment.Extent.StartOffset -or - $resultPathAssignment.Extent.EndOffset -ge - $resultAssignment.Extent.StartOffset -or - $resultAssignment.Extent.EndOffset -ge - $artifactCountGates[0].Extent.StartOffset -or - $artifactCountGates[0].Extent.EndOffset -ge - $artifactLoops[0].Extent.StartOffset) { - return $false - } - $artifactLoop = $artifactLoops[0] - $resultDirJoinCommands = @( - $artifactLoop.Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -ceq 'Join-Path' -and - (($node.Extent.Text -replace '\s+', ' ').Trim()) -ceq - 'Join-Path $resultDir ([string]$artifact.path)' - }, $true) | - Where-Object { - $_.Extent.StartOffset -ge - $artifactLoop.Body.Extent.StartOffset -and - $_.Extent.EndOffset -le - $artifactLoop.Body.Extent.EndOffset - } - ) - if ($resultDirJoinCommands.Count -ne 1) { return $false } - - $hashesBoundAssignments = @( - Get-NamedVariableAssignments -Ast $ast -Name 'hashesBound' - ) - $hashesBoundInitializers = @( - $hashesBoundAssignments | - Where-Object { - $_.Right.Extent.Text -ceq '$true' -and - (Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $runBody) - } - ) - $hashesBoundFailures = @() - foreach ($conditionText in @( - '-not $artifact.sha256 -or -not $artifact.path', - '-not $artifactPath.StartsWith($artifactRootFull, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-Path -LiteralPath $artifactPath -PathType Leaf)', - '$actualHash -ne ([string]$artifact.sha256).ToLowerInvariant()' - )) { - $failureGuards = @( - $artifactLoop.Body.FindAll({ - param($node) - if ($node -isnot - [System.Management.Automation.Language.IfStatementAst] -or - $node.Clauses.Count -ne 1 -or - $null -ne $node.ElseClause) { - return $false - } - $condition = Get-SinglePipelineExpression ` - -Node $node.Clauses[0].Item1 - return $null -ne $condition -and - (($condition.Extent.Text -replace '\s+', ' ').Trim()) -ceq - $conditionText - }, $true) | - Where-Object { - Test-DirectStatementBlockChild ` - -Node $_ ` - -StatementBlock $artifactLoop.Body - } +if ($script:ContractFailures.Count -gt 0) { + foreach ($failure in $script:ContractFailures) { + [Console]::Error.WriteLine( + "CONTRACT FAILURE [$($failure.SourceFragment)] $($failure.Description)" ) - if ($failureGuards.Count -ne 1) { return $false } - $failureStatements = @($failureGuards[0].Clauses[0].Item2.Statements) - if ($failureStatements.Count -ne 2 -or - $failureStatements[0] -isnot - [System.Management.Automation.Language.AssignmentStatementAst] -or - $failureStatements[0].Left.Extent.Text -cne '$hashesBound' -or - $failureStatements[0].Right.Extent.Text -cne '$false' -or - $failureStatements[1] -isnot - [System.Management.Automation.Language.BreakStatementAst]) { - return $false - } - $hashesBoundFailures += $failureStatements[0] - } - if ($hashesBoundAssignments.Count -ne 4 -or - $hashesBoundInitializers.Count -ne 1 -or - $hashesBoundFailures.Count -ne 3) { - return $false - } - foreach ($assignment in @($hashesBoundInitializers) + $hashesBoundFailures) { - if (@($hashesBoundAssignments | Where-Object { - [object]::ReferenceEquals($_, $assignment) - }).Count -ne 1) { - return $false - } - } - - $evidenceRunAssignments = @( - Get-NamedVariableAssignments -Ast $ast -Name 'evidenceRun' - ) - $evidenceInitializers = @( - $evidenceRunAssignments | - Where-Object { - $_.Right.Extent.Text -ceq '$null' -and - (Test-DirectNamedBlockChild ` - -Node $_ ` - -NamedBlock $ast.EndBlock) - } - ) - $evidenceSuccessAssignments = @( - $evidenceRunAssignments | - Where-Object { $_.Right.Extent.Text -ceq '$run' } - ) - if ($evidenceRunAssignments.Count -ne 2 -or - $evidenceInitializers.Count -ne 1 -or - $evidenceSuccessAssignments.Count -ne 1 -or - -not [object]::ReferenceEquals( - $evidenceSuccessAssignments[0], - $evidenceSuccessAssignment - )) { - return $false - } - $evidenceGuards = @( - $ast.FindAll({ - param($node) - if ($node -isnot - [System.Management.Automation.Language.IfStatementAst] -or - $node.Clauses.Count -ne 1) { - return $false - } - $condition = Get-SinglePipelineExpression ` - -Node $node.Clauses[0].Item1 - return $null -ne $condition -and - (($condition.Extent.Text -replace '\s+', ' ').Trim()) -ceq - '-not $evidenceRun' -and - $node.Clauses[0].Item2.Statements.Count -eq 1 -and - $node.Clauses[0].Item2.Statements[0] -is - [System.Management.Automation.Language.ThrowStatementAst] - }, $true) | - Where-Object { - Test-DirectNamedBlockChild ` - -Node $_ ` - -NamedBlock $ast.EndBlock - } - ) - if ($evidenceGuards.Count -ne 1 -or - $evidenceInitializers[0].Extent.EndOffset -ge - $matchingLoops[0].Extent.StartOffset -or - $matchingLoops[0].Extent.EndOffset -ge - $evidenceGuards[0].Extent.StartOffset) { - return $false - } - $evidenceWriteHosts = @( - $ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -ceq 'Write-Host' -and - (($node.Extent.Text -replace '\s+', ' ').Trim()) -ceq - 'Write-Host "Release SHA $sha is covered by interactive Test run $($evidenceRun.databaseId)."' - }, $true) | - Where-Object { - Test-DirectNamedBlockChild ` - -Node $_ ` - -NamedBlock $ast.EndBlock - } - ) - if ($evidenceWriteHosts.Count -ne 1 -or - $evidenceGuards[0].Extent.EndOffset -ge - $evidenceWriteHosts[0].Extent.StartOffset) { - return $false - } - - $criticalVariableContracts = @( - [pscustomobject]@{ - Name = 'sha' - Containers = @( - $shaAssignment, - $runList, - $runListToRunsStatements[0], - $matchingAssignment, - $matchingCountIfs[0], - $evidenceIf, - $evidenceGuards[0], - $evidenceWriteHosts[0] - ) - }, - [pscustomobject]@{ - Name = 'resultFiles' - Containers = @( - $resultFilesAssignment, - $resultFilesCountGates[0], - $resultPathAssignment - ) - }, - [pscustomobject]@{ - Name = 'resultPath' - Containers = @( - $resultPathAssignment, - $resultAssignment, - $resultDirAssignment - ) - }, - [pscustomobject]@{ - Name = 'result' - Containers = @( - $resultAssignment, - $artifactCountGates[0], - $artifactLoop.Condition, - $evidenceIf - ) - }, - [pscustomobject]@{ - Name = 'resultDir' - Containers = @( - $resultDirAssignment, - $resultDirJoinCommands[0] - ) - }, - [pscustomobject]@{ - Name = 'hashesBound' - Containers = @( - $hashesBoundInitializers[0], - $hashesBoundFailures[0], - $hashesBoundFailures[1], - $hashesBoundFailures[2], - $evidenceIf - ) - }, - [pscustomobject]@{ - Name = 'evidenceRun' - Containers = @( - $evidenceInitializers[0], - $evidenceSuccessAssignment, - $evidenceGuards[0], - $evidenceWriteHosts[0] - ) - } - ) - foreach ($contract in $criticalVariableContracts) { - if (-not (Test-ExactVariableUseSet ` - -Ast $ast ` - -Name $contract.Name ` - -ExpectedContainers $contract.Containers)) { - return $false - } - } - return (Get-CanonicalTextSha256 -Text $ScriptText) -ceq - $ExpectedScriptSha256 -} - -function Get-PowerShellBlockText { - param( - [Parameter(Mandatory)] [string] $Content, - [Parameter(Mandatory)] [string] $HeaderPattern - ) - - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput( - $Content, - [ref]$tokens, - [ref]$errors - ) - if ($errors.Count -ne 0) { throw "PowerShell contract source does not parse: $($errors[0].Message)" } - $blocks = @($ast.FindAll({ - param($node) - ($node -is [System.Management.Automation.Language.FunctionDefinitionAst] -or - $node -is [System.Management.Automation.Language.IfStatementAst] -or - $node -is [System.Management.Automation.Language.ForEachStatementAst]) -and - $node.Extent.Text -match $HeaderPattern - }, $true)) - if ($blocks.Count -ne 1) { throw "Expected exactly one PowerShell block for '$HeaderPattern'; found $($blocks.Count)." } - $blocks[0].Extent.Text -} - -function Test-DirectStatementBlockChild { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node, - [Parameter(Mandatory)] [System.Management.Automation.Language.StatementBlockAst] $StatementBlock - ) - - $ancestor = $Node.Parent - while ($null -ne $ancestor -and $ancestor -isnot [System.Management.Automation.Language.StatementBlockAst]) { - if ($ancestor -isnot [System.Management.Automation.Language.PipelineAst] -and - $ancestor -isnot [System.Management.Automation.Language.AssignmentStatementAst] -and - $ancestor -isnot [System.Management.Automation.Language.CommandExpressionAst]) { - return $false + if (-not [string]::IsNullOrWhiteSpace($failure.Detail)) { + [Console]::Error.WriteLine(" $($failure.Detail)") } - $ancestor = $ancestor.Parent - } - [object]::ReferenceEquals($ancestor, $StatementBlock) -} - -function Get-DirectStatementBlockChild { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node, - [Parameter(Mandatory)] [System.Management.Automation.Language.StatementBlockAst] $StatementBlock - ) - - if (-not (Test-DirectStatementBlockChild -Node $Node -StatementBlock $StatementBlock)) { - return $null - } - - $statement = $Node - $ancestor = $Node.Parent - while ($null -ne $ancestor -and $ancestor -isnot [System.Management.Automation.Language.StatementBlockAst]) { - $statement = $ancestor - $ancestor = $ancestor.Parent } - if (-not [object]::ReferenceEquals($ancestor, $StatementBlock)) { return $null } - return $statement -} - -function Test-DirectNamedBlockChild { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node, - [Parameter(Mandatory)] [System.Management.Automation.Language.NamedBlockAst] $NamedBlock + [Console]::Error.WriteLine( + "flagship verification contracts: FAIL ($($script:ContractFailures.Count) failures)" ) - - $ancestor = $Node.Parent - while ($null -ne $ancestor -and - $ancestor -isnot [System.Management.Automation.Language.NamedBlockAst]) { - if ($ancestor -isnot [System.Management.Automation.Language.PipelineAst] -and - $ancestor -isnot - [System.Management.Automation.Language.AssignmentStatementAst] -and - $ancestor -isnot - [System.Management.Automation.Language.CommandExpressionAst]) { - return $false - } - $ancestor = $ancestor.Parent - } - [object]::ReferenceEquals($ancestor, $NamedBlock) -} - -function Get-MemberExpressionName { - param([Parameter(Mandatory)] [System.Management.Automation.Language.MemberExpressionAst] $Node) - - return ([string] $Node.Member.Value).Trim() -} - -function Get-NamedFunctionDefinitions { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [string] $Name - ) - - return @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq $Name - }, $true)) -} - -function Get-NamedCommands { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [string] $Name - ) - - return @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq $Name - }, $true)) -} - -function Get-NamedMemberExpressions { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [string] $Name, - [switch] $InvocationOnly - ) - - return @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.MemberExpressionAst] -and - (-not $InvocationOnly -or - $node -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) -and - (Get-MemberExpressionName -Node $node) -eq $Name - }, $true)) -} - -function Get-ExpressionRootVariableName { - param([Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node) - - while ($Node -is [System.Management.Automation.Language.MemberExpressionAst] -or - $Node -is [System.Management.Automation.Language.ParenExpressionAst]) { - if ($Node -is [System.Management.Automation.Language.MemberExpressionAst]) { - $Node = $Node.Expression - } else { - $pipelineElements = @($Node.Pipeline.PipelineElements) - if ($pipelineElements.Count -ne 1 -or - $pipelineElements[0] -isnot - [System.Management.Automation.Language.CommandExpressionAst]) { - return '' - } - $Node = $pipelineElements[0].Expression - } - } - if ($Node -isnot [System.Management.Automation.Language.VariableExpressionAst]) { - return '' - } - return ($Node.VariablePath.UserPath -split ':')[-1] -} - -function Test-CommandHasStringArgument { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.CommandAst] $Command, - [Parameter(Mandatory)] [string] $Value - ) - - return @($Command.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.StringConstantExpressionAst] -and - $node.Value -eq $Value - }, $true)).Count -gt 0 -} - -function Get-CommandParameterArgument { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.CommandAst] $Command, - [Parameter(Mandatory)] [string] $Name - ) - - $elements = @($Command.CommandElements) - for ($index = 0; $index -lt $elements.Count; $index++) { - $element = $elements[$index] - if ($element -isnot [System.Management.Automation.Language.CommandParameterAst] -or - $element.ParameterName -ne $Name) { - continue - } - if ($null -ne $element.Argument) { return $element.Argument } - if (($index + 1) -lt $elements.Count -and - $elements[$index + 1] -isnot - [System.Management.Automation.Language.CommandParameterAst]) { - return $elements[$index + 1] - } - return $null - } - return $null -} - -function Get-ContainingStatementBlock { - param([Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node) - - while ($null -ne $Node -and - $Node -isnot [System.Management.Automation.Language.StatementBlockAst]) { - $Node = $Node.Parent - } - return $Node -} - -function Get-VariableExpressionName { - param([System.Management.Automation.Language.Ast] $Node) - - if ($Node -isnot [System.Management.Automation.Language.VariableExpressionAst]) { - return '' - } - return ($Node.VariablePath.UserPath -split ':')[-1] -} - -function Test-StaticMemberReference { - param( - [System.Management.Automation.Language.Ast] $Node, - [Parameter(Mandatory)] [string] $TypeName, - [Parameter(Mandatory)] [string] $MemberName - ) - - return $Node -is [System.Management.Automation.Language.MemberExpressionAst] -and - $Node.Static -and - (Get-MemberExpressionName -Node $Node) -eq $MemberName -and - $Node.Expression -is [System.Management.Automation.Language.TypeExpressionAst] -and - $Node.Expression.TypeName.FullName -eq $TypeName -} - -function Test-DirectJoinPathNameExpression { - param( - [System.Management.Automation.Language.Ast] $Node, - [Parameter(Mandatory)] [string] $ParentVariable, - [AllowEmptyString()] [string] $ParentMember = '', - [Parameter(Mandatory)] [string] $ChildVariable, - [Parameter(Mandatory)] [string] $ChildSuffix, - [Parameter(Mandatory)] [ValidateSet('Expandable', 'Format')] [string] $Style - ) - - if ($Node -isnot [System.Management.Automation.Language.PipelineAst] -or - $Node.PipelineElements.Count -ne 1 -or - $Node.PipelineElements[0] -isnot - [System.Management.Automation.Language.CommandAst]) { - return $false - } - $command = $Node.PipelineElements[0] - $elements = @($command.CommandElements) - if ($command.GetCommandName() -ne 'Join-Path' -or $elements.Count -ne 3) { - return $false - } - $parent = $elements[1] - if ([string]::IsNullOrEmpty($ParentMember)) { - if ((Get-VariableExpressionName -Node $parent) -ne $ParentVariable) { - return $false - } - } elseif ($parent -isnot - [System.Management.Automation.Language.MemberExpressionAst] -or - (Get-ExpressionRootVariableName -Node $parent) -ne $ParentVariable -or - (Get-MemberExpressionName -Node $parent) -ne $ParentMember) { - return $false - } - - $child = $elements[2] - if ($Style -eq 'Expandable') { - return $child -is - [System.Management.Automation.Language.ExpandableStringExpressionAst] -and - $child.Value -ceq ('$' + $ChildVariable + $ChildSuffix) -and - $child.NestedExpressions.Count -eq 1 -and - (Get-VariableExpressionName -Node $child.NestedExpressions[0]) -eq - $ChildVariable - } - if ($child -isnot [System.Management.Automation.Language.ParenExpressionAst] -or - $child.Pipeline.PipelineElements.Count -ne 1 -or - $child.Pipeline.PipelineElements[0] -isnot - [System.Management.Automation.Language.CommandExpressionAst]) { - return $false - } - $format = $child.Pipeline.PipelineElements[0].Expression - return $format -is [System.Management.Automation.Language.BinaryExpressionAst] -and - $format.Operator -eq [System.Management.Automation.Language.TokenKind]::Format -and - $format.Left -is - [System.Management.Automation.Language.StringConstantExpressionAst] -and - $format.Left.Value -ceq ('{0}' + $ChildSuffix) -and - (Get-VariableExpressionName -Node $format.Right) -eq $ChildVariable -} - -function Test-InjectiveHarnessRunNameExpression { - param([System.Management.Automation.Language.Ast] $Node) - - if ($Node -isnot [System.Management.Automation.Language.IfStatementAst] -or - $Node.Clauses.Count -ne 1 -or $null -eq $Node.ElseClause) { - return $false - } - $conditionElements = @($Node.Clauses[0].Item1.PipelineElements) - $thenStatements = @($Node.Clauses[0].Item2.Statements) - $elseStatements = @($Node.ElseClause.Statements) - if ($conditionElements.Count -ne 1 -or - $conditionElements[0] -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - $thenStatements.Count -ne 1 -or $elseStatements.Count -ne 1) { - return $false - } - $condition = $conditionElements[0].Expression - if ($condition -isnot - [System.Management.Automation.Language.InvokeMemberExpressionAst] -or - -not $condition.Static -or - $condition.Expression -isnot - [System.Management.Automation.Language.TypeExpressionAst] -or - $condition.Expression.TypeName.FullName -ne 'string' -or - (Get-MemberExpressionName -Node $condition) -ne 'IsNullOrWhiteSpace' -or - $condition.Arguments.Count -ne 1 -or - (Get-VariableExpressionName -Node $condition.Arguments[0]) -ne - 'ScenarioSlug') { - return $false - } - $thenElements = @($thenStatements[0].PipelineElements) - $elseElements = @($elseStatements[0].PipelineElements) - if ($thenElements.Count -ne 1 -or $elseElements.Count -ne 1 -or - $thenElements[0] -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - $elseElements[0] -isnot - [System.Management.Automation.Language.CommandExpressionAst] -or - (Get-VariableExpressionName -Node $thenElements[0].Expression) -ne - 'ScriptName') { - return $false - } - $format = $elseElements[0].Expression - if ($format -isnot [System.Management.Automation.Language.BinaryExpressionAst] -or - $format.Operator -ne [System.Management.Automation.Language.TokenKind]::Format -or - $format.Left -isnot - [System.Management.Automation.Language.StringConstantExpressionAst] -or - $format.Left.Value -cne '{0}-{1}' -or - $format.Right -isnot [System.Management.Automation.Language.ArrayLiteralAst] -or - $format.Right.Elements.Count -ne 2) { - return $false - } - $scriptName = $format.Right.Elements[0] - return $scriptName -is - [System.Management.Automation.Language.InvokeMemberExpressionAst] -and - $scriptName.Static -and - $scriptName.Expression -is - [System.Management.Automation.Language.TypeExpressionAst] -and - $scriptName.Expression.TypeName.FullName -eq 'System.IO.Path' -and - (Get-MemberExpressionName -Node $scriptName) -eq - 'GetFileNameWithoutExtension' -and - $scriptName.Arguments.Count -eq 1 -and - (Get-VariableExpressionName -Node $scriptName.Arguments[0]) -eq - 'ScriptName' -and - (Get-VariableExpressionName -Node $format.Right.Elements[1]) -eq - 'ScenarioSlug' -} - -function Test-TextChangedHandlerOperation { - param( - [System.Management.Automation.Language.Ast] $Node, - [Parameter(Mandatory)] [ValidateSet('Add', 'Remove')] [string] $Operation - ) - - if ($Node -isnot [System.Management.Automation.Language.InvokeMemberExpressionAst] -or - -not $Node.Static -or - $Node.Expression -isnot [System.Management.Automation.Language.TypeExpressionAst] -or - $Node.Expression.TypeName.FullName -ne - 'System.Windows.Automation.Automation' -or - (Get-MemberExpressionName -Node $Node) -ne - "${Operation}AutomationEventHandler") { - return $false - } - $arguments = @($Node.Arguments) - $expectedCount = if ($Operation -eq 'Add') { 4 } else { 3 } - if ($arguments.Count -ne $expectedCount -or - -not (Test-StaticMemberReference ` - -Node $arguments[0] ` - -TypeName 'System.Windows.Automation.TextPattern' ` - -MemberName 'TextChangedEvent') -or - (Get-VariableExpressionName -Node $arguments[1]) -eq '' -or - (Get-VariableExpressionName -Node $arguments[-1]) -eq '') { - return $false - } - if ($Operation -eq 'Add' -and - -not (Test-StaticMemberReference ` - -Node $arguments[2] ` - -TypeName 'System.Windows.Automation.TreeScope' ` - -MemberName 'Element')) { - return $false - } - return $true -} - -function Assert-NoUnreachableStatements { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [string] $Context - ) - - $terminatorTypes = @( - [System.Management.Automation.Language.ReturnStatementAst], - [System.Management.Automation.Language.ThrowStatementAst], - [System.Management.Automation.Language.ExitStatementAst], - [System.Management.Automation.Language.BreakStatementAst], - [System.Management.Automation.Language.ContinueStatementAst] - ) - foreach ($block in @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.StatementBlockAst] - }, $true))) { - $statements = @($block.Statements) - for ($index = 0; $index -lt ($statements.Count - 1); $index++) { - $statementType = $statements[$index].GetType() - if ($terminatorTypes -contains $statementType) { - throw "Unreachable statement after $($statementType.Name): $Context" - } - } - } -} - -function Test-DynamicScriptTypeName { - param([Parameter(Mandatory)] [AllowEmptyString()] [string] $TypeName) - - $typeName = ((($TypeName -split ',', 2)[0] -replace '\s', '')).ToLowerInvariant() - return $typeName -in @( - 'scriptblock', - 'system.management.automation.scriptblock', - 'powershell', - 'system.management.automation.powershell', - 'runspace', - 'runspacefactory', - 'system.management.automation.runspaces.runspace', - 'system.management.automation.runspaces.runspacefactory' - ) -} - -function Test-DynamicScriptTypeExpression { - param([Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node) - - return $Node -is [System.Management.Automation.Language.TypeExpressionAst] -and - (Test-DynamicScriptTypeName -TypeName $Node.TypeName.FullName) -} - -function Test-StrictReleaseForbiddenTypeName { - param([Parameter(Mandatory)] [AllowEmptyString()] [string] $TypeName) - - $typeName = ((($TypeName -split ',', 2)[0] -replace '\s', '')).ToLowerInvariant() - return $typeName -in @( - 'powershell', - 'system.management.automation.powershell', - 'runspace', - 'runspacefactory', - 'initialsessionstate', - 'system.management.automation.runspaces.runspace', - 'system.management.automation.runspaces.runspacefactory', - 'system.management.automation.runspaces.initialsessionstate', - 'microsoft.csharp.csharpcodeprovider', - 'system.codedom.compiler.codedomprovider', - 'system.codedom.compiler.compilerparameters', - 'microsoft.codeanalysis.compilation', - 'microsoft.codeanalysis.csharp.csharpcompilation', - 'assembly', - 'reflection.assembly', - 'system.reflection.assembly', - 'system.reflection.emit.assemblybuilder', - 'system.reflection.emit.modulebuilder', - 'system.reflection.emit.typebuilder', - 'system.reflection.emit.methodbuilder', - 'system.reflection.emit.dynamicmethod' - ) -} - -function Test-StrictReleaseForbiddenParameterName { - param([Parameter(Mandatory)] [AllowEmptyString()] [string] $ParameterName) - - if ($ParameterName.Length -eq 0) { return $false } - if ($ParameterName -in @('ov', 'pv', 'ev', 'wv', 'iv')) { - return $true - } - foreach ($canonicalName in @( - 'OutVariable', - 'PipelineVariable', - 'ErrorVariable', - 'WarningVariable', - 'InformationVariable', - 'MemberName' - )) { - if ($canonicalName.StartsWith( - $ParameterName, - [StringComparison]::OrdinalIgnoreCase - )) { - return $true - } - } - return $false -} - -function Test-ExecutionContextRoot { - param([Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node) - - while ($Node -is [System.Management.Automation.Language.ParenExpressionAst] -or - $Node -is [System.Management.Automation.Language.SubExpressionAst]) { - $pipeline = if ($Node -is [System.Management.Automation.Language.ParenExpressionAst]) { - $Node.Pipeline - } else { - $statements = @($Node.SubExpression.Statements) - if ($Node.SubExpression.Traps.Count -ne 0 -or $statements.Count -ne 1 -or - $statements[0] -isnot [System.Management.Automation.Language.PipelineAst]) { - return $false - } - $statements[0] - } - $elements = @($pipeline.PipelineElements) - if ($elements.Count -ne 1 -or $elements[0] -isnot [System.Management.Automation.Language.CommandExpressionAst]) { - return $false - } - $Node = $elements[0].Expression - } - if ($Node -isnot [System.Management.Automation.Language.VariableExpressionAst]) { return $false } - return (($Node.VariablePath.UserPath -split ':')[-1] -eq 'ExecutionContext') -} - -function Test-ExecutionContextMemberChain { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node, - [Parameter(Mandatory)] [string[]] $Members - ) - - if ($Node -isnot [System.Management.Automation.Language.MemberExpressionAst] -or - (Get-MemberExpressionName -Node $Node) -ne $Members[-1]) { - return $false - } - if ($Members.Count -eq 1) { return Test-ExecutionContextRoot -Node $Node.Expression } - return Test-ExecutionContextMemberChain -Node $Node.Expression -Members @($Members[0..($Members.Count - 2)]) -} - -function Test-ForbiddenScriptMutationNode { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Node, - [switch] $StrictReleaseContract - ) - - if ($StrictReleaseContract -and - $Node -is [System.Management.Automation.Language.CommandParameterAst] -and - (Test-StrictReleaseForbiddenParameterName ` - -ParameterName $Node.ParameterName)) { - return $true - } - if ($StrictReleaseContract -and - $Node -is [System.Management.Automation.Language.TypeExpressionAst] -and - (Test-StrictReleaseForbiddenTypeName -TypeName $Node.TypeName.FullName)) { - return $true - } - if ($StrictReleaseContract -and - $Node -is [System.Management.Automation.Language.StringConstantExpressionAst] -and - (Test-StrictReleaseForbiddenTypeName -TypeName $Node.Value)) { - return $true - } - if ($Node -is [System.Management.Automation.Language.ConvertExpressionAst] -and - (($Node.Type.TypeName.FullName -replace '\s', '').ToLowerInvariant() -in @('type', 'system.type'))) { - return $Node.Child -isnot [System.Management.Automation.Language.StringConstantExpressionAst] -or - (Test-DynamicScriptTypeName -TypeName $Node.Child.Value) - } - if ($Node -is [System.Management.Automation.Language.BinaryExpressionAst] -and - $Node.Operator -eq [System.Management.Automation.Language.TokenKind]::As -and - $Node.Right -is [System.Management.Automation.Language.TypeExpressionAst] -and - (($Node.Right.TypeName.FullName -replace '\s', '').ToLowerInvariant() -in @('type', 'system.type'))) { - return $Node.Left -isnot [System.Management.Automation.Language.StringConstantExpressionAst] -or - (Test-DynamicScriptTypeName -TypeName $Node.Left.Value) - } - if ($StrictReleaseContract -and - $Node -is [System.Management.Automation.Language.VariableExpressionAst] -and - (($Node.VariablePath.UserPath -split ':')[-1]) -in @( - 'ExecutionContext', 'PSCmdlet', 'Host', 'MyInvocation' - )) { - return $true - } - if ($Node -is [System.Management.Automation.Language.StringConstantExpressionAst] -and - (Test-DynamicScriptTypeName -TypeName $Node.Value)) { - $isTypeConversion = $Node.Parent -is [System.Management.Automation.Language.ConvertExpressionAst] -and - (($Node.Parent.Type.TypeName.FullName -replace '\s', '') -in @('type', 'System.Type')) - $isGetTypeArgument = $Node.Parent -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and - (Get-MemberExpressionName -Node $Node.Parent) -eq 'GetType' -and - @($Node.Parent.Arguments | Where-Object { [object]::ReferenceEquals($_, $Node) }).Count -eq 1 - $isAsTypeArgument = $Node.Parent -is [System.Management.Automation.Language.BinaryExpressionAst] -and - $Node.Parent.Operator -eq [System.Management.Automation.Language.TokenKind]::As - if ($isTypeConversion -or $isGetTypeArgument -or $isAsTypeArgument) { return $true } - } - if ($Node -is [System.Management.Automation.Language.VariableExpressionAst] -and - (($Node.VariablePath.UserPath -split ':')[-1] -eq 'ExecutionContext')) { - $ancestor = $Node.Parent - while ($null -ne $ancestor -and $ancestor -isnot [System.Management.Automation.Language.AssignmentStatementAst]) { - $ancestor = $ancestor.Parent - } - if ($ancestor -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $ancestor.Right -is [System.Management.Automation.Language.CommandExpressionAst] -and - (Test-ExecutionContextRoot -Node $ancestor.Right.Expression)) { - return $true - } - } - if ($Node -is [System.Management.Automation.Language.TypeExpressionAst] -and - (Test-DynamicScriptTypeExpression -Node $Node) -and - $Node.Parent -isnot [System.Management.Automation.Language.MemberExpressionAst]) { - return $true - } - if ($Node -is [System.Management.Automation.Language.MemberExpressionAst]) { - if ($Node.Member -isnot [System.Management.Automation.Language.StringConstantExpressionAst]) { - return $true - } - $memberName = Get-MemberExpressionName -Node $Node - if ($Node.Extent.Text -match '(?is)TypeAccelerators.*(?:Add|Remove)\b') { return $true } - if ($StrictReleaseContract -and - $memberName -in @( - 'DefaultRunspace', 'SessionStateProxy', - 'SetVariable', 'GetVariable', 'RemoveVariable', - 'SessionState', 'PSVariable', - 'PSObject', 'PSBase', 'PSAdapted', - 'Members', 'Methods', 'Properties', - 'CompileAssemblyFromSource', - 'CompileAssemblyFromFile', - 'CompileAssemblyFromDom', - 'CreateCompiler', 'CreateProvider', - 'DefineDynamicAssembly', 'DefineDynamicModule', - 'DefineType', 'DefineMethod', - 'CreateType', 'CreateTypeInfo', - 'BakeByteArray', 'Emit', - 'Load', 'LoadFile', 'LoadFrom', 'UnsafeLoadFrom', 'LoadModule' - )) { - return $true - } - if ($StrictReleaseContract -and - $Node -is - [System.Management.Automation.Language.InvokeMemberExpressionAst] -and - $memberName -in @( - 'GetType', - 'GetMethod', 'GetMethods', - 'GetConstructor', 'GetConstructors', - 'GetField', 'GetFields', - 'GetProperty', 'GetProperties', - 'GetMember', 'GetMembers', - 'Invoke', 'InvokeMember', 'DynamicInvoke', - 'CreateDelegate', 'CreateInstance', - 'MakeGenericMethod', 'MakeGenericType' - )) { - return $true - } - if ($Node -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and - $memberName -eq 'GetType' -and $Node.Arguments.Count -ne 0) { - return $true - } - if ((Test-DynamicScriptTypeExpression -Node $Node.Expression) -and - $memberName -in @('Create', 'CreateRunspace', 'CreatePipeline', 'CreateNestedPipeline', 'GetMethod', 'GetMethods')) { - return $true - } - if ($memberName -eq 'SessionState' -and - (Test-ExecutionContextRoot -Node $Node.Expression)) { - return $true - } - # Reject access to the InvokeCommand object itself so assigning it to - # an alias cannot bypass the direct member-chain checks. - if ($memberName -eq 'InvokeCommand' -and - ((Test-ExecutionContextRoot -Node $Node.Expression) -or - (Test-ExecutionContextMemberChain -Node $Node.Expression -Members @('SessionState')))) { - return $true - } - if ($memberName -eq 'InvokeProvider' -and - ((Test-ExecutionContextRoot -Node $Node.Expression) -or - (Test-ExecutionContextMemberChain -Node $Node.Expression -Members @('SessionState')))) { - return $true - } - } - if ($Node -is [System.Management.Automation.Language.CommandAst]) { - $name = $Node.GetCommandName() - if ($StrictReleaseContract -and - $Node.InvocationOperator -in @( - [System.Management.Automation.Language.TokenKind]::Ampersand, - [System.Management.Automation.Language.TokenKind]::Dot - )) { - return $true - } - $leafName = if ($null -eq $name) { '' } else { ($name -split '\\')[-1] } - return $leafName -in @('Set-Alias', 'sal', 'New-Alias', 'nal', 'Remove-Alias', 'ral', 'Import-Alias', 'ipal', 'Import-Module', 'ipmo', 'Import-PSSession', 'Add-PSSnapin', 'asnp', 'Remove-PSSnapin', 'rsnp', 'Invoke-Expression', 'iex') -or - ($StrictReleaseContract -and - $leafName -in @( - 'Get-Command', 'gcm', - 'Add-Type', - 'Tee-Object', 'tee', - 'Get-Member', 'gm', - 'Get-Variable', 'gv', - 'Set-Variable', 'sv', 'set', - 'New-Variable', 'nv', - 'Remove-Variable', 'rv', - 'Clear-Variable', 'clv', - 'Set-Item', 'si', 'Clear-Item', 'cli', - 'Remove-Item', 'del', 'erase', 'rd', 'ri', 'rm', 'rmdir', - 'Copy-Item', 'copy', 'cp', 'cpi', - 'Move-Item', 'mi', 'move', 'mv', - 'Rename-Item', 'ren', 'rni', 'Get-Item', 'gi', - 'Set-ItemProperty', 'sp', - 'New-ItemProperty', - 'Clear-ItemProperty', 'clp', - 'Remove-ItemProperty', 'rp', - 'Copy-ItemProperty', 'cpp', - 'Move-ItemProperty', 'mp', - 'Rename-ItemProperty', 'rnp', - 'Get-ItemProperty', 'gp', - 'Get-ItemPropertyValue', 'gpv', - 'Set-Content', 'sc', 'Add-Content', 'ac', - 'Clear-Content', 'clc' - )) -or - $Node.Extent.Text -match '(?i)(?:alias|function|variable):' - } - if ($Node -is [System.Management.Automation.Language.AssignmentStatementAst]) { - return $Node.Left.Extent.Text -match '(?i)^\$(?:\{)?(?:global:|script:|local:|private:)?(?:alias|function|variable):' - } - return $false -} - -function Assert-CommandResolutionContract { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [System.Management.Automation.Language.Token[]] $Tokens, - [Parameter(Mandatory)] [string] $Context, - [string[]] $ExpectedDotSources = @(), - [string[]] $ExpectedAmpersandCommands = @() - ) - - $mutators = @($Ast.FindAll({ param($node) Test-ForbiddenScriptMutationNode -Node $node }, $true)) - if ($mutators.Count -ne 0) { throw "Command resolution mutation is forbidden: $Context" } - if (Test-CommandLoadingRequirement -Ast $Ast -Tokens $Tokens) { - throw "Command-loading #requires directive is forbidden: $Context" - } - $dotSources = @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.InvocationOperator -eq [System.Management.Automation.Language.TokenKind]::Dot - }, $true)) - if ($dotSources.Count -ne $ExpectedDotSources.Count) { throw "Unexpected dot-source count: $Context" } - for ($i = 0; $i -lt $ExpectedDotSources.Count; $i++) { - if ($dotSources[$i].Extent.Text.Trim() -ne $ExpectedDotSources[$i]) { - throw "Unexpected dot-source command: $Context" - } - } - $ampersandCommands = @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.InvocationOperator -eq [System.Management.Automation.Language.TokenKind]::Ampersand - }, $true)) - if ($ampersandCommands.Count -ne $ExpectedAmpersandCommands.Count) { throw "Unexpected call-operator count: $Context" } - for ($i = 0; $i -lt $ExpectedAmpersandCommands.Count; $i++) { - if ($ampersandCommands[$i].Extent.Text.Trim() -ne $ExpectedAmpersandCommands[$i]) { - throw "Unexpected call-operator command: $Context" - } - } -} - -function Test-CommandLoadingRequirement { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [System.Management.Automation.Language.Token[]] $Tokens - ) - - if ($null -ne $Ast.ScriptRequirements -and - @($Ast.ScriptRequirements.RequiredModules).Count -ne 0) { - return $true - } - - return @($Tokens | Where-Object { - $_.Kind -eq [System.Management.Automation.Language.TokenKind]::Comment -and - $_.Text -match '(?i)^#requires\b[^\r\n]*[ \t]-(?:M(?:o(?:d(?:u(?:l(?:e(?:s)?)?)?)?)?)?|P(?:S(?:S(?:n(?:a(?:p(?:i(?:n)?)?)?)?)?)?)?)(?::|[ \t]|$)' - }).Count -ne 0 -} - -function Assert-NoProtectedFunctionDefinitions { - param( - [Parameter(Mandatory)] [System.Management.Automation.Language.Ast] $Ast, - [Parameter(Mandatory)] [string] $Context - ) - - $protectedNames = @('Get-InteractiveWin11MessageTimeoutMs', 'Assert-InteractiveWin11WindowOwner', 'Invoke-InteractiveWin11PostMessage', 'Invoke-StatefulPostedCommand') - $definitions = @($Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - (($node.Name -replace '^(?i)(?:global|script|local|private):', '') -in $protectedNames) - }, $true)) - if ($definitions.Count -ne 0) { throw "Harness must not redefine protected interactive functions: $Context" } -} - -$commandResolutionProbes = @( - [pscustomobject]@{ Reject = $true; Text = '[ScriptBlock]::' + [Environment]::NewLine + '''Create''("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '[ System.Management.Automation.ScriptBlock, System.Management.Automation ]::Create("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '[PowerShell]::Create()' } - [pscustomobject]@{ Reject = $true; Text = '$ExecutionContext.' + [Environment]::NewLine + 'InvokeCommand.' + [Environment]::NewLine + 'InvokeScript("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '${ExecutionContext}.SessionState.InvokeCommand.''InvokeScript''("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$($ExecutionContext).InvokeCommand.InvokeScript("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$global:ExecutionContext.InvokeCommand.NewScriptBlock("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$ic = $ExecutionContext.InvokeCommand; $ic.InvokeScript("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$ic = ${ExecutionContext}.SessionState.InvokeCommand; $ic.NewScriptBlock("1+1")' } - [pscustomobject]@{ Reject = $false; Text = '$ec = Get-Variable ExecutionContext -ValueOnly; $ec.InvokeCommand.InvokeScript("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$(Get-Item variable:ExecutionContext).Value.InvokeCommand.InvokeScript("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$ss = $ExecutionContext.SessionState; $ss.InvokeCommand.InvokeScript("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$member = "Create"; [ScriptBlock]::$member("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$member = "InvokeCommand"; $ExecutionContext.$member.InvokeScript("1+1")' } - [pscustomobject]@{ Reject = $true; Text = '$ec = $($ExecutionContext)' } - [pscustomobject]@{ Reject = $true; Text = '$factory = [ScriptBlock]' } - [pscustomobject]@{ Reject = $true; Text = '[ScriptBlock].GetMethod("Create")' } - [pscustomobject]@{ Reject = $true; Text = '[PSObject].Assembly.GetType("System.Management.Automation.ScriptBlock")' } - [pscustomobject]@{ Reject = $true; Text = '[PSObject].Assembly.GetType("System.Management.Automation." + "ScriptBlock")' } - [pscustomobject]@{ Reject = $true; Text = '$typeName = "System.Management.Automation.ScriptBlock"; [PSObject].Assembly.GetType($typeName)' } - [pscustomobject]@{ Reject = $true; Text = '$factory = [type]"System.Management.Automation.ScriptBlock"' } - [pscustomobject]@{ Reject = $true; Text = '$typeName = "System.Management.Automation.ScriptBlock"; $factory = [type]$typeName' } - [pscustomobject]@{ Reject = $true; Text = '$factory = [type]("System.Management.Automation." + "ScriptBlock")' } - [pscustomobject]@{ Reject = $true; Text = '$factory = "System.Management.Automation.ScriptBlock" -as [type]' } - [pscustomobject]@{ Reject = $true; Text = '$typeName = "System.Management.Automation.ScriptBlock"; $factory = $typeName -as [type]' } - [pscustomobject]@{ Reject = $false; Text = '$factory = "NocttyStatefulNative" -as [type]' } - [pscustomobject]@{ Reject = $true; Text = '[runspacefactory]::CreateRunspace()' } - [pscustomobject]@{ Reject = $true; Text = '[System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()' } - [pscustomobject]@{ Reject = $false; Text = '[ScriptBlock]::CreateDelegate("x")' } - [pscustomobject]@{ Reject = $true; Text = '$ExecutionContext.InvokeCommand.InvokeScriptBlock("x")' } - [pscustomobject]@{ Reject = $false; Text = '$list.Add("[ScriptBlock]::Create")' } - [pscustomobject]@{ Reject = $false; Text = 'Write-Host "ScriptBlock"' } - [pscustomobject]@{ Reject = $false; Text = '$list.Add("System.Management.Automation.ScriptBlock")' } - [pscustomobject]@{ Reject = $true; Text = 'Invoke-Expression ''Write-Host bypass''' } - [pscustomobject]@{ Reject = $true; Text = 'iex ''Write-Host bypass''' } - [pscustomobject]@{ Reject = $true; Text = 'Add-PSSnapin Example.SnapIn' } - [pscustomobject]@{ Reject = $true; Text = 'asnp Example.SnapIn' } - [pscustomobject]@{ Reject = $true; Text = '#requires -PSSnapin Example.SnapIn' } - [pscustomobject]@{ Reject = $true; Text = '#requires -PSSnapin:Example.SnapIn' } - [pscustomobject]@{ Reject = $true; Text = '#requires -P Example.SnapIn' } - [pscustomobject]@{ Reject = $true; Text = '#requires -PS Example.SnapIn' } - [pscustomobject]@{ Reject = $true; Text = '#requires -PSSn Example.SnapIn' } - [pscustomobject]@{ Reject = $true; Text = '#ReQuIrEs -Version 5.1 -PsSnApIn Example.SnapIn' } - [pscustomobject]@{ Reject = $true; Text = '#requires -Modules Example.Module' } - [pscustomobject]@{ Reject = $true; Text = '#requires -M Example.Module' } - [pscustomobject]@{ Reject = $false; Text = '#requires -Version 5.1' } - [pscustomobject]@{ Reject = $false; Text = '#requires -PSEdition Desktop' } - [pscustomobject]@{ Reject = $false; Text = 'Write-Host ''#requires -PSSnapin Example.SnapIn''' } - [pscustomobject]@{ Reject = $false; Text = ("@'" + [Environment]::NewLine + '#requires -Modules Example.Module' + [Environment]::NewLine + "'@") } -) -foreach ($probe in $commandResolutionProbes) { - $probeTokens = $null - $probeErrors = $null - $probeAst = [System.Management.Automation.Language.Parser]::ParseInput($probe.Text, [ref] $probeTokens, [ref] $probeErrors) - if ($probeErrors.Count -ne 0) { throw "Command-resolution probe does not parse: $($probe.Text)" } - $probeRejected = $false - $probeFailure = $null - try { - Assert-CommandResolutionContract -Ast $probeAst -Tokens $probeTokens -Context "probe: $($probe.Text)" - } catch { - $probeRejected = $true - $probeFailure = $_.Exception.Message - } - if ($probeRejected -ne $probe.Reject) { - throw "Command-resolution probe contract failed: $($probe.Text) (contract result: $probeFailure)" - } -} - -$strictOnlyCommandProbes = @( - 'Get-Variable value', - 'gv value', - 'Set-Variable value 1', - 'sv value 1', - 'set value 1', - 'New-Variable value 1', - 'nv value 1', - 'Remove-Variable value', - 'rv value', - 'Clear-Variable value', - 'clv value', - '. ./scripts/replace-release-evidence.ps1', - '& Write-Output harmless' -) -$strictOnlyCommandProbes += @( - @( - 'GetType', - 'GetMethod', 'GetMethods', - 'GetConstructor', 'GetConstructors', - 'GetField', 'GetFields', - 'GetProperty', 'GetProperties', - 'GetMember', 'GetMembers', - 'Invoke', 'InvokeMember', 'DynamicInvoke', - 'CreateDelegate', 'CreateInstance', - 'MakeGenericMethod', 'MakeGenericType' - ) | ForEach-Object { '$value.' + $_ + '()' } -) -$strictOnlyCommandProbes += @( - @( - 'powershell', - 'System.Management.Automation.PowerShell', - 'runspace', - 'runspacefactory', - 'initialsessionstate', - 'System.Management.Automation.Runspaces.Runspace', - 'System.Management.Automation.Runspaces.RunspaceFactory', - 'System.Management.Automation.Runspaces.InitialSessionState', - 'Microsoft.CSharp.CSharpCodeProvider', - 'Reflection.Assembly', - 'System.Reflection.Emit.AssemblyBuilder' - ) | ForEach-Object { - "'$_'" - "[$_]::Name" - } -) -$strictOnlyCommandProbes += @( - @( - 'DefaultRunspace', - 'SessionStateProxy', - 'SetVariable', - 'GetVariable', - 'RemoveVariable' - ) | ForEach-Object { '$value.' + $_ } -) -$strictOnlyCommandProbes += @( - 'Tee-Object -Variable value', - 'tee -Variable value', - 'Write-Output x -OutVariable value', - 'Write-Output x -ov value', - 'Write-Output x -OutV value', - 'Write-Output x -PipelineV value', - 'Write-Output x -ev value', - 'Write-Output x -wv value', - 'Write-Output x -iv value', - 'Write-Output x | ForEach-Object -MemberName ToString', - 'Write-Output x | % -M ToString', - 'gm -InputObject value', - 'Add-Type -TypeDefinition ''public class StrictProbe {}''', - 'Microsoft.PowerShell.Utility\Add-Type -TypeDefinition ''public class StrictProbe {}''' -) -$strictOnlyCommandProbes += @( - @( - 'PSObject', - 'Properties', - 'CompileAssemblyFromSource', - 'DefineDynamicAssembly', - 'Load' - ) | ForEach-Object { '$value.' + $_ } -) -$strictOnlyCommandProbes += @( - '$PSCmdlet', - '$Host', - '$MyInvocation', - '$value.SessionState', - '$value.PSVariable' -) -foreach ($probeText in $strictOnlyCommandProbes) { - $probeTokens = $null - $probeErrors = $null - $probeAst = [System.Management.Automation.Language.Parser]::ParseInput( - $probeText, - [ref]$probeTokens, - [ref]$probeErrors - ) - if ($probeErrors.Count -ne 0) { - throw "Strict command probe does not parse: $probeText" - } - $generalRejections = @($probeAst.FindAll({ - param($node) - Test-ForbiddenScriptMutationNode -Node $node - }, $true)) - $strictRejections = @($probeAst.FindAll({ - param($node) - Test-ForbiddenScriptMutationNode ` - -Node $node ` - -StrictReleaseContract - }, $true)) - if ($generalRejections.Count -ne 0 -or - $strictRejections.Count -ne 1) { - throw "Strict command mode contract failed: $probeText" - } -} - -$directStatementTokens = $null -$directStatementErrors = $null -$directStatementAst = [System.Management.Automation.Language.Parser]::ParseInput( - 'try { if ($(Invoke-StatefulPostedCommand 1 2 $deadline $process)) { } } finally { }', - [ref] $directStatementTokens, - [ref] $directStatementErrors -) -if ($directStatementErrors.Count -ne 0) { - throw "Direct-statement control-flow probe does not parse: $($directStatementErrors[0].Message)" -} -$directStatementTry = @($directStatementAst.FindAll({ param($node) $node -is [System.Management.Automation.Language.TryStatementAst] }, $true))[0] -$conditionalPostedCall = @($directStatementAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Invoke-StatefulPostedCommand' -}, $true))[0] -if ($null -ne (Get-DirectStatementBlockChild -Node $conditionalPostedCall -StatementBlock $directStatementTry.Body)) { - throw 'Direct-statement helper accepted a protected call nested in control flow.' -} - -$stepBoundaryProbe = @' - - name: Target - run: inside-step - - uses: example/outside-step@v1 - - name: Other - run: outside-step -'@ -if ((Get-YamlStepBlock -Content $stepBoundaryProbe -Name 'Target' -Source 'step boundary probe') -match 'outside-step') { - throw 'Workflow step extraction crossed a step boundary.' -} -$duplicateStepProbe = @' - - name: Target - run: first - - name: Target - run: second -'@ -$duplicateStepRejected = $false -try { - Get-YamlStepBlock ` - -Content $duplicateStepProbe ` - -Name 'Target' ` - -Source 'duplicate step probe' | - Out-Null -} -catch { - $duplicateStepRejected = $true -} -if (-not $duplicateStepRejected) { - throw 'Workflow step extraction accepted an ambiguous duplicate name.' -} -$stepTailProbe = " - name: Target`n run: inside-step`n env: # job-level tail`n VALUE: outside-step" -if ((Get-YamlStepBlock -Content $stepTailProbe -Name 'Target' -Source 'step tail probe') -match 'outside-step') { - throw 'Workflow step extraction crossed a job-level key boundary.' -} -$jobBoundaryProbe = " target:`n value: inside-job`n `"other.job`": # annotated`n value: outside-job" -if ((Get-YamlJobText -Content $jobBoundaryProbe -Name 'target' -Source 'job boundary probe') -match 'outside-job') { - throw 'Workflow job extraction crossed an annotated job boundary.' -} -$blockBoundaryProbe = @' -if ($RequirePackageManagers) { - Write-Host "inside { literal" - # A comment containing } is not a block boundary. -} -Write-Host outside-block -'@ -if ((Get-PowerShellBlockText -Content $blockBoundaryProbe -HeaderPattern '^if \(\$RequirePackageManagers\)') -match 'outside-block') { - throw 'PowerShell AST extraction crossed a block boundary.' -} - -$schemaPaths = @( - 'scenario.schema.json' - 'result.schema.json' - 'baseline-manifest.schema.json' -) | ForEach-Object { Join-Path $root $_ } - -foreach ($schemaPath in $schemaPaths) { - Get-Content -LiteralPath $schemaPath -Raw | ConvertFrom-Json | Out-Null -} - -$scenarioSchema = Join-Path $root 'scenario.schema.json' -$scenarioIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) -foreach ($scenarioPath in Get-ChildItem -LiteralPath (Join-Path $root 'scenarios') -Filter '*.json') { - Assert-JsonDocument -Path $scenarioPath.FullName -SchemaPath $scenarioSchema - $scenario = Get-Content -LiteralPath $scenarioPath.FullName -Raw | ConvertFrom-Json - if (-not $scenarioIds.Add($scenario.id)) { - throw "Duplicate flagship scenario id: $($scenario.id)" - } -} - -Assert-JsonDocument ` - -Path (Join-Path $root 'examples\result.json') ` - -SchemaPath (Join-Path $root 'result.schema.json') -$exampleResult = Get-Content -LiteralPath (Join-Path $root 'examples\result.json') -Raw | ConvertFrom-Json -if (-not $exampleResult.example -or -not $exampleResult.scenario_id.StartsWith('example.')) { - throw 'Example verification result must be explicitly marked and use an example.* scenario id.' -} -Assert-PassArtifactsBound -Path (Join-Path $root 'examples\result.json') -foreach ($resultPath in Get-ChildItem -LiteralPath (Join-Path $root 'artifacts') -Filter result.json -Recurse -ErrorAction SilentlyContinue) { - Assert-JsonDocument -Path $resultPath.FullName -SchemaPath (Join-Path $root 'result.schema.json') - Assert-PassArtifactsBound -Path $resultPath.FullName -} -Assert-JsonDocument ` - -Path (Join-Path $root 'examples\baseline-manifest.json') ` - -SchemaPath (Join-Path $root 'baseline-manifest.schema.json') - -$releaseWorkflow = Join-Path $repoRoot '.github\workflows\release.yml' -$readinessWorkflow = Join-Path $repoRoot '.github\workflows\release-readiness.yml' -$testWorkflow = Join-Path $repoRoot '.github\workflows\test.yml' -$siteDeployWorkflow = Join-Path $repoRoot '.github\workflows\deploy-site.yml' -$siteAssetBuilder = Join-Path $repoRoot 'scripts\build-site-assets.mjs' -$sitePayloadBuilder = Join-Path $repoRoot 'scripts\build-site-payload.ps1' -$siteHeaderContract = Join-Path $repoRoot 'scripts\get-site-header-contract.ps1' -$siteDeploymentHeadGate = Join-Path $repoRoot 'scripts\require-site-deployment-head.ps1' -$cloudflarePagesVerifier = Join-Path $repoRoot 'scripts\verify-cloudflare-pages.ps1' -$siteHeaders = Join-Path $repoRoot 'site\_headers' -$siteReadme = Join-Path $repoRoot 'site\README.md' -$site404 = Join-Path $repoRoot 'site\404.html' -$siteIndex = Join-Path $repoRoot 'site\index.html' -$siteGitattributes = Join-Path $repoRoot '.gitattributes' -$accessibilityChecker = Join-Path $repoRoot 'scripts\check-accessibility-evidence.ps1' -$runnerProvenanceChecker = Join-Path $repoRoot 'test\windows\assert-interactive-runner.ps1' -$interactiveWin11Lib = Join-Path $repoRoot 'scripts\interactive-win11-lib.ps1' -$cliShellHarness = Join-Path $repoRoot 'test\windows\cli-shell-command.ps1' -$statefulWin11Lib = Join-Path $repoRoot 'test\windows\interactive-win11-stateful-lib.ps1' -$accessibilityHarness = Join-Path $repoRoot 'test\windows\interactive-win11-accessibility.ps1' -$sessionRestoreHarness = Join-Path $repoRoot 'test\windows\interactive-win11-session-restore.ps1' -$paletteThemeHarness = Join-Path $repoRoot 'test\windows\interactive-win11-palette-theme.ps1' -$newTabHarness = Join-Path $repoRoot 'test\windows\interactive-win11-new-tab.ps1' -$undoHarness = Join-Path $repoRoot 'test\windows\interactive-win11-undo.ps1' -$resizeHarness = Join-Path $repoRoot 'test\windows\interactive-win11-resize.ps1' -$keyInputHarness = Join-Path $repoRoot 'test\windows\interactive-win11-key-input.ps1' -$interactiveValidator = Join-Path $repoRoot 'test\windows\interactive-win11-validate.ps1' -$shaderHarness = Join-Path $repoRoot 'test\windows\interactive-win11-shaders.ps1' -$win32Runtime = Join-Path $repoRoot 'src\apprt\win32.zig' -$win32Settings = Join-Path $repoRoot 'src\apprt\win32_settings.zig' -$win32Theme = Join-Path $repoRoot 'src\apprt\win32_theme.zig' -$win32UiaWidgets = Join-Path $repoRoot 'src\apprt\win32_uia\widgets.zig' -$terminalOutputCapture = Join-Path $repoRoot 'src\termio\semantic_output_capture.zig' -$terminalStreamHandler = Join-Path $repoRoot 'src\termio\stream_handler.zig' -$terminalSemanticOutput = Join-Path $repoRoot 'src\terminal\semantic_output.zig' -$termioRuntime = Join-Path $repoRoot 'src\termio\Termio.zig' -$surfaceRuntime = Join-Path $repoRoot 'src\apprt\surface.zig' -$terminalAccessibility = Join-Path $repoRoot 'src\apprt\win32_terminal_accessibility.zig' -$interactivePrSmoke = Join-Path $repoRoot 'test\windows\interactive-win11-pr-smoke.ps1' -$releaseCopyChecker = Join-Path $repoRoot 'scripts\check-release-copy.ps1' -$releasePreflight = Join-Path $repoRoot 'scripts\release-preflight.ps1' -$publishedReleaseVerifier = Join-Path $repoRoot 'scripts\verify-published-release.ps1' -$windowsPackager = Join-Path $repoRoot 'scripts\package-windows.ps1' -$windowsBuildCapabilities = Join-Path $repoRoot 'scripts\windows-build-capabilities.ps1' -$signingTrust = Join-Path $repoRoot 'scripts\signing-trust.ps1' -$signingTrustTest = Join-Path $repoRoot 'scripts\test-signing-trust.ps1' -$releaseWorkflowText = Get-Content -LiteralPath $releaseWorkflow -Raw -$readinessWorkflowText = Get-Content -LiteralPath $readinessWorkflow -Raw -$testWorkflowText = Get-Content -LiteralPath $testWorkflow -Raw -$siteDeployWorkflowText = Get-Content -LiteralPath $siteDeployWorkflow -Raw -$siteAssetBuilderText = Get-Content -LiteralPath $siteAssetBuilder -Raw -$sitePayloadBuilderText = Get-Content -LiteralPath $sitePayloadBuilder -Raw -$cloudflarePagesVerifierText = Get-Content -LiteralPath $cloudflarePagesVerifier -Raw -$siteHeadersText = Get-Content -LiteralPath $siteHeaders -Raw -$siteReadmeText = Get-Content -LiteralPath $siteReadme -Raw -$site404Text = Get-Content -LiteralPath $site404 -Raw -$siteIndexText = Get-Content -LiteralPath $siteIndex -Raw -$siteGitattributesText = Get-Content -LiteralPath $siteGitattributes -Raw -$interactiveWin11LibText = Get-Content -LiteralPath $interactiveWin11Lib -Raw -$cliShellHarnessText = Get-Content -LiteralPath $cliShellHarness -Raw -$statefulWin11LibText = Get-Content -LiteralPath $statefulWin11Lib -Raw -$accessibilityHarnessText = Get-Content -LiteralPath $accessibilityHarness -Raw -$publishedReleaseVerifierText = Get-Content -LiteralPath $publishedReleaseVerifier -Raw -$windowsPackagerText = Get-Content -LiteralPath $windowsPackager -Raw -$signingTrustText = Get-Content -LiteralPath $signingTrust -Raw -$signingTrustTestText = Get-Content -LiteralPath $signingTrustTest -Raw -$win32RuntimeText = Get-Content -LiteralPath $win32Runtime -Raw -$win32SettingsText = Get-Content -LiteralPath $win32Settings -Raw -$win32ThemeText = Get-Content -LiteralPath $win32Theme -Raw -$win32UiaWidgetsText = Get-Content -LiteralPath $win32UiaWidgets -Raw -$terminalOutputCaptureText = Get-Content -LiteralPath $terminalOutputCapture -Raw -$terminalStreamHandlerText = Get-Content -LiteralPath $terminalStreamHandler -Raw -$terminalSemanticOutputText = Get-Content -LiteralPath $terminalSemanticOutput -Raw -$termioRuntimeText = Get-Content -LiteralPath $termioRuntime -Raw -$surfaceRuntimeText = Get-Content -LiteralPath $surfaceRuntime -Raw -$terminalAccessibilityText = Get-Content -LiteralPath $terminalAccessibility -Raw -$sessionRestoreHarnessText = Get-Content -LiteralPath $sessionRestoreHarness -Raw -$paletteThemeHarnessText = Get-Content -LiteralPath $paletteThemeHarness -Raw -$keyInputHarnessText = Get-Content -LiteralPath $keyInputHarness -Raw -$interactiveValidatorText = Get-Content -LiteralPath $interactiveValidator -Raw -$accessibilityHarnessTokens = $null -$accessibilityHarnessErrors = $null -$accessibilityHarnessAst = [System.Management.Automation.Language.Parser]::ParseInput( - $accessibilityHarnessText, - [ref]$accessibilityHarnessTokens, - [ref]$accessibilityHarnessErrors -) -if ($accessibilityHarnessErrors.Count -ne 0) { - throw "Accessibility harness does not parse: $($accessibilityHarnessErrors[0].Message)" -} -foreach ($source in @( - [pscustomobject]@{ Name = 'key-input harness'; Text = $keyInputHarnessText }, - [pscustomobject]@{ Name = 'interactive validator'; Text = $interactiveValidatorText } -)) { - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput( - $source.Text, - [ref]$tokens, - [ref]$errors - ) - if ($errors.Count -ne 0) { - throw "$($source.Name) does not parse: $($errors[0].Message)" - } - if ($source.Name -eq 'key-input harness') { $keyInputHarnessAst = $ast } - else { $interactiveValidatorAst = $ast } -} - -$injectiveMutationTokens = $null -$injectiveMutationErrors = $null -$injectiveMutationAst = [System.Management.Automation.Language.Parser]::ParseInput( - @' -$stdoutPath = Join-Path $layout.Logs "constant.log" -$runName = if ([string]::IsNullOrWhiteSpace($ScenarioSlug)) { - $ScriptName -} else { - $ScriptName -} -'@, - [ref] $injectiveMutationTokens, - [ref] $injectiveMutationErrors -) -$injectiveMutationAssignments = @($injectiveMutationAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -}, $true)) -if ($injectiveMutationErrors.Count -ne 0 -or - $injectiveMutationAssignments.Count -ne 2 -or - (Test-DirectJoinPathNameExpression ` - -Node $injectiveMutationAssignments[0].Right ` - -ParentVariable 'layout' ` - -ParentMember 'Logs' ` - -ChildVariable 'artifactPrefix' ` - -ChildSuffix '-stdout.log' ` - -Style Expandable) -or - (Test-InjectiveHarnessRunNameExpression ` - -Node $injectiveMutationAssignments[1].Right)) { - throw 'Injective artifact-name contracts must reject constant-path mutations.' -} - -$keyInputParameterNames = @($keyInputHarnessAst.ParamBlock.Parameters | ForEach-Object { - $_.Name.VariablePath.UserPath -}) -if ($keyInputParameterNames -contains 'Route') { - throw 'Key-input harness must always target the surface directly; Route is not a valid public option.' -} -$keyInputKeyParameters = @($keyInputHarnessAst.ParamBlock.Parameters | Where-Object { - $_.Name.VariablePath.UserPath -eq 'Key' -}) -$keyInputKeyValidateSets = if ($keyInputKeyParameters.Count -eq 1) { - @($keyInputKeyParameters[0].Attributes | Where-Object { - $_ -is [System.Management.Automation.Language.AttributeAst] -and - $_.TypeName.FullName -eq 'ValidateSet' - }) -} else { - @() -} -$keyInputKeyValues = if ($keyInputKeyValidateSets.Count -eq 1) { - @($keyInputKeyValidateSets[0].PositionalArguments | ForEach-Object { - if ($_ -isnot [System.Management.Automation.Language.StringConstantExpressionAst]) { - throw 'Key-input Key ValidateSet values must be static strings.' - } - $_.Value - }) -} else { - @() -} -$requiredKeyInputValues = @( - 'a', - 'space', - 'unicode-bmp', - 'unicode-supplementary', - 'unicode-burst', - 'unicode-cr', - 'unicode-lf', - 'unicode-tab', - 'unicode-backspace', - 'unicode-escape' -) -if ($keyInputKeyValues.Count -ne $requiredKeyInputValues.Count -or - @(Compare-Object ` - -ReferenceObject $requiredKeyInputValues ` - -DifferenceObject $keyInputKeyValues ` - -SyncWindow 0 ` - -CaseSensitive).Count -ne 0 -or - @($keyInputKeyValues | Sort-Object -Unique).Count -ne - $keyInputKeyValues.Count) { - throw 'Key-input harness must expose the exact ten-key input ValidateSet.' -} -if (@($keyInputKeyValues | Where-Object { - $_ -match '(?i)alt|numpad' -}).Count -ne 0) { - throw 'Key-input harness cannot expose Alt+numpad input synthesis.' -} -$slugFunctions = @(Get-NamedFunctionDefinitions -Ast $keyInputHarnessAst -Name 'Get-KeyInputScenarioSlug') -if ($slugFunctions.Count -ne 1) { - throw 'Key-input harness must own exactly one scenario-slug function.' -} -try { - . ([scriptblock]::Create($slugFunctions[0].Extent.Text)) - $slugFixtures = @( - foreach ($keyValue in $keyInputKeyValues) { - Get-KeyInputScenarioSlug -Key $keyValue - } - ) - if (@($slugFixtures | Where-Object { [string]::IsNullOrWhiteSpace($_) }).Count -ne 0 -or - $slugFixtures.Count -ne 10 -or - @($slugFixtures | Sort-Object -Unique).Count -ne - $slugFixtures.Count) { - throw 'The ten-key set must produce exactly ten unique artifact slugs.' - } -} -finally { - Remove-Item -LiteralPath Function:\Get-KeyInputScenarioSlug -ErrorAction SilentlyContinue -} -$sandboxCalls = @(Get-NamedCommands -Ast $keyInputHarnessAst -Name 'Initialize-InteractiveWin11Sandbox') -$scenarioSlugAssignments = @($keyInputHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - (Get-VariableExpressionName -Node $node.Left) -eq 'scenarioSlug' -}, $true)) -$artifactAssignments = @($keyInputHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - (Get-VariableExpressionName -Node $node.Left) -in @( - 'artifactPrefix', 'stdoutPath', 'stderrPath', 'resultPath' - ) -}, $true)) -$sandboxName = if ($sandboxCalls.Count -eq 1) { - Get-CommandParameterArgument -Command $sandboxCalls[0] -Name 'SandboxName' -} -$scenarioSlugCalls = if ($scenarioSlugAssignments.Count -eq 1) { - @(Get-NamedCommands ` - -Ast $scenarioSlugAssignments[0].Right ` - -Name 'Get-KeyInputScenarioSlug') -} else { - @() -} -$scenarioSlugKey = if ($scenarioSlugCalls.Count -eq 1) { - Get-CommandParameterArgument -Command $scenarioSlugCalls[0] -Name 'Key' -} -$artifactVariableNames = @( - 'artifactPrefix', 'stdoutPath', 'stderrPath', 'resultPath' -) -$artifactAssignmentContract = @( - foreach ($artifactVariableName in $artifactVariableNames) { - $assignments = @($artifactAssignments | Where-Object { - (Get-VariableExpressionName -Node $_.Left) -eq - $artifactVariableName - }) - if ($assignments.Count -ne 1) { $false; continue } - if ($artifactVariableName -eq 'artifactPrefix') { - $expression = $assignments[0].Right - $expression -is - [System.Management.Automation.Language.CommandExpressionAst] -and - $expression.Expression -is - [System.Management.Automation.Language.ExpandableStringExpressionAst] -and - $expression.Expression.Value -ceq - 'interactive-win11-key-input-$scenarioSlug' -and - $expression.Expression.NestedExpressions.Count -eq 1 -and - (Get-VariableExpressionName ` - -Node $expression.Expression.NestedExpressions[0]) -eq - 'scenarioSlug' - continue - } - $spec = switch ($artifactVariableName) { - 'stdoutPath' { @('layout', 'Logs', 'artifactPrefix', '-stdout.log') } - 'stderrPath' { @('layout', 'Logs', 'artifactPrefix', '-stderr.log') } - 'resultPath' { @('layout', 'Temp', 'artifactPrefix', '-result.json') } - } - Test-DirectJoinPathNameExpression ` - -Node $assignments[0].Right ` - -ParentVariable $spec[0] ` - -ParentMember $spec[1] ` - -ChildVariable $spec[2] ` - -ChildSuffix $spec[3] ` - -Style Expandable - } -) -if ($sandboxCalls.Count -ne 1 -or - -not (Test-DirectNamedBlockChild ` - -Node $sandboxCalls[0] ` - -NamedBlock $keyInputHarnessAst.EndBlock) -or - $sandboxName -isnot - [System.Management.Automation.Language.ExpandableStringExpressionAst] -or - $sandboxName.Value -cne 'key-input-$scenarioSlug' -or - $sandboxName.NestedExpressions.Count -ne 1 -or - (Get-VariableExpressionName -Node $sandboxName.NestedExpressions[0]) -ne - 'scenarioSlug' -or - $scenarioSlugAssignments.Count -ne 1 -or - -not [object]::ReferenceEquals( - $scenarioSlugAssignments[0].Parent, - $keyInputHarnessAst.EndBlock - ) -or - $scenarioSlugAssignments[0].Right -isnot - [System.Management.Automation.Language.PipelineAst] -or - $scenarioSlugAssignments[0].Right.PipelineElements.Count -ne 1 -or - -not [object]::ReferenceEquals( - $scenarioSlugAssignments[0].Right.PipelineElements[0], - $scenarioSlugCalls[0] - ) -or - $scenarioSlugCalls.Count -ne 1 -or - (Get-VariableExpressionName -Node $scenarioSlugKey) -ne 'Key' -or - @($artifactAssignments | Where-Object { - -not [object]::ReferenceEquals($_.Parent, $keyInputHarnessAst.EndBlock) - }).Count -ne 0 -or - $artifactAssignmentContract.Count -ne $artifactVariableNames.Count -or - @($artifactAssignmentContract | Where-Object { -not $_ }).Count -ne 0) { - throw 'Key-input scenario slug must flow into its sandbox, result, and log artifact names.' -} - -$keyInputCompositeCalls = @(Get-NamedCommands -Ast $interactiveValidatorAst -Name 'Invoke-HarnessWithPassSentinel' | - Where-Object { - $scriptName = Get-CommandParameterArgument -Command $_ -Name 'ScriptName' - $scriptName -is [System.Management.Automation.Language.StringConstantExpressionAst] -and - $scriptName.Value -eq 'interactive-win11-key-input.ps1' - }) -$coveredKeyScenarios = [Collections.Generic.List[string]]::new() -$compositeSlugs = [Collections.Generic.List[string]]::new() -foreach ($command in $keyInputCompositeCalls) { - $slug = Get-CommandParameterArgument -Command $command -Name 'ScenarioSlug' - if ($slug -isnot [System.Management.Automation.Language.StringConstantExpressionAst] -or - [string]::IsNullOrWhiteSpace($slug.Value)) { - throw 'Every composite key-input run must provide a nonempty static scenario slug.' - } - [void]$compositeSlugs.Add($slug.Value) - $additional = Get-CommandParameterArgument -Command $command -Name 'AdditionalArguments' - if ($null -eq $additional) { - [void]$coveredKeyScenarios.Add('a') - continue - } - $keyValues = @($additional.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.StringConstantExpressionAst] -and - $node.Value -in @( - 'unicode-bmp', - 'unicode-supplementary', - 'unicode-burst', - 'unicode-cr', - 'unicode-lf', - 'unicode-tab', - 'unicode-backspace', - 'unicode-escape' - ) - }, $true)) - if ($keyValues.Count -ne 1) { throw 'Composite key-input run has an ambiguous Unicode scenario.' } - [void]$coveredKeyScenarios.Add($keyValues[0].Value) -} -$expectedKeyScenarios = @( - 'a', - 'unicode-bmp', - 'unicode-supplementary', - 'unicode-burst', - 'unicode-cr', - 'unicode-lf', - 'unicode-tab', - 'unicode-backspace', - 'unicode-escape' -) -if ($keyInputCompositeCalls.Count -ne 9 -or - @($compositeSlugs | Sort-Object -Unique).Count -ne 9 -or - (Compare-Object $expectedKeyScenarios @($coveredKeyScenarios) -SyncWindow 0)) { - throw 'Composite validator must isolate every key-input scenario except space.' -} -$invokeHarnessFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $interactiveValidatorAst ` - -Name 'Invoke-HarnessWithPassSentinel' -) -$startHarnessFunctions = @(Get-NamedFunctionDefinitions -Ast $interactiveValidatorAst -Name 'Start-Harness') -$getHarnessArgumentsFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $interactiveValidatorAst ` - -Name 'Get-HarnessArguments' -) -$invokeStartHarnessCalls = if ($invokeHarnessFunctions.Count -eq 1) { - @(Get-NamedCommands ` - -Ast $invokeHarnessFunctions[0].Body ` - -Name 'Start-Harness') -} else { - @() -} -$startGetHarnessArgumentsCalls = if ($startHarnessFunctions.Count -eq 1) { - @(Get-NamedCommands ` - -Ast $startHarnessFunctions[0].Body ` - -Name 'Get-HarnessArguments') -} else { - @() -} -$invokeAdditionalArguments = - if ($invokeStartHarnessCalls.Count -eq 1) { - Get-CommandParameterArgument ` - -Command $invokeStartHarnessCalls[0] ` - -Name 'AdditionalArguments' - } -$invokeScenarioSlug = - if ($invokeStartHarnessCalls.Count -eq 1) { - Get-CommandParameterArgument ` - -Command $invokeStartHarnessCalls[0] ` - -Name 'ScenarioSlug' - } -$startAdditionalArguments = - if ($startGetHarnessArgumentsCalls.Count -eq 1) { - Get-CommandParameterArgument ` - -Command $startGetHarnessArgumentsCalls[0] ` - -Name 'AdditionalArguments' - } -$startHarnessAssignments = if ($startHarnessFunctions.Count -eq 1) { - @($startHarnessFunctions[0].Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - (Get-VariableExpressionName -Node $node.Left) -in @('runName', 'stdoutPath', 'stderrPath') - }, $true)) -} else { - @() -} -$startHarnessVariableNames = @('runName', 'stdoutPath', 'stderrPath') -$startHarnessAssignmentContract = @( - foreach ($startHarnessVariableName in $startHarnessVariableNames) { - $assignments = @($startHarnessAssignments | Where-Object { - (Get-VariableExpressionName -Node $_.Left) -eq - $startHarnessVariableName - }) - if ($assignments.Count -ne 1) { $false; continue } - if ($startHarnessVariableName -eq 'runName') { - Test-InjectiveHarnessRunNameExpression -Node $assignments[0].Right - continue - } - $suffix = if ($startHarnessVariableName -eq 'stdoutPath') { - '.stdout.log' - } else { - '.stderr.log' - } - Test-DirectJoinPathNameExpression ` - -Node $assignments[0].Right ` - -ParentVariable 'suiteLogDir' ` - -ChildVariable 'runName' ` - -ChildSuffix $suffix ` - -Style Format - } -) -if ($invokeHarnessFunctions.Count -ne 1 -or - $startHarnessFunctions.Count -ne 1 -or - $getHarnessArgumentsFunctions.Count -ne 1 -or - $invokeStartHarnessCalls.Count -ne 1 -or - $startGetHarnessArgumentsCalls.Count -ne 1 -or - -not (Test-DirectNamedBlockChild ` - -Node $invokeStartHarnessCalls[0] ` - -NamedBlock $invokeHarnessFunctions[0].Body.EndBlock) -or - -not (Test-DirectNamedBlockChild ` - -Node $startGetHarnessArgumentsCalls[0] ` - -NamedBlock $startHarnessFunctions[0].Body.EndBlock) -or - (Get-VariableExpressionName -Node $invokeAdditionalArguments) -ne - 'AdditionalArguments' -or - (Get-VariableExpressionName -Node $invokeScenarioSlug) -ne - 'ScenarioSlug' -or - (Get-VariableExpressionName -Node $startAdditionalArguments) -ne - 'AdditionalArguments' -or - @($startHarnessAssignments | Where-Object { - -not [object]::ReferenceEquals( - $_.Parent, - $startHarnessFunctions[0].Body.EndBlock - ) - }).Count -ne 0 -or - $startHarnessAssignmentContract.Count -ne - $startHarnessVariableNames.Count -or - @($startHarnessAssignmentContract | Where-Object { -not $_ }).Count -ne - 0) { - throw 'Composite log paths must derive from each run scenario slug.' -} - -Assert-DeferredZigFixtureExecution ` - -WorkflowText $testWorkflowText ` - -Source $testWorkflow - -$nativeKeyFilters = @('VK_PACKET', 'deferred char') -foreach ($nativeKeyFilter in $nativeKeyFilters) { - # Zig accepts a zero-match test filter. A narrow declaration check is - # appropriate here only to prove that the semantic runner below is nonempty. - $nativeKeyDeclarations = [regex]::Matches( - $win32RuntimeText, - '(?m)^test "win32 ' + [regex]::Escape($nativeKeyFilter) + - '[^"\r\n]*" \{$' - ) - if ($nativeKeyDeclarations.Count -lt 1) { - throw "$nativeKeyFilter semantic fixture has no matching Zig test declaration." - } - Invoke-ZigFixture ` - -RepoRoot $repoRoot ` - -Filter $nativeKeyFilter -} - -if ($termioRuntimeText.Contains('self.surface_mailbox.pushTerminalOutput(buf)') -or - -not $termioRuntimeText.Contains('self.terminal_stream.handler.semantic_output.begin(') -or - -not $termioRuntimeText.Contains('self.terminal_stream.handler.semantic_output.finish()') -or - -not $termioRuntimeText.Contains('self.terminal_output_transport.captureEpoch()') -or - -not $termioRuntimeText.Contains('self.terminal_output_transport.pushSemanticBatchForEpoch(') -or - -not $termioRuntimeText.Contains('decision.semantic_output.slice()')) { - throw 'Termio must publish parser-derived semantic batches instead of forwarding raw PTY bytes.' -} -if ($terminalAccessibilityText.Contains('OutputSanitizer') -or - $terminalAccessibilityText.Contains('sanitizeAnnouncementByte') -or - $surfaceRuntimeText.Contains('terminal output transport drains inactive split controls without speech')) { - throw 'Win32 accessibility cannot retain a second terminal parser or its fake transport parser fixture.' -} -$semanticOutputInterestPolicyMatches = [regex]::Matches( - $terminalAccessibilityText, - '(?ms)^fn semanticOutputInterestPolicy\(\r?\n\s+attached: bool,\r?\n\s+provider_ready: bool,\r?\n\s+focused: bool,\r?\n\) bool \{\r?\n(?.*?)^\}' -) -if ($semanticOutputInterestPolicyMatches.Count -ne 1 -or - -not $semanticOutputInterestPolicyMatches[0].Groups['body'].Value.Contains( - 'return attached and provider_ready and focused;' - ) -or - -not $terminalAccessibilityText.Contains('.emit_events = clients_listening,') -or - -not $terminalAccessibilityText.Contains('win32_uia.events.clientsAreListening(),')) { - throw 'Semantic output interest must require attachment, provider readiness, and focus while event emission remains listener-gated.' -} - -$semanticPolicyTestMatch = [regex]::Match( - $terminalAccessibilityText, - '(?ms)^test "terminal accessibility refresh and query policies" \{\r?\n(?.*?)^\}' -) -$semanticPolicyAssertions = @( - 'try std.testing.expect(semanticOutputInterestPolicy(true, true, true));', - 'try std.testing.expect(!semanticOutputInterestPolicy(true, true, false));', - 'try std.testing.expect(!semanticOutputInterestPolicy(true, false, true));', - 'try std.testing.expect(!semanticOutputInterestPolicy(false, true, true));', - 'try std.testing.expect(!query_only.emit_events);', - 'try std.testing.expect(subscribed.emit_events);' -) -if (-not $semanticPolicyTestMatch.Success) { - throw 'Semantic output interest policy has no exact Zig test declaration.' -} -foreach ($assertion in $semanticPolicyAssertions) { - if (-not $semanticPolicyTestMatch.Groups['body'].Value.Contains($assertion)) { - throw "Semantic output interest policy test is missing assertion: $assertion" - } -} -if (-not $terminalSemanticOutputText.Contains('pub const transport_chunk_bytes = 1_000;') -or - -not $terminalSemanticOutputText.Contains('pub const transport_max_chunks = 8;') -or - -not $terminalSemanticOutputText.Contains('pub const capacity = transport_chunk_bytes * transport_max_chunks;') -or - -not $terminalOutputCaptureText.Contains('pub const capacity = semantic_output.capacity;') -or - -not $surfaceRuntimeText.Contains('pub const capacity = semantic_output.capacity;') -or - $terminalOutputCaptureText.Contains('recordRepeat') -or - $terminalStreamHandlerText.Contains('recordRepeat')) { - throw 'Semantic capture and transport must share one 8000-byte capacity and no duplicate REP interface.' -} - -$realParserSemanticFragments = @( - 'stream.nextSlice("\x1b[3b");', - '\x1b[31mright', - '\x1b]8;;https://secret.example', - '\x1bPqDCS-SECRET', - '\x1b[1$}\r\n\thidden\x1b[0$}visible', - '\x1b(0`\x1b(B', - 'before\x1bcafter', - 'stream.nextSlice("\xcc\x81");' -) -foreach ($fragment in $realParserSemanticFragments) { - if (-not $terminalStreamHandlerText.Contains($fragment)) { - throw "Real-parser semantic fixture is missing executable coverage fragment: $fragment" - } -} - -Assert-ZigTestsDiscoveredAndRun ` - -RepoRoot $repoRoot ` - -Sources @{ - $terminalOutputCapture = $terminalOutputCaptureText - $terminalStreamHandler = $terminalStreamHandlerText - } ` - -ExpectedNames @( - 'semantic output capture uninterested fast path', - 'semantic output capture preserves utf8 and control order', - 'semantic output finished batch owns bytes across capture reuse', - 'semantic output full reset discards prior bytes and omission', - 'semantic output capture retains codepoint-aligned prefix before omission', - 'semantic output capture explicit partial error omission retains prefix', - 'semantic output capture follows real stream handler parser' - ) ` - -Filter 'semantic output capture' - -Assert-ZigTestsDiscoveredAndRun ` - -RepoRoot $repoRoot ` - -Sources @{ $surfaceRuntime = $surfaceRuntimeText } ` - -ExpectedNames @( - 'terminal output transport saturation is nonblocking and ordered', - 'terminal output transport keeps utf8 chunks before batch omission marker', - 'terminal output transport reentrant callbacks preserve epochs and contention marker', - 'terminal output transport rejects stale interest epoch without poisoning new epoch', - 'terminal output transport orders silent reset before post reset data' - ) ` - -Filter 'terminal output transport' - -Assert-ZigTestsDiscoveredAndRun ` - -RepoRoot $repoRoot ` - -Sources @{ $terminalAccessibility = $terminalAccessibilityText } ` - -ExpectedNames @( - 'terminal accessibility refresh and query policies' - ) ` - -Filter 'terminal accessibility refresh and query policies' - -Assert-ZigTestsDiscoveredAndRun ` - -RepoRoot $repoRoot ` - -Sources @{ $terminalAccessibility = $terminalAccessibilityText } ` - -ExpectedNames @( - 'terminal output announcement normalization allocation failure becomes ordered omission' - ) ` - -Filter 'normalization allocation failure becomes ordered omission' - -Assert-ZigTestsDiscoveredAndRun ` - -RepoRoot $repoRoot ` - -Sources @{ $win32UiaWidgets = $win32UiaWidgetsText } ` - -ExpectedNames @( - 'TerminalProvider legacy caret compatibility reports no mutable selection' - ) ` - -Filter 'legacy caret compatibility reports no mutable selection' -Assert-ZigTestsDiscoveredAndRun ` - -RepoRoot $repoRoot ` - -Sources @{ $win32UiaWidgets = $win32UiaWidgetsText } ` - -ExpectedNames @( - 'TerminalTextRangeProvider reports unsupported mutation and scrolling honestly' - ) ` - -Filter 'unsupported mutation and scrolling honestly' - -if (-not $win32ThemeText.Contains('const DWMSBT_NONE: u32 = 1;')) { - throw 'Win32 theme policy must use the documented DWMSBT_NONE value 1.' -} -Assert-ZigTestsDiscoveredAndRun ` - -RepoRoot $repoRoot ` - -Sources @{ $win32Theme = $win32ThemeText } ` - -ExpectedNames @( - 'settings window policy explicitly disables system backdrop', - 'DWM system backdrop constants match Win32 ABI' - ) ` - -Filter 'system backdrop' - -$publishedReleaseVerifierTokens = $null -$publishedReleaseVerifierErrors = $null -[void][System.Management.Automation.Language.Parser]::ParseInput( - $publishedReleaseVerifierText, - [ref]$publishedReleaseVerifierTokens, - [ref]$publishedReleaseVerifierErrors -) -if ($publishedReleaseVerifierErrors.Count -ne 0) { - throw "Published release verifier does not parse: $($publishedReleaseVerifierErrors[0].Message)" -} -$resolutionSourceAsts = foreach ($source in @( - [pscustomobject]@{ Path = $interactiveWin11Lib; Text = $interactiveWin11LibText }, - [pscustomobject]@{ Path = $statefulWin11Lib; Text = $statefulWin11LibText } -)) { - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput($source.Text, [ref]$tokens, [ref]$errors) - if ($errors.Count -ne 0) { throw "PowerShell resolution source does not parse: $($source.Path) ($($errors[0].Message))" } - [pscustomobject]@{ Path = $source.Path; Ast = $ast; Tokens = $tokens } -} -foreach ($source in $resolutionSourceAsts) { - $expectedAmpersands = if ($source.Path -eq $interactiveWin11Lib) { - @( - '& $bootstrapCmd powershell.exe -ExecutionPolicy Bypass -File $LauncherPath @ArgumentList', - '& cmd /c $devWindowsCmd zig build -Demit-exe=true', - '& $Condition' - ) - } else { @() } - Assert-CommandResolutionContract -Ast $source.Ast -Tokens $source.Tokens -Context $source.Path -ExpectedAmpersandCommands $expectedAmpersands - $definitions = @($source.Ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) - foreach ($name in @('Get-InteractiveWin11MessageTimeoutMs', 'Assert-InteractiveWin11WindowOwner', 'Invoke-InteractiveWin11PostMessage', 'Invoke-StatefulPostedCommand')) { - $expected = if ($source.Path -eq $interactiveWin11Lib) { - if ($name -eq 'Invoke-StatefulPostedCommand') { 0 } else { 1 } - } else { - if ($name -eq 'Invoke-StatefulPostedCommand') { 1 } else { 0 } - } - if (@($definitions | Where-Object { ($_.Name -replace '^(?i)(?:global|script|local|private):', '') -eq $name }).Count -ne $expected) { - throw "Interactive library has the wrong ownership count for protected function $name`: $($source.Path)" - } - } -} -$interactiveWin11LibAst = @($resolutionSourceAsts | Where-Object { $_.Path -eq $interactiveWin11Lib })[0].Ast -$stopProcessFunctions = @($interactiveWin11LibAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq 'Stop-InteractiveWin11Process' -}, $true)) -$processTreeSnapshotFunctions = @($interactiveWin11LibAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Get-InteractiveWin11ProcessTreeSnapshot' - }, $true)) -$processTreeExitedFunctions = @($interactiveWin11LibAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Test-InteractiveWin11ProcessTreeSnapshotExited' - }, $true)) -$waitProcessTreeExitedFunctions = @($interactiveWin11LibAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Wait-InteractiveWin11ProcessTreeSnapshotExited' - }, $true)) -$stopRootHandleFunctions = @($interactiveWin11LibAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Stop-InteractiveWin11RootHandle' - }, $true)) -if ($stopProcessFunctions.Count -ne 1 -or - $processTreeSnapshotFunctions.Count -ne 1 -or - $processTreeExitedFunctions.Count -ne 1 -or - $waitProcessTreeExitedFunctions.Count -ne 1 -or - $stopRootHandleFunctions.Count -ne 1 -or - -not [object]::ReferenceEquals($stopProcessFunctions[0].Parent, $interactiveWin11LibAst.EndBlock) -or - -not [object]::ReferenceEquals($processTreeSnapshotFunctions[0].Parent, $interactiveWin11LibAst.EndBlock) -or - -not [object]::ReferenceEquals($processTreeExitedFunctions[0].Parent, $interactiveWin11LibAst.EndBlock) -or - -not [object]::ReferenceEquals($waitProcessTreeExitedFunctions[0].Parent, $interactiveWin11LibAst.EndBlock) -or - -not [object]::ReferenceEquals($stopRootHandleFunctions[0].Parent, $interactiveWin11LibAst.EndBlock) -or - @($interactiveWin11LibAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.TrapStatementAst] - }, $true)).Count -ne 0 -or - $null -ne $stopProcessFunctions[0].Body.BeginBlock -or - $null -ne $stopProcessFunctions[0].Body.ProcessBlock -or - $null -ne $stopProcessFunctions[0].Body.DynamicParamBlock -or - $null -ne $stopProcessFunctions[0].Body.CleanBlock -or - $null -eq $stopProcessFunctions[0].Body.ParamBlock -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters.Count -ne 4 -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[0].Name.VariablePath.UserPath -ne 'Process' -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[0].Attributes.Count -ne 2 -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[0].Attributes[0].Extent.Text.Trim() -ne '[Parameter(Mandatory)]' -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[0].Attributes[1].Extent.Text.Trim() -ne '[System.Diagnostics.Process]' -or - $null -ne $stopProcessFunctions[0].Body.ParamBlock.Parameters[0].DefaultValue -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[1].Name.VariablePath.UserPath -ne 'RequireLiveRoot' -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[1].Attributes.Count -ne 1 -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[1].Attributes[0].Extent.Text.Trim() -ne '[switch]' -or - $null -ne $stopProcessFunctions[0].Body.ParamBlock.Parameters[1].DefaultValue -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[2].Name.VariablePath.UserPath -ne 'Contained' -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[2].Attributes[0].Extent.Text.Trim() -ne '[switch]' -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[3].Name.VariablePath.UserPath -ne 'AllowAlreadyExited' -or - $stopProcessFunctions[0].Body.ParamBlock.Parameters[3].Attributes[0].Extent.Text.Trim() -ne '[switch]') { - throw 'Interactive process cleanup must expose explicit contained, live-root, and already-exited contracts.' -} -$snapshotCimCommands = @($processTreeSnapshotFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Get-CimInstance' - }, $true)) -$verificationCimCommands = @($processTreeExitedFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Get-CimInstance' - }, $true)) -$snapshotCalls = @($stopProcessFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Get-InteractiveWin11ProcessTreeSnapshot' - }, $true)) -$rootStopCalls = @($stopProcessFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Stop-InteractiveWin11RootHandle' - }, $true)) -$waitCalls = @($stopProcessFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Wait-InteractiveWin11ProcessTreeSnapshotExited' - }, $true)) -$stopProcessText = $stopProcessFunctions[0].Extent.Text -$rootTerminateCalls = @($stopRootHandleFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and - $node.Extent.Text -eq '[InteractiveWin11ProcessNative]::TerminateProcess($RootProcessHandle, 1)' - }, $true)) -$rootWaitCalls = @($stopRootHandleFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and - $node.Extent.Text -eq '[InteractiveWin11ProcessNative]::WaitForSingleObject($RootProcessHandle, 15000)' - }, $true)) -if ($snapshotCimCommands.Count -ne 1 -or - $verificationCimCommands.Count -ne 1 -or - $snapshotCimCommands[0].Extent.Text -notmatch '(?s)-ClassName\s+Win32_Process\s+-OperationTimeoutSec\s+5\s+-ErrorAction\s+Stop' -or - $verificationCimCommands[0].Extent.Text -notmatch '(?s)-ClassName\s+Win32_Process.*?-OperationTimeoutSec\s+\$OperationTimeoutSec.*?-ErrorAction\s+Stop' -or - $snapshotCimCommands[0].Extent.Text -match '(?i)-Filter\b' -or - $verificationCimCommands[0].Extent.Text -match '(?i)-Filter\b' -or - @($processTreeSnapshotFunctions[0].FindAll({ param($node) $node -is [System.Management.Automation.Language.CatchClauseAst] }, $true)).Count -ne 0 -or - @($processTreeExitedFunctions[0].FindAll({ param($node) $node -is [System.Management.Automation.Language.CatchClauseAst] }, $true)).Count -ne 0 -or - $snapshotCalls.Count -ne 1 -or - $rootStopCalls.Count -ne 1 -or - $waitCalls.Count -ne 1 -or - $snapshotCalls[0].Extent.StartOffset -ge $rootStopCalls[0].Extent.StartOffset -or - $rootStopCalls[0].Extent.StartOffset -ge $waitCalls[0].Extent.StartOffset -or - $rootTerminateCalls.Count -ne 1 -or - $rootWaitCalls.Count -ne 1 -or - $stopProcessText -notmatch '(?s)if \(-not \$Contained\).*?taskkill\.exe.*?WaitForExit\(10000\).*?Kill\(\).*?WaitForExit\(5000\)' -or - $stopProcessText -notmatch '(?s)\$taskkillCleanupError.*?if \(\$null -ne \$taskkillCleanupError\)\s*\{\s*throw' -or - $stopProcessText -notmatch '(?s)if \(\$Contained -and \$null -ne \$snapshotError\).*?return' -or - $stopProcessText -notmatch '(?s)\$lifecycleModeCount.*?if \(\$lifecycleModeCount -gt 1\).*?mutually exclusive' -or - $stopProcessText -notmatch '(?s)if \(\$null -ne \$verificationError\).*?if \(\$Contained\).*?return.*?throw' -or - $waitProcessTreeExitedFunctions[0].Extent.Text -notmatch '(?s)\$deadline\s*=\s*\[DateTime\]::UtcNow\.AddSeconds\(\$TimeoutSeconds\).*?while \(\[DateTime\]::UtcNow -lt \$deadline\)') { - throw 'Interactive cleanup must use bounded uncontained tree kill, contained Job cleanup, native root termination, and deadline-bound verification.' -} - -. ([scriptblock]::Create($processTreeSnapshotFunctions[0].Extent.Text)) -. ([scriptblock]::Create($processTreeExitedFunctions[0].Extent.Text)) -$script:verificationCimProcesses = @() -$script:verificationCimFailure = $null -function script:Get-CimInstance { - [CmdletBinding()] - param( - [Parameter(Mandatory)] [string] $ClassName, - [string] $Filter, - [uint32] $OperationTimeoutSec - ) - - if ($ClassName -ne 'Win32_Process' -or - $PSBoundParameters.ContainsKey('Filter') -or - $OperationTimeoutSec -lt 1 -or - $OperationTimeoutSec -gt 5) { - throw 'Verification mock requires one bounded, unfiltered Win32_Process query.' - } - if ($null -ne $script:verificationCimFailure) { - throw $script:verificationCimFailure - } - $script:verificationCimProcesses -} - -try { - $rootCreated = [datetime]'2026-07-14T15:00:00Z' - $childCreated = $rootCreated.AddSeconds(1) - $grandchildCreated = $rootCreated.AddSeconds(2) - $script:verificationCimProcesses = @( - [pscustomobject]@{ ProcessId = 100; ParentProcessId = 4; CreationDate = $rootCreated }, - [pscustomobject]@{ ProcessId = 101; ParentProcessId = 100; CreationDate = $childCreated }, - [pscustomobject]@{ ProcessId = 102; ParentProcessId = 101; CreationDate = $grandchildCreated }, - [pscustomobject]@{ ProcessId = 103; ParentProcessId = 100; CreationDate = $rootCreated.AddMinutes(-1) }, - [pscustomobject]@{ ProcessId = 200; ParentProcessId = 4; CreationDate = $rootCreated } - ) - $snapshot = @(Get-InteractiveWin11ProcessTreeSnapshot -RootProcessId 100 -RootStartedAt $rootCreated) - if ((@($snapshot.ProcessId | Sort-Object) -join ',') -ne '100,101,102') { - throw 'Interactive process snapshot did not close over the full descendant tree.' - } - if (Test-InteractiveWin11ProcessTreeSnapshotExited -Snapshot $snapshot) { - throw 'Interactive process verification accepted captured identities that were still live.' - } - - $script:verificationCimProcesses = @( - [pscustomobject]@{ ProcessId = 100; ParentProcessId = 4; CreationDate = $rootCreated.AddMinutes(1) }, - [pscustomobject]@{ ProcessId = 200; ParentProcessId = 4; CreationDate = $rootCreated } - ) - if (-not (Test-InteractiveWin11ProcessTreeSnapshotExited -Snapshot $snapshot)) { - throw 'Interactive process verification confused a reused PID with a captured identity.' - } - - $script:verificationCimProcesses = @( - [pscustomobject]@{ ProcessId = 201; ParentProcessId = 100; CreationDate = $rootCreated.AddMinutes(2) } - ) - if (Test-InteractiveWin11ProcessTreeSnapshotExited -Snapshot $snapshot) { - throw 'Interactive process verification missed a child created after the snapshot.' - } - $script:verificationCimProcesses = @( - [pscustomobject]@{ ProcessId = 201; ParentProcessId = 100; CreationDate = $rootCreated.AddMinutes(-1) } - ) - if (-not (Test-InteractiveWin11ProcessTreeSnapshotExited -Snapshot $snapshot)) { - throw 'Interactive process verification confused a stale child from a reused parent PID with a live descendant.' - } - - $script:verificationCimProcesses = @( - [pscustomobject]@{ ProcessId = 200; ParentProcessId = 4; CreationDate = $rootCreated } - ) - $missingRootRejected = $false - try { [void](Get-InteractiveWin11ProcessTreeSnapshot -RootProcessId 100 -RootStartedAt $rootCreated) } catch { $missingRootRejected = $true } - if (-not $missingRootRejected) { - throw 'Interactive process snapshot accepted a missing root identity.' - } - - $script:verificationCimProcesses = @( - [pscustomobject]@{ ProcessId = 100; ParentProcessId = 4; CreationDate = $rootCreated.AddMinutes(1) } - ) - $reusedRootRejected = $false - try { [void](Get-InteractiveWin11ProcessTreeSnapshot -RootProcessId 100 -RootStartedAt $rootCreated) } catch { $reusedRootRejected = $true } - if (-not $reusedRootRejected) { - throw 'Interactive process snapshot accepted a reused root PID.' - } - - $script:verificationCimFailure = 'simulated CIM failure' - $snapshotFailureRejected = $false - try { [void](Get-InteractiveWin11ProcessTreeSnapshot -RootProcessId 100 -RootStartedAt $rootCreated) } catch { $snapshotFailureRejected = $true } - $verificationFailureRejected = $false - try { [void](Test-InteractiveWin11ProcessTreeSnapshotExited -Snapshot $snapshot) } catch { $verificationFailureRejected = $true } - if (-not $snapshotFailureRejected -or -not $verificationFailureRejected) { - throw 'Interactive process cleanup treated a CIM failure as proof of exit.' - } -} -finally { - Remove-Item -LiteralPath Function:\Get-CimInstance -ErrorAction SilentlyContinue - Remove-Variable -Scope Script -Name verificationCimProcesses, verificationCimFailure -ErrorAction SilentlyContinue -} - -. ([scriptblock]::Create($stopProcessFunctions[0].Extent.Text)) -$script:cleanupSnapshotFailure = $false -$script:cleanupVerificationFailure = $false -$script:cleanupVerificationExited = $true -function script:Get-InteractiveWin11ProcessTreeSnapshot { - param([int] $RootProcessId, [datetime] $RootStartedAt) - if ($script:cleanupSnapshotFailure) { throw 'simulated snapshot failure' } - @([pscustomobject]@{ ProcessId = $RootProcessId; CreationDate = $RootStartedAt }) -} -function script:Stop-InteractiveWin11RootHandle { - param( - [System.Diagnostics.Process] $Process, - [IntPtr] $RootProcessHandle, - [datetime] $RootStartedAt - ) - $Process.Kill() - if (-not $Process.WaitForExit(5000)) { throw 'mock root process did not exit' } -} -function script:Wait-InteractiveWin11ProcessTreeSnapshotExited { - param([object[]] $Snapshot) - if ($script:cleanupVerificationFailure) { throw 'simulated verification query failure' } - $script:cleanupVerificationExited -} -function script:Start-CleanupContractProcess { - $startInfo = [System.Diagnostics.ProcessStartInfo]::new() - $startInfo.FileName = Join-Path $PSHOME 'pwsh.exe' - $startInfo.UseShellExecute = $false - $startInfo.CreateNoWindow = $true - $startInfo.ArgumentList.Add('-NoProfile') - $startInfo.ArgumentList.Add('-Command') - $startInfo.ArgumentList.Add('Start-Sleep -Seconds 30') - [System.Diagnostics.Process]::Start($startInfo) -} -try { - $process = Start-CleanupContractProcess - $script:cleanupVerificationFailure = $true - Stop-InteractiveWin11Process -Process $process -Contained - if (-not $process.HasExited) { - throw 'Contained cleanup did not terminate the root before accepting unavailable verification.' - } - $process.Dispose() - $process = $null - - $process = Start-CleanupContractProcess - $script:cleanupVerificationFailure = $false - $script:cleanupVerificationExited = $false - $liveDescendantRejected = $false - try { Stop-InteractiveWin11Process -Process $process -Contained } catch { $liveDescendantRejected = $true } - if (-not $liveDescendantRejected -or -not $process.HasExited) { - throw 'Contained cleanup accepted a successful verification query that reported live descendants.' - } - $process.Dispose() - $process = $null - - $process = Start-CleanupContractProcess - $script:cleanupSnapshotFailure = $true - $script:cleanupVerificationExited = $true - Stop-InteractiveWin11Process -Process $process -Contained - if (-not $process.HasExited) { - throw 'Contained cleanup did not terminate the root after initial snapshot failure.' - } - $process.Dispose() - $process = $null - - $process = Start-CleanupContractProcess - $script:cleanupSnapshotFailure = $true - Stop-InteractiveWin11Process -Process $process -AllowAlreadyExited - if (-not $process.HasExited) { - throw 'Already-exited cleanup did not terminate the root after a snapshot race.' - } - $process.Dispose() - $process = $null - - $conflictingModesRejected = $false - try { - Stop-InteractiveWin11Process ` - -Process ([System.Diagnostics.Process]::GetCurrentProcess()) ` - -Contained ` - -RequireLiveRoot - } - catch { $conflictingModesRejected = $true } - if (-not $conflictingModesRejected) { - throw 'Interactive cleanup accepted conflicting lifecycle modes.' - } -} -finally { - if ($null -ne $process) { - if (-not $process.HasExited) { $process.Kill() } - $process.Dispose() - } - Remove-Item -LiteralPath Function:\Get-InteractiveWin11ProcessTreeSnapshot -ErrorAction SilentlyContinue - Remove-Item -LiteralPath Function:\Stop-InteractiveWin11RootHandle -ErrorAction SilentlyContinue - Remove-Item -LiteralPath Function:\Wait-InteractiveWin11ProcessTreeSnapshotExited -ErrorAction SilentlyContinue - Remove-Item -LiteralPath Function:\Start-CleanupContractProcess -ErrorAction SilentlyContinue - Remove-Item -LiteralPath Function:\Stop-InteractiveWin11Process -ErrorAction SilentlyContinue - Remove-Variable -Scope Script -Name cleanupSnapshotFailure, cleanupVerificationFailure, cleanupVerificationExited -ErrorAction SilentlyContinue -} - -$containmentFunctions = @($interactiveWin11LibAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Get-InteractiveWin11ContainmentArguments' - }, $true)) -$launchArgumentFunctions = @($interactiveWin11LibAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Get-InteractiveWin11LaunchArguments' - }, $true)) -if ($containmentFunctions.Count -ne 1 -or - $launchArgumentFunctions.Count -ne 1 -or - $containmentFunctions[0].Extent.Text -notmatch [regex]::Escape("'--linux-cgroup=always'") -or - $containmentFunctions[0].Extent.Text -notmatch [regex]::Escape("'--linux-cgroup-hard-fail=true'") -or - $containmentFunctions[0].Extent.Text -notmatch [regex]::Escape("'--windows-job-object-kill-on-close=true'") -or - $launchArgumentFunctions[0].Extent.Text -notmatch '\bGet-InteractiveWin11ContainmentArguments\b') { - throw 'Interactive Win11 launches must opt into hard-fail kill-on-close Job Object containment.' -} -$interactiveLibraryTest = Get-Content -LiteralPath (Join-Path $repoRoot 'test\windows\interactive-win11.ps1') -Raw -if ($interactiveLibraryTest -notmatch [regex]::Escape("Assert-Equal `$launchArgs.Count 5 'launch args should include containment and isolation overrides'") -or - $interactiveLibraryTest -notmatch [regex]::Escape("Assert-True (`$launchArgs -contains '--linux-cgroup=always')") -or - $interactiveLibraryTest -notmatch [regex]::Escape("Assert-True (`$launchArgs -contains '--linux-cgroup-hard-fail=true')") -or - $interactiveLibraryTest -notmatch [regex]::Escape("Assert-True (`$launchArgs -contains '--windows-job-object-kill-on-close=true')")) { - throw 'Interactive helper tests must assert all containment and isolation launch arguments.' -} -$interactiveHarnessFiles = @( - Get-ChildItem -LiteralPath (Join-Path $repoRoot 'test\windows') -Filter 'interactive-win11-*.ps1' -File -) -$launchContainmentViolations = [Collections.Generic.List[string]]::new() -$forbiddenConfigOverrides = [Collections.Generic.List[string]]::new() -foreach ($file in @($interactiveHarnessFiles) + @(Get-Item -LiteralPath $interactiveWin11Lib)) { - $content = Get-Content -LiteralPath $file.FullName -Raw - foreach ($match in [regex]::Matches($content, '[''"]--single-instance=false[''"]')) { - $prefixStart = [math]::Max(0, $match.Index - 512) - $prefix = $content.Substring($prefixStart, $match.Index - $prefixStart) - if ($prefix -notmatch '\bGet-InteractiveWin11(?:Launch|Containment)Arguments\b') { - [void]$launchContainmentViolations.Add("$($file.Name):$($match.Index)") - } - } - if ($file.FullName -ne $interactiveWin11Lib -and - $content -match '(?im)^\s*(?:linux-cgroup|linux-cgroup-hard-fail|windows-job-object-kill-on-close)\s*=') { - [void]$forbiddenConfigOverrides.Add($file.Name) - } -} -if ($launchContainmentViolations.Count -ne 0) { - throw "Interactive launch arguments bypass Job Object containment: $($launchContainmentViolations -join ', ')" -} -if ($forbiddenConfigOverrides.Count -ne 0) { - throw "Interactive harness config overrides CLI containment: $($forbiddenConfigOverrides -join ', ')" -} - -$cleanupContractViolations = [Collections.Generic.List[string]]::new() -$cleanupScriptFiles = @( - Get-ChildItem -LiteralPath (Join-Path $repoRoot 'test\windows') -Filter '*.ps1' -File -Recurse | - Where-Object { $_.FullName -ne $PSCommandPath } -) -foreach ($file in $cleanupScriptFiles) { - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$errors) - if ($errors.Count -ne 0) { - throw "PowerShell parser errors while checking cleanup contract: $($file.FullName)" - } - foreach ($command in @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Stop-InteractiveWin11Process' - }, $true))) { - $text = $command.Extent.Text - $modeCount = @( - $text -match '(?i)(?:^|\s)-Contained(?:\s|$)' - $text -match '(?i)(?:^|\s)-RequireLiveRoot(?:\s|$)' - $text -match '(?i)(?:^|\s)-AllowAlreadyExited(?:\s|$)' - ).Where({ $_ }).Count - if ($modeCount -ne 1 -or - ($text -match '(?i)(?:^|\s)-AllowAlreadyExited(?:\s|$)' -and $file.Name -ne 'vt-probe-win32-conformance.ps1')) { - [void]$cleanupContractViolations.Add("$($file.Name):$($command.Extent.StartLineNumber): $text") - } - } -} -if ($cleanupContractViolations.Count -ne 0) { - throw "Interactive cleanup calls lack exactly one explicit lifecycle contract:`n$($cleanupContractViolations -join "`n")" -} -$jobObjectSource = Get-Content -LiteralPath (Join-Path $repoRoot 'src\apprt\win32_job_object.zig') -Raw -$commandSource = Get-Content -LiteralPath (Join-Path $repoRoot 'src\Command.zig') -Raw -if ($jobObjectSource -notmatch 'JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE' -or - $jobObjectSource -notmatch 'kill_on_close' -or - $commandSource -notmatch '(?s)CREATE_SUSPENDED.*?AssignProcessToJobObject.*?ResumeThread') { - throw 'Windows child containment must assign the suspended process to a kill-on-close Job Object before resume.' -} - -Assert-TextContract ` - -Content $interactiveWin11LibText ` - -Pattern ([regex]::Escape('throw "Interactive Win11 root process $RootProcessId was absent from the process-table snapshot."')) ` - -Description 'interactive cleanup fails closed if root is absent from process snapshot' ` - -Context $interactiveWin11Lib -Assert-TextContract ` - -Content $interactiveWin11LibText ` - -Pattern ([regex]::Escape('throw ''Interactive Win11 process-tree verification requires a non-empty snapshot.''')) ` - -Description 'interactive cleanup rejects vacuous empty-snapshot verification' ` - -Context $interactiveWin11Lib -Assert-TextContract ` - -Content $interactiveWin11LibText ` - -Pattern ([regex]::Escape('$capturedStartedAtById[$parentProcessId]')) ` - -Description 'interactive cleanup fails closed on identity-matched descendants created after snapshot' ` - -Context $interactiveWin11Lib -Assert-TextContract ` - -Content $interactiveWin11LibText ` - -Pattern ([regex]::Escape('$terminationRequested = [InteractiveWin11ProcessNative]::TerminateProcess($RootProcessHandle, 1)')) ` - -Description 'interactive cleanup terminates only the captured root handle' ` - -Context $interactiveWin11Lib -Assert-TextContract ` - -Content $interactiveWin11LibText ` - -Pattern ([regex]::Escape('throw "Failed to verify cleanup of interactive Win11 process tree $rootProcessId')) ` - -Description 'interactive cleanup still fails closed on unverified process tree' ` - -Context $interactiveWin11Lib -Assert-TextContract ` - -Content $interactiveWin11LibText ` - -Pattern ([regex]::Escape('public static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);')) ` - -Description 'native root fallback termination declaration' ` - -Context $interactiveWin11Lib -Assert-TextContract ` - -Content $interactiveWin11LibText ` - -Pattern ([regex]::Escape('public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);')) ` - -Description 'native root termination wait declaration' ` - -Context $interactiveWin11Lib -$expectedCliShellHarnessText = @' -param( - [Parameter(Mandatory = $true)] - [ValidateSet('cmd', 'powershell')] - [string] $Shell, - - [Parameter(Mandatory = $true)] - [string[]] $Arguments, - - [Parameter(Mandatory = $true)] - [string] $ExpectedText, - - [int] $ExpectedExitCode = 0, - - [string] $BinDir -) - -$ErrorActionPreference = 'Stop' - -$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) -. (Join-Path $repoRoot 'scripts\interactive-win11-lib.ps1') - -function Format-CmdArgument { - param( - [Parameter(Mandatory = $true)] - [AllowEmptyString()] - [string] $Argument - ) - - if ($Argument.Length -eq 0) { - return '""' - } - - $escaped = $Argument.Replace('%', '%%').Replace('"', '""') - if ($escaped -match '[\s"&|<>()^!]') { - return '"' + $escaped + '"' - } - - return $escaped -} - -function Format-PowerShellLiteral { - param( - [Parameter(Mandatory = $true)] - [AllowEmptyString()] - [string] $Argument - ) - - return "'" + $Argument.Replace("'", "''") + "'" -} - -$binDir = [System.IO.Path]::GetFullPath($(if ($BinDir) { $BinDir } else { Join-Path $repoRoot 'zig-out\bin' })) -$guiExe = Join-Path $binDir 'noctty.exe' -$commandExe = Join-Path $binDir 'noctty.com' -$cmdExe = Join-Path ([Environment]::SystemDirectory) 'cmd.exe' -$powershellExe = Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell\v1.0\powershell.exe' - -foreach ($requiredExecutable in @($guiExe, $commandExe, $cmdExe, $powershellExe)) { - if (-not (Test-Path -LiteralPath $requiredExecutable -PathType Leaf)) { - throw "Missing required executable: $requiredExecutable. Run `zig build -Demit-exe=true` if the noctty binaries are absent." - } -} - -$envPath = "$binDir;$env:PATH" -$argsDisplay = [string]::Join(' ', $Arguments) -$shellLauncherTimeoutSeconds = 30 - -switch ($Shell) { - 'cmd' { - $resolved = & $cmdExe /d /c "set ""PATH=$envPath""&& where noctty" - if ($LASTEXITCODE -ne 0) { - throw "cmd could not resolve noctty from PATH." - } - $resolvedPath = [System.IO.Path]::GetFullPath(($resolved | Select-Object -First 1)) - if (-not [string]::Equals($resolvedPath, $commandExe, [StringComparison]::OrdinalIgnoreCase)) { - throw "cmd resolved noctty to the wrong artifact: $resolvedPath" - } - - $stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) ("noctty-cli-shell-" + [System.Guid]::NewGuid().ToString("N") + "-stdout.txt") - $stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) ("noctty-cli-shell-" + [System.Guid]::NewGuid().ToString("N") + "-stderr.txt") - $payloadPath = Join-Path ([System.IO.Path]::GetTempPath()) ("noctty-cli-shell-" + [System.Guid]::NewGuid().ToString("N") + ".cmd") - try { - $cmdArgs = [string]::Join(' ', ($Arguments | ForEach-Object { Format-CmdArgument $_ })) - $cmdCommand = if ([string]::IsNullOrEmpty($cmdArgs)) { 'noctty' } else { "noctty $cmdArgs" } - @( - '@echo off' - "set `"PATH=$envPath`"" - $cmdCommand - ) | Set-Content -LiteralPath $payloadPath -Encoding ASCII - - $process = Start-Process ` - -FilePath $cmdExe ` - -ArgumentList "/d /c `"$payloadPath`"" ` - -RedirectStandardOutput $stdoutPath ` - -RedirectStandardError $stderrPath ` - -WindowStyle Hidden ` - -PassThru - $processHandle = $process.Handle - if (-not $process.WaitForExit($shellLauncherTimeoutSeconds * 1000)) { - Stop-InteractiveWin11Process -Process $process -RequireLiveRoot - throw "Timed out waiting $shellLauncherTimeoutSeconds seconds for shell launcher process to exit." - } - - $exitCode = Get-InteractiveWin11ProcessExitCode -Process $process -ProcessHandle $processHandle - $stdoutText = if (Test-Path -LiteralPath $stdoutPath) { - Get-Content -LiteralPath $stdoutPath -Raw - } else { - '' - } - $stderrText = if (Test-Path -LiteralPath $stderrPath) { - Get-Content -LiteralPath $stderrPath -Raw - } else { - '' - } - $output = $stdoutText + $stderrText - } - finally { - Remove-Item -LiteralPath $stdoutPath, $stderrPath, $payloadPath -ErrorAction SilentlyContinue - } - } - - 'powershell' { - $oldPath = $env:PATH - $env:PATH = $envPath - try { - $resolved = & $powershellExe -NoProfile -Command "(Get-Command noctty).Source" - if ($LASTEXITCODE -ne 0) { - throw "PowerShell could not resolve noctty from PATH." - } - $resolvedPath = [System.IO.Path]::GetFullPath($resolved) - if (-not [string]::Equals($resolvedPath, $commandExe, [StringComparison]::OrdinalIgnoreCase)) { - throw "PowerShell resolved noctty to the wrong artifact: $resolvedPath" - } - - $stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) ("noctty-cli-shell-" + [System.Guid]::NewGuid().ToString("N") + "-stdout.txt") - $stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) ("noctty-cli-shell-" + [System.Guid]::NewGuid().ToString("N") + "-stderr.txt") - $payloadPath = Join-Path ([System.IO.Path]::GetTempPath()) ("noctty-cli-shell-" + [System.Guid]::NewGuid().ToString("N") + ".ps1") - try { - $env:PATH = $envPath - $argLiterals = [string]::Join(', ', ($Arguments | ForEach-Object { Format-PowerShellLiteral $_ })) - @( - '$argsList = @(' + $argLiterals + ')' - '$output = & noctty @argsList | Out-String' - '$exitCode = $LASTEXITCODE' - '[Console]::Out.Write($output)' - 'exit $exitCode' - ) | Set-Content -LiteralPath $payloadPath -Encoding UTF8 - - $process = Start-Process ` - -FilePath $powershellExe ` - -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $payloadPath) ` - -RedirectStandardOutput $stdoutPath ` - -RedirectStandardError $stderrPath ` - -WindowStyle Hidden ` - -PassThru - $processHandle = $process.Handle - if (-not $process.WaitForExit($shellLauncherTimeoutSeconds * 1000)) { - Stop-InteractiveWin11Process -Process $process -RequireLiveRoot - throw "Timed out waiting $shellLauncherTimeoutSeconds seconds for shell launcher process to exit." - } - - $exitCode = Get-InteractiveWin11ProcessExitCode -Process $process -ProcessHandle $processHandle - $stdoutText = if (Test-Path -LiteralPath $stdoutPath) { - Get-Content -LiteralPath $stdoutPath -Raw - } else { - '' - } - $stderrText = if (Test-Path -LiteralPath $stderrPath) { - Get-Content -LiteralPath $stderrPath -Raw - } else { - '' - } - $output = $stdoutText + $stderrText - } - finally { - Remove-Item -LiteralPath $stdoutPath, $stderrPath, $payloadPath -ErrorAction SilentlyContinue - } - } - finally { - $env:PATH = $oldPath - } - } -} - -if ($exitCode -ne $ExpectedExitCode) { - throw "$Shell shell launcher should exit with code $ExpectedExitCode, got $exitCode." -} - -$outputText = if ($output -is [string]) { $output } else { ($output | Out-String) } -if (-not $outputText.Contains($ExpectedText)) { - throw "$Shell shell launcher output did not contain expected text '$ExpectedText'." -} - -Write-Host "shell launcher validation: PASS (shell=$Shell, args=$argsDisplay)" -'@ -if ((($cliShellHarnessText -replace "\r\n?", "`n") -replace '\n+\z', '') -cne - (($expectedCliShellHarnessText -replace "\r\n?", "`n") -replace '\n+\z', '')) { - throw 'CLI shell harness must match its complete reviewed source snapshot.' -} -$cliShellTokens = $null -$cliShellErrors = $null -$cliShellAst = [System.Management.Automation.Language.Parser]::ParseInput( - $cliShellHarnessText, - [ref]$cliShellTokens, - [ref]$cliShellErrors -) -if ($cliShellErrors.Count -ne 0) { throw 'CLI shell harness must parse without errors.' } -Assert-CommandResolutionContract -Ast $cliShellAst -Tokens $cliShellTokens -Context $cliShellHarness -ExpectedDotSources @( - ". (Join-Path `$repoRoot 'scripts\interactive-win11-lib.ps1')" -) -ExpectedAmpersandCommands @( - '& $cmdExe /d /c "set ""PATH=$envPath""&& where noctty"' - '& $powershellExe -NoProfile -Command "(Get-Command noctty).Source"' -) -$accessibilityTokens = $null -$accessibilityErrors = $null -$accessibilityAst = [System.Management.Automation.Language.Parser]::ParseInput( - $accessibilityHarnessText, - [ref]$accessibilityTokens, - [ref]$accessibilityErrors -) -$accessibilityUIntPtrConversions = @($accessibilityAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ConvertExpressionAst] -and - $node.Type.TypeName.FullName -eq 'UIntPtr' -}, $true)) -if ($accessibilityErrors.Count -ne 0 -or $accessibilityUIntPtrConversions.Count -ne 0) { - throw 'Accessibility harness must parse and construct nonzero WPARAM values through UIntPtr::new([uint64] ...).' -} -$textRangeEndpointReferences = @($accessibilityHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.MemberExpressionAst] -and - $node.Static -and - (Get-MemberExpressionName -Node $node) -in @('Start', 'End') -and - $node.Expression -is [System.Management.Automation.Language.TypeExpressionAst] -and - $node.Expression.TypeName.FullName -eq - 'System.Windows.Automation.Text.TextPatternRangeEndpoint' -}, $true)) -$wrongTextRangeEndpointReferences = @($accessibilityHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.MemberExpressionAst] -and - $node.Static -and - (Get-MemberExpressionName -Node $node) -in @('Start', 'End') -and - $node.Expression -is [System.Management.Automation.Language.TypeExpressionAst] -and - $node.Expression.TypeName.Name -eq 'TextPatternRangeEndpoint' -and - $node.Expression.TypeName.FullName -ne - 'System.Windows.Automation.Text.TextPatternRangeEndpoint' -}, $true)) -if ($textRangeEndpointReferences.Count -ne 6 -or - $wrongTextRangeEndpointReferences.Count -ne 0) { - throw 'Accessibility caret assertions must use the installed UIAutomation Text.TextPatternRangeEndpoint type.' -} -if ($accessibilityHarnessText -match 'VkKeyScanW|SendAsciiText' -or - $accessibilityHarnessText -notmatch 'private const uint KEYEVENTF_UNICODE = 0x0004;' -or - ([regex]::Matches($accessibilityHarnessText, 'inputs\.Add\(Key\(0, value, KEYEVENTF_UNICODE(?: \| KEYEVENTF_KEYUP)?\)\);')).Count -ne 2) { - throw 'Accessibility text injection must use layout-independent KEYEVENTF_UNICODE key down/up pairs.' -} -$highContrastFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name 'Invoke-AccessibilityHighContrastProof' -) -$highContrastCalls = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Invoke-AccessibilityHighContrastProof' -) -if ($highContrastFunctions.Count -ne 1 -or $highContrastCalls.Count -ne 2) { - throw 'Targeted and full accessibility evidence must share one High Contrast proof helper.' -} -$highContrastFunctionText = $highContrastFunctions[0].Extent.Text -if ($highContrastFunctionText -notmatch [regex]::Escape('$compoundRestoreBudgetSeconds = 20') -or - $highContrastFunctionText -notmatch [regex]::Escape('[DateTime]::UtcNow.AddSeconds($compoundRestoreBudgetSeconds)') -or - $highContrastFunctionText -notmatch [regex]::Escape('budget_seconds = $compoundRestoreBudgetSeconds')) { - throw 'Compound High Contrast restoration must reserve a diagnostic-consistent 20-second convergence budget.' -} -Assert-NoUnreachableStatements ` - -Ast $highContrastFunctions[0].Body ` - -Context 'Invoke-AccessibilityHighContrastProof' -$highContrastRecoveryTries = @($highContrastFunctions[0].Body.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.TryStatementAst] -or - $null -eq $node.Finally) { - return $false - } - $commands = @($node.Finally.FindAll({ - param($child) - $child -is [System.Management.Automation.Language.CommandAst] - }, $true) | ForEach-Object { $_.GetCommandName() }) - return $commands -contains 'Set-HighContrastState' -and - $commands -contains 'Get-HighContrastState' -and - $commands -contains 'Write-HighContrastRestoreDiagnostic' -and - $commands -contains 'Wait-AccessibilityCondition' -}, $true)) -if ($highContrastRecoveryTries.Count -ne 1) { - throw 'High Contrast proof must have one fail-closed finally block that restores exact SPI state, verifies recovery boundedly, and writes diagnostics.' -} -$dwmHighContrastDiagnosticFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $highContrastFunctions[0].Body ` - -Name 'Get-DwmHighContrastResetDiagnostic' -) -if ($dwmHighContrastDiagnosticFunctions.Count -ne 1) { - throw 'High Contrast proof must define one pure DWM reset diagnostic.' -} -Assert-NoUnreachableStatements ` - -Ast $dwmHighContrastDiagnosticFunctions[0].Body ` - -Context 'Get-DwmHighContrastResetDiagnostic' -. ([scriptblock]::Create($dwmHighContrastDiagnosticFunctions[0].Extent.Text)) -$dwmNames = @( - 'immersive_dark_20', - 'immersive_dark_19', - 'caption_color', - 'text_color', - 'backdrop_type' -) -$dwmBeforeFixture = [ordered]@{} -$dwmDuringSuccessFixture = [ordered]@{} -$dwmDuringFailureFixture = [ordered]@{} -for ($dwmIndex = 0; $dwmIndex -lt $dwmNames.Count; $dwmIndex++) { - $dwmName = $dwmNames[$dwmIndex] - $dwmExpected = [uint32]($dwmIndex + 10) - $dwmBeforeFixture[$dwmName] = [pscustomobject]@{ - attribute = $dwmIndex + 19 - supported = $true - hresult = '0x00000000' - value = [uint32]0 - expected_high_contrast = $dwmExpected - } - $dwmDuringSuccessFixture[$dwmName] = [pscustomobject]@{ - attribute = $dwmIndex + 19 - supported = $true - hresult = '0x00000000' - value = $dwmExpected - expected_high_contrast = $dwmExpected - } - $dwmDuringFailureFixture[$dwmName] = [pscustomobject]@{ - attribute = $dwmIndex + 19 - supported = $true - hresult = '0x00000000' - value = if ($dwmName -eq 'caption_color') { - [uint32]($dwmExpected + 1) - } else { - $dwmExpected - } - expected_high_contrast = $dwmExpected - } -} -$dwmSuccessOutputs = @( - Get-DwmHighContrastResetDiagnostic ` - -Before $dwmBeforeFixture ` - -During $dwmDuringSuccessFixture -) -$dwmFailureOutputs = @( - Get-DwmHighContrastResetDiagnostic ` - -Before $dwmBeforeFixture ` - -During $dwmDuringFailureFixture -) -if ($dwmSuccessOutputs.Count -ne 1 -or - $dwmSuccessOutputs[0].exact -ne $true -or - @($dwmSuccessOutputs[0].failures).Count -ne 0 -or - $dwmFailureOutputs.Count -ne 1 -or - $dwmFailureOutputs[0].exact -ne $false -or - @($dwmFailureOutputs[0].failures).Count -ne 1 -or - [string]::IsNullOrWhiteSpace([string]$dwmFailureOutputs[0].failures[0])) { - throw 'High Contrast DWM diagnostic must emit one exact success or one shaped failure without stray output.' -} -$openSettingsLoops = @($accessibilityHarnessAst.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.DoWhileStatementAst]) { - return $false - } - $settingsClassReferences = @($node.Body.FindAll({ - param($child) - $child -is [System.Management.Automation.Language.StringConstantExpressionAst] -and - $child.Value -eq 'noctty.win32.settings' - }, $true)) - return $settingsClassReferences.Count -gt 0 -and - (Get-NamedMemberExpressions ` - -Ast $node.Body ` - -Name 'TopLevelWindowsForProcess' ` - -InvocationOnly).Count -ge 2 -and - (Get-NamedMemberExpressions ` - -Ast $node.Body ` - -Name 'VisibleTerminalChildren' ` - -InvocationOnly).Count -ge 1 -and - (Get-NamedMemberExpressions ` - -Ast $node.Body ` - -Name 'SendChord' ` - -InvocationOnly).Count -ge 1 -}, $true)) -if ($openSettingsLoops.Count -ne 1) { - throw 'Settings open recovery must have one owned bounded discovery/recovery loop.' -} -$openSettingsLoop = $openSettingsLoops[0] -$openSettingsFunction = $openSettingsLoop -while ($null -ne $openSettingsFunction -and - $openSettingsFunction -isnot - [System.Management.Automation.Language.FunctionDefinitionAst]) { - $openSettingsFunction = $openSettingsFunction.Parent -} -if ($null -eq $openSettingsFunction) { - throw 'Settings open recovery loop must be owned by a function.' -} -Assert-NoUnreachableStatements ` - -Ast $openSettingsFunction.Body ` - -Context 'Settings open recovery' -$openSettingsTopLevelQueries = @( - Get-NamedMemberExpressions ` - -Ast $openSettingsLoop.Body ` - -Name 'TopLevelWindowsForProcess' ` - -InvocationOnly | - Sort-Object { $_.Extent.StartOffset } -) -$openSettingsIsWindowCalls = @( - Get-NamedMemberExpressions ` - -Ast $openSettingsLoop.Body ` - -Name 'IsWindow' ` - -InvocationOnly -) -$openSettingsSendChordCalls = @( - Get-NamedMemberExpressions ` - -Ast $openSettingsLoop.Body ` - -Name 'SendChord' ` - -InvocationOnly -) -$openSettingsSetFocusCalls = @( - Get-NamedMemberExpressions ` - -Ast $openSettingsLoop.Body ` - -Name 'SetFocus' ` - -InvocationOnly -) -$openSettingsReturns = @($openSettingsLoop.Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ReturnStatementAst] -}, $true)) -$openSettingsRecoveryBranches = @($openSettingsLoop.Body.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.IfStatementAst]) { - return $false - } - return (Get-NamedMemberExpressions ` - -Ast $node ` - -Name 'VisibleTerminalChildren' ` - -InvocationOnly).Count -ge 1 -and - (Get-NamedMemberExpressions ` - -Ast $node ` - -Name 'SendChord' ` - -InvocationOnly).Count -ge 1 -}, $true)) -if ($openSettingsTopLevelQueries.Count -lt 2 -or - $openSettingsIsWindowCalls.Count -lt 2 -or - (Get-NamedMemberExpressions -Ast $openSettingsLoop.Body -Name 'FindAll' -InvocationOnly).Count -lt 1 -or - (Get-NamedMemberExpressions -Ast $openSettingsLoop.Body -Name 'FocusedWindowFor' -InvocationOnly).Count -lt 2 -or - $openSettingsSetFocusCalls.Count -lt 1 -or - $openSettingsSendChordCalls.Count -lt 1 -or - $openSettingsReturns.Count -lt 1 -or - $openSettingsRecoveryBranches.Count -lt 1 -or - (Get-NamedCommands -Ast $openSettingsLoop.Body -Name 'Start-Sleep').Count -lt 1 -or - $openSettingsSetFocusCalls[0].Extent.StartOffset -gt - $openSettingsSendChordCalls[0].Extent.StartOffset) { - throw 'Settings open recovery must use one deadline, one chord per zero-window state, terminal focus restoration, and an atomic stable HWND/UIA/section return.' -} -$forbiddenThemeApiPattern = '\b(?:DwmSetWindowAttribute|SetWindowTheme)\s*\(' -$themeApiLeaks = @( - Get-ChildItem -LiteralPath (Join-Path $repoRoot 'src\apprt') -Filter '*.zig' -File -Recurse | - Where-Object { $_.FullName -ne $win32Theme } | - ForEach-Object { - if ((Get-Content -LiteralPath $_.FullName -Raw) -match $forbiddenThemeApiPattern) { - $_.FullName - } - } -) -if ($themeApiLeaks.Count -ne 0) { - throw "DWM and native-control theme APIs must remain private to win32_theme.zig: $($themeApiLeaks -join ', ')" -} -if ($win32RuntimeText -notmatch '\bwin32_theme\.WindowThemeAdapter\.applyHost\s*\(' -or - $win32SettingsText -notmatch '\bwin32_theme\.WindowThemeAdapter\.applySettings\s*\(' -or - $win32SettingsText -notmatch '\bwin32_theme\.WindowThemeAdapter\.applyNativeControl\s*\(') { - throw 'Win32 host and Settings callers must use the shared WindowThemeAdapter interface.' -} -$queryOnlyMarkerCommands = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Send-AccessibilityOutputMarker' | - Where-Object { - Test-CommandHasStringArgument ` - -Command $_ ` - -Value 'query-only TextPattern marker' - } -) -if ($queryOnlyMarkerCommands.Count -ne 1) { - throw 'Accessibility query-only contract must have one marker command.' -} -$queryOnlyMarkerCommand = $queryOnlyMarkerCommands[0] -$queryOnlyOwner = Get-ContainingStatementBlock -Node $queryOnlyMarkerCommand -$queryOnlyTextPatternName = Get-VariableExpressionName -Node ( - Get-CommandParameterArgument ` - -Command $queryOnlyMarkerCommand ` - -Name 'TextPattern' -) -$queryOnlyRemoveHandlers = @($queryOnlyOwner.FindAll({ - param($node) - Test-TextChangedHandlerOperation -Node $node -Operation 'Remove' -}, $true) | - Where-Object { - $_.Extent.StartOffset -lt $queryOnlyMarkerCommand.Extent.StartOffset -and - [object]::ReferenceEquals( - (Get-ContainingStatementBlock -Node $_), - $queryOnlyOwner - ) - } | - Sort-Object { $_.Extent.StartOffset } -) -$queryOnlyColdCalls = @( - Get-NamedCommands ` - -Ast $queryOnlyOwner ` - -Name 'Invoke-AccessibilityColdFirstReadProof' | - Where-Object { - $_.Extent.StartOffset -gt $queryOnlyMarkerCommand.Extent.StartOffset -and - [object]::ReferenceEquals( - (Get-ContainingStatementBlock -Node $_), - $queryOnlyOwner - ) -and - (Test-CommandHasStringArgument ` - -Command $_ ` - -Value 'cold first-read TextPattern marker') - } | - Sort-Object { $_.Extent.StartOffset } -) -$queryOnlyInactiveCalls = if ($queryOnlyColdCalls.Count -eq 1) { - @( - Get-NamedCommands ` - -Ast $queryOnlyOwner ` - -Name 'Invoke-AccessibilityInactiveTabFirstReadProof' | - Where-Object { - $_.Extent.StartOffset -gt - $queryOnlyColdCalls[0].Extent.StartOffset -and - [object]::ReferenceEquals( - (Get-ContainingStatementBlock -Node $_), - $queryOnlyOwner - ) - } | - Sort-Object { $_.Extent.StartOffset } - ) -} else { - @() -} -$queryOnlyAddHandlers = if ($queryOnlyInactiveCalls.Count -eq 1) { - @($queryOnlyOwner.FindAll({ - param($node) - Test-TextChangedHandlerOperation -Node $node -Operation 'Add' - }, $true) | - Where-Object { - $_.Extent.StartOffset -gt - $queryOnlyInactiveCalls[0].Extent.StartOffset -and - [object]::ReferenceEquals( - (Get-ContainingStatementBlock -Node $_), - $queryOnlyOwner - ) - } | - Sort-Object { $_.Extent.StartOffset } - ) -} else { - @() -} -$queryOnlyHandlerPairs = @( - foreach ($removeHandler in $queryOnlyRemoveHandlers) { - foreach ($addHandler in $queryOnlyAddHandlers) { - $removeArguments = @($removeHandler.Arguments) - $addArguments = @($addHandler.Arguments) - if ((Get-VariableExpressionName -Node $removeArguments[1]) -eq - (Get-VariableExpressionName -Node $addArguments[1]) -and - (Get-VariableExpressionName -Node $removeArguments[-1]) -eq - (Get-VariableExpressionName -Node $addArguments[-1])) { - [pscustomobject]@{ - Remove = $removeHandler - Add = $addHandler - } - } - } - } -) -$queryOnlyPreviousRangeAssignments = if ($queryOnlyHandlerPairs.Count -eq 1) { - @( - $queryOnlyOwner.FindAll({ - param($node) - if ($node -isnot - [System.Management.Automation.Language.AssignmentStatementAst] -or - $node.Extent.StartOffset -ge - $queryOnlyHandlerPairs[0].Remove.Extent.StartOffset -or - -not [object]::ReferenceEquals( - (Get-ContainingStatementBlock -Node $node), - $queryOnlyOwner - )) { - return $false - } - return (Get-VariableExpressionName -Node $node.Left) -ne '' -and - @( - Get-NamedMemberExpressions ` - -Ast $node.Right ` - -Name 'DocumentRange' | - Where-Object { - (Get-ExpressionRootVariableName -Node $_) -eq - $queryOnlyTextPatternName - } - ).Count -eq 1 - }, $true) | - Sort-Object { $_.Extent.StartOffset } | - Select-Object -Last 1 - ) -} else { - @() -} -$queryOnlyPreviousRangeName = - if ($queryOnlyPreviousRangeAssignments.Count -eq 1) { - Get-VariableExpressionName ` - -Node $queryOnlyPreviousRangeAssignments[0].Left - } else { - '' - } -$queryOnlyCurrentReads = if ($queryOnlyColdCalls.Count -eq 1) { - @( - Get-NamedMemberExpressions ` - -Ast $queryOnlyOwner ` - -Name 'GetText' ` - -InvocationOnly | - Where-Object { - $_.Extent.StartOffset -gt - $queryOnlyMarkerCommand.Extent.StartOffset -and - $_.Extent.StartOffset -lt - $queryOnlyColdCalls[0].Extent.StartOffset -and - (Get-ExpressionRootVariableName -Node $_.Expression) -eq - $queryOnlyTextPatternName -and - (Get-NamedMemberExpressions ` - -Ast $_.Expression ` - -Name 'DocumentRange').Count -eq 1 - } - ) -} else { - @() -} -$queryOnlyPreviousReads = if ($queryOnlyColdCalls.Count -eq 1) { - @( - Get-NamedMemberExpressions ` - -Ast $queryOnlyOwner ` - -Name 'GetText' ` - -InvocationOnly | - Where-Object { - $_.Extent.StartOffset -gt - $queryOnlyMarkerCommand.Extent.StartOffset -and - $_.Extent.StartOffset -lt - $queryOnlyColdCalls[0].Extent.StartOffset -and - (Get-ExpressionRootVariableName -Node $_.Expression) -eq - $queryOnlyPreviousRangeName - } - ) -} else { - @() -} -if ($null -ne $queryOnlyOwner) { - Assert-NoUnreachableStatements ` - -Ast $queryOnlyOwner ` - -Context 'query-only TextChanged handler ownership' -} -if ($null -eq $queryOnlyOwner -or - [string]::IsNullOrWhiteSpace($queryOnlyTextPatternName) -or - $queryOnlyColdCalls.Count -ne 1 -or - $queryOnlyInactiveCalls.Count -ne 1 -or - $queryOnlyHandlerPairs.Count -ne 1 -or - $queryOnlyPreviousRangeAssignments.Count -ne 1 -or - $queryOnlyCurrentReads.Count -ne 1 -or - $queryOnlyPreviousReads.Count -ne 1 -or - -not ( - $queryOnlyPreviousRangeAssignments[0].Extent.StartOffset -lt - $queryOnlyHandlerPairs[0].Remove.Extent.StartOffset -and - $queryOnlyHandlerPairs[0].Remove.Extent.StartOffset -lt - $queryOnlyMarkerCommand.Extent.StartOffset -and - $queryOnlyMarkerCommand.Extent.StartOffset -lt - $queryOnlyCurrentReads[0].Extent.StartOffset -and - $queryOnlyCurrentReads[0].Extent.StartOffset -lt - $queryOnlyPreviousReads[0].Extent.StartOffset -and - $queryOnlyPreviousReads[0].Extent.StartOffset -lt - $queryOnlyColdCalls[0].Extent.StartOffset -and - $queryOnlyColdCalls[0].Extent.StartOffset -lt - $queryOnlyInactiveCalls[0].Extent.StartOffset -and - $queryOnlyInactiveCalls[0].Extent.StartOffset -lt - $queryOnlyHandlerPairs[0].Add.Extent.StartOffset - )) { - throw 'Accessibility query-only contract must own one ordered proof, remove and restore the identical TextChanged event/document/handler triple, refresh the retained TextPattern, and preserve the prior immutable range.' -} -$outputMarkerFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name 'Send-AccessibilityOutputMarker' -) -if ($outputMarkerFunctions.Count -ne 1) { - throw 'Accessibility evidence must define exactly one output-marker input helper.' -} -$outputMarkerFunction = $outputMarkerFunctions[0] -Assert-NoUnreachableStatements ` - -Ast $outputMarkerFunction.Body ` - -Context 'Send-AccessibilityOutputMarker' -$outputMarkerLaunchers = @( - Get-NamedCommands ` - -Ast $outputMarkerFunction.Body ` - -Name 'New-AccessibilityTempCmdLauncher' -) -$outputMarkerOwnerAssertions = @( - Get-NamedCommands ` - -Ast $outputMarkerFunction.Body ` - -Name 'Assert-AccessibilityInputOwner' | - Sort-Object { $_.Extent.StartOffset } -) -$outputMarkerUnicodeSends = @( - Get-NamedMemberExpressions ` - -Ast $outputMarkerFunction.Body ` - -Name 'SendUnicodeText' ` - -InvocationOnly -) -$outputMarkerEchoWaits = @( - Get-NamedCommands ` - -Ast $outputMarkerFunction.Body ` - -Name 'Wait-AccessibilityTerminalCommandEcho' -) -$outputMarkerTextReads = @( - Get-NamedMemberExpressions ` - -Ast $outputMarkerFunction.Body ` - -Name 'GetText' ` - -InvocationOnly | - Sort-Object { $_.Extent.StartOffset } -) -$outputMarkerEnterSends = @( - Get-NamedCommands ` - -Ast $outputMarkerFunction.Body ` - -Name 'Send-AccessibilityChord' -) -$outputMarkerOutputWaits = @( - Get-NamedCommands ` - -Ast $outputMarkerFunction.Body ` - -Name 'Wait-AccessibilityCondition' -) -$outputMarkerCleanupTries = @($outputMarkerFunction.Body.FindAll({ - param($node) - if ($node -isnot [System.Management.Automation.Language.TryStatementAst] -or - $null -eq $node.Finally) { - return $false - } - return (Get-NamedMemberExpressions ` - -Ast $node.Finally ` - -Name 'Delete' ` - -InvocationOnly).Count -eq 1 -}, $true)) -if ($outputMarkerLaunchers.Count -ne 1 -or - $outputMarkerOwnerAssertions.Count -ne 2 -or - $outputMarkerUnicodeSends.Count -ne 1 -or - $outputMarkerEchoWaits.Count -ne 1 -or - $outputMarkerTextReads.Count -ne 2 -or - $outputMarkerEnterSends.Count -ne 1 -or - $outputMarkerOutputWaits.Count -ne 1 -or - $outputMarkerCleanupTries.Count -ne 1 -or - -not ( - $outputMarkerLaunchers[0].Extent.StartOffset -lt - $outputMarkerOwnerAssertions[0].Extent.StartOffset -and - $outputMarkerOwnerAssertions[0].Extent.StartOffset -lt - $outputMarkerUnicodeSends[0].Extent.StartOffset -and - $outputMarkerUnicodeSends[0].Extent.StartOffset -lt - $outputMarkerEchoWaits[0].Extent.StartOffset -and - $outputMarkerEchoWaits[0].Extent.StartOffset -lt - $outputMarkerTextReads[0].Extent.StartOffset -and - $outputMarkerTextReads[0].Extent.StartOffset -lt - $outputMarkerOwnerAssertions[1].Extent.StartOffset -and - $outputMarkerOwnerAssertions[1].Extent.StartOffset -lt - $outputMarkerEnterSends[0].Extent.StartOffset -and - $outputMarkerEnterSends[0].Extent.StartOffset -lt - $outputMarkerOutputWaits[0].Extent.StartOffset -and - $outputMarkerOutputWaits[0].Extent.StartOffset -lt - $outputMarkerTextReads[1].Extent.StartOffset - )) { - throw 'Accessibility output markers must observe the full command, record/recover exact owner immediately before one Enter, and retain diagnostics.' -} -$notificationDiagnosticFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name 'Get-AccessibilityOutputNotificationDiagnostic' -) -if ($notificationDiagnosticFunctions.Count -ne 1) { - throw 'Accessibility output notification evidence must define one pure diagnostic.' -} -Assert-NoUnreachableStatements ` - -Ast $notificationDiagnosticFunctions[0].Body ` - -Context 'Get-AccessibilityOutputNotificationDiagnostic' -. ([scriptblock]::Create($notificationDiagnosticFunctions[0].Extent.Text)) -$notificationRawSuccessFixture = @() -$notificationRawSuccessFixture += ,([object[]]@( - 'Other', - 'ignored', - 0, - 'OtherActivity' -)) -$notificationRawSuccessFixture += ,([object[]]@( - 'ActionCompleted', - 'prefix-', - 2, - 'TerminalTextOutput' -)) -$notificationRawSuccessFixture += ,([object[]]@( - 'ActionCompleted', - 'MARKER', - 2, - 'TerminalTextOutput' -)) -$notificationSuccessOutputs = @( - Get-AccessibilityOutputNotificationDiagnostic ` - -RawNotificationHistory $notificationRawSuccessFixture ` - -Marker 'MARKER' ` - -FocusMismatchPolls 2 ` - -FocusRecoveryCount 1 ` - -StolenForegroundHwnds @(41, 42) ` - -LastForegroundHwnd 43 ` - -LastFocusedHwnd 44 -) -$notificationRawFailureFixture = @() -$notificationRawFailureFixture += ,([object[]]@( - 'ActionCompleted', - 'different', - 2, - 'TerminalTextOutput' -)) -$notificationRawFailureFixture += ,([object[]]@( - 'Other', - 'MARKER', - 0, - 'OtherActivity' -)) -$notificationFailureOutputs = @( - Get-AccessibilityOutputNotificationDiagnostic ` - -RawNotificationHistory $notificationRawFailureFixture ` - -Marker 'MARKER' -) -$notificationRequiredProperties = @( - 'raw_notification_history', - 'notification_history', - 'notification_text', - 'matched', - 'history', - 'text', - 'count', - 'focus_mismatch_polls', - 'focus_recovery_count', - 'stolen_foreground_hwnds', - 'last_foreground_hwnd', - 'last_focused_hwnd' -) -if ($notificationSuccessOutputs.Count -ne 1 -or - @($notificationRequiredProperties | Where-Object { - $notificationSuccessOutputs[0].PSObject.Properties.Name -notcontains $_ - }).Count -ne 0 -or - @($notificationSuccessOutputs[0].raw_notification_history).Count -ne 3 -or - @($notificationSuccessOutputs[0].notification_history).Count -ne 2 -or - $notificationSuccessOutputs[0].notification_text -cne 'prefix-MARKER' -or - $notificationSuccessOutputs[0].matched -ne $true -or - @($notificationSuccessOutputs[0].history).Count -ne 2 -or - $notificationSuccessOutputs[0].text -cne 'prefix-MARKER' -or - $notificationSuccessOutputs[0].count -ne 2 -or - $notificationSuccessOutputs[0].focus_mismatch_polls -ne 2 -or - $notificationSuccessOutputs[0].focus_recovery_count -ne 1 -or - @($notificationSuccessOutputs[0].stolen_foreground_hwnds).Count -ne 2 -or - $notificationSuccessOutputs[0].last_foreground_hwnd -ne 43 -or - $notificationSuccessOutputs[0].last_focused_hwnd -ne 44 -or - $notificationFailureOutputs.Count -ne 1 -or - @($notificationFailureOutputs[0].raw_notification_history).Count -ne 2 -or - @($notificationFailureOutputs[0].notification_history).Count -ne 1 -or - $notificationFailureOutputs[0].notification_text -cne 'different' -or - $notificationFailureOutputs[0].matched -ne $false) { - throw 'Accessibility output notification diagnostic must atomically shape raw, matching, text, focus, success, and failure evidence without stray output.' -} -$ownedWarmNotificationFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name 'Wait-AccessibilityOwnedOutputNotification' -) -$ownedWarmNotificationCalls = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Wait-AccessibilityOwnedOutputNotification' -) -$coldFirstReadFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name 'Invoke-AccessibilityColdFirstReadProof' -) -$coldFirstReadCalls = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Invoke-AccessibilityColdFirstReadProof' -) -$coldOwnedNotificationCalls = if ($coldFirstReadFunctions.Count -eq 1) { - @($coldFirstReadFunctions[0].Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Wait-AccessibilityOwnedOutputNotification' - }, $true)) -} else { - @() -} -$coldCleanupTries = if ($coldFirstReadFunctions.Count -eq 1) { - @($coldFirstReadFunctions[0].Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.TryStatementAst] -and - $null -ne $node.Finally - }, $true)) -} else { - @() -} -if ($ownedWarmNotificationFunctions.Count -ne 1 -or - $ownedWarmNotificationCalls.Count -ne 3 -or - $coldFirstReadFunctions.Count -ne 1 -or - $coldFirstReadCalls.Count -ne 2 -or - $coldOwnedNotificationCalls.Count -ne 1 -or - $coldCleanupTries.Count -lt 1) { - throw 'Warm and cold accessibility evidence must share one owner-aware notification wait, one cold proof, and fail-closed cleanup.' -} -. ([scriptblock]::Create($ownedWarmNotificationFunctions[0].Extent.Text)) -function Wait-AccessibilityCondition { - param( - $Deadline, - [string] $Description, - [scriptblock] $Condition - ) -} -try { - $notificationWaitFixtureProcess = - [System.Diagnostics.Process]::GetCurrentProcess() - $notificationWaitWithoutDiagnostic = @( - Wait-AccessibilityOwnedOutputNotification ` - -Process $notificationWaitFixtureProcess ` - -ExpectedFocusedHwnd ([IntPtr]::Zero) ` - -Marker 'MARKER' ` - -Description 'optional diagnostic omitted fixture' - ) - $notificationWaitDiagnosticValue = $null - $notificationWaitWithDiagnostic = @( - Wait-AccessibilityOwnedOutputNotification ` - -Process $notificationWaitFixtureProcess ` - -ExpectedFocusedHwnd ([IntPtr]::Zero) ` - -Marker 'MARKER' ` - -Description 'optional diagnostic reference fixture' ` - -Diagnostic ([ref] $notificationWaitDiagnosticValue) - ) - $notificationWaitRejectedScalar = $false - try { - $null = Wait-AccessibilityOwnedOutputNotification ` - -Process $notificationWaitFixtureProcess ` - -ExpectedFocusedHwnd ([IntPtr]::Zero) ` - -Marker 'MARKER' ` - -Description 'invalid diagnostic scalar fixture' ` - -Diagnostic 'not-a-reference' - } - catch { - if ($_.Exception.Message -cne - 'Diagnostic must be a [ref] value when supplied.') { - throw - } - $notificationWaitRejectedScalar = $true - } - if ($notificationWaitWithoutDiagnostic.Count -ne 1 -or - $notificationWaitWithoutDiagnostic[0].matched -ne $false -or - $notificationWaitWithDiagnostic.Count -ne 1 -or - $notificationWaitWithDiagnostic[0].matched -ne $false -or - $null -eq $notificationWaitDiagnosticValue -or - $notificationWaitDiagnosticValue.matched -ne $false -or - -not $notificationWaitRejectedScalar) { - throw 'Owner-aware notification wait must support omitted and [ref] diagnostics while rejecting scalar diagnostics.' - } -} -finally { - Remove-Item ` - -LiteralPath Function:\Wait-AccessibilityOwnedOutputNotification ` - -ErrorAction SilentlyContinue - Remove-Item ` - -LiteralPath Function:\Wait-AccessibilityCondition ` - -ErrorAction SilentlyContinue -} -Assert-NoUnreachableStatements ` - -Ast $ownedWarmNotificationFunctions[0].Body ` - -Context 'Wait-AccessibilityOwnedOutputNotification' -Assert-NoUnreachableStatements ` - -Ast $coldFirstReadFunctions[0].Body ` - -Context 'Invoke-AccessibilityColdFirstReadProof' -$coldReadyWaits = @( - Get-NamedCommands ` - -Ast $coldFirstReadFunctions[0].Body ` - -Name 'Wait-AccessibilityCondition' -) -$coldInactivitySleeps = @( - Get-NamedCommands ` - -Ast $coldFirstReadFunctions[0].Body ` - -Name 'Start-Sleep' -) -$coldStartCaptures = @( - Get-NamedMemberExpressions ` - -Ast $coldFirstReadFunctions[0].Body ` - -Name 'StartNotificationCapture' ` - -InvocationOnly -) -$coldStopCaptures = @( - Get-NamedMemberExpressions ` - -Ast $coldFirstReadFunctions[0].Body ` - -Name 'StopNotificationCapture' ` - -InvocationOnly -) -$coldFinalReads = @( - Get-NamedMemberExpressions ` - -Ast $coldFirstReadFunctions[0].Body ` - -Name 'GetText' ` - -InvocationOnly -) -$coldTriggerWrites = @( - Get-NamedMemberExpressions ` - -Ast $coldFirstReadFunctions[0].Body ` - -Name 'WriteAllText' ` - -InvocationOnly | - Where-Object { - $_.Arguments.Count -gt 0 -and - $_.Arguments[0] -is - [System.Management.Automation.Language.VariableExpressionAst] -and - ($_.Arguments[0].VariablePath.UserPath -split ':')[-1] -eq - 'triggerPath' -and - $_.Extent.StartOffset -lt - $coldOwnedNotificationCalls[0].Extent.StartOffset - } -) -$coldFailClosedCleanupTries = @( - $coldCleanupTries | - Where-Object { - (Get-NamedMemberExpressions ` - -Ast $_.Finally ` - -Name 'StopNotificationCapture' ` - -InvocationOnly).Count -eq 1 -and - (Get-NamedMemberExpressions ` - -Ast $_.Finally ` - -Name 'Delete' ` - -InvocationOnly).Count -eq 1 - } -) -if ($coldReadyWaits.Count -ne 1 -or - $coldInactivitySleeps.Count -ne 1 -or - $coldStartCaptures.Count -ne 1 -or - $coldStopCaptures.Count -ne 1 -or - $coldFinalReads.Count -ne 1 -or - (Get-ExpressionRootVariableName -Node $coldFinalReads[0].Expression) -ne - 'TextPattern' -or - (Get-NamedMemberExpressions -Ast $coldFinalReads[0].Expression -Name 'DocumentRange').Count -ne 1 -or - $coldTriggerWrites.Count -ne 1 -or - $coldFailClosedCleanupTries.Count -ne 1 -or - -not ( - $coldStartCaptures[0].Extent.StartOffset -lt - $coldReadyWaits[0].Extent.StartOffset -and - $coldReadyWaits[0].Extent.StartOffset -lt - $coldInactivitySleeps[0].Extent.StartOffset -and - $coldInactivitySleeps[0].Extent.StartOffset -lt - $coldTriggerWrites[0].Extent.StartOffset -and - $coldTriggerWrites[0].Extent.StartOffset -lt - $coldOwnedNotificationCalls[0].Extent.StartOffset -and - $coldOwnedNotificationCalls[0].Extent.StartOffset -lt - $coldFinalReads[0].Extent.StartOffset - )) { - throw 'Cold first-read proof must establish readiness and query inactivity, trigger output, observe owned notification evidence, then perform its sole TextPattern read with fail-closed capture cleanup.' -} -$ownedDiagnosticCalls = @( - Get-NamedCommands ` - -Ast $ownedWarmNotificationFunctions[0].Body ` - -Name 'Get-AccessibilityOutputNotificationDiagnostic' -) -$ownedRawHistorySnapshots = @( - Get-NamedMemberExpressions ` - -Ast $ownedWarmNotificationFunctions[0].Body ` - -Name 'NotificationHistorySnapshot' -) -$ownedDiagnosticValueAssignments = @( - Get-NamedMemberExpressions ` - -Ast $ownedWarmNotificationFunctions[0].Body ` - -Name 'Value' | - Where-Object { - (Get-ExpressionRootVariableName -Node $_) -eq 'diagnosticState' -and - $_.Parent -is - [System.Management.Automation.Language.AssignmentStatementAst] -and - [object]::ReferenceEquals($_.Parent.Left, $_) - } -) -if ($ownedDiagnosticCalls.Count -ne 3 -or - $ownedRawHistorySnapshots.Count -ne 2 -or - $ownedDiagnosticValueAssignments.Count -ne 2 -or - (Get-NamedCommands -Ast $ownedWarmNotificationFunctions[0].Body -Name 'Wait-AccessibilityCondition').Count -ne 1 -or - (Get-NamedCommands -Ast $ownedWarmNotificationFunctions[0].Body -Name 'Where-Object').Count -ne 0) { - throw 'Owner-aware output notification wait must recompute one atomic diagnostic from raw capture history on every poll and failure.' -} -$inactiveTabFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name 'Invoke-AccessibilityInactiveTabFirstReadProof' -) -$inactiveTabCalls = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Invoke-AccessibilityInactiveTabFirstReadProof' -) -$inactiveCleanupTries = if ($inactiveTabFunctions.Count -eq 1) { - @($inactiveTabFunctions[0].Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.TryStatementAst] -and - $null -ne $node.Finally - }, $true)) -} else { - @() -} -if ($inactiveTabFunctions.Count -ne 1 -or - $inactiveTabCalls.Count -ne 2 -or - $inactiveCleanupTries.Count -lt 1) { - throw 'Targeted and full evidence must share one inactive-tab proof with fail-closed cleanup.' -} -Assert-NoUnreachableStatements ` - -Ast $inactiveTabFunctions[0].Body ` - -Context 'Invoke-AccessibilityInactiveTabFirstReadProof' -$inactiveAckWaits = @( - Get-NamedCommands ` - -Ast $inactiveTabFunctions[0].Body ` - -Name 'Wait-AccessibilityCondition' | - Where-Object { - Test-CommandHasStringArgument ` - -Command $_ ` - -Value 'inactive-output external ack' - } -) -$inactiveQuietLoops = @($inactiveTabFunctions[0].Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.DoWhileStatementAst] -and - (Get-NamedMemberExpressions ` - -Ast $node.Body ` - -Name 'IsWindowResponsive' ` - -InvocationOnly).Count -eq 1 -and - (Get-NamedCommands ` - -Ast $node.Body ` - -Name 'Get-AccessibilityOutputNotificationDiagnostic').Count -eq 1 -}, $true)) -$inactiveStartCaptures = @( - Get-NamedMemberExpressions ` - -Ast $inactiveTabFunctions[0].Body ` - -Name 'StartNotificationCapture' ` - -InvocationOnly -) -$inactiveStopCaptures = @( - Get-NamedMemberExpressions ` - -Ast $inactiveTabFunctions[0].Body ` - -Name 'StopNotificationCapture' ` - -InvocationOnly | - Sort-Object { $_.Extent.StartOffset } -) -$inactiveFirstReads = @( - Get-NamedMemberExpressions ` - -Ast $inactiveTabFunctions[0].Body ` - -Name 'GetText' ` - -InvocationOnly | - Sort-Object { $_.Extent.StartOffset } -) -if ($inactiveAckWaits.Count -ne 1 -or - $inactiveQuietLoops.Count -ne 1 -or - $inactiveStartCaptures.Count -ne 1 -or - $inactiveStopCaptures.Count -ne 2 -or - $inactiveFirstReads.Count -ne 1) { - throw 'Inactive-tab proof must define one ACK, quiet loop, capture lifetime, and first refocused TextPattern read.' -} -$inactiveNormalStop = @( - $inactiveStopCaptures | - Where-Object { - $_.Extent.StartOffset -gt $inactiveQuietLoops[0].Extent.EndOffset -and - $_.Extent.StartOffset -lt $inactiveFirstReads[0].Extent.StartOffset - } -) -$inactiveQuietBody = $inactiveQuietLoops[0].Body -$inactiveFinalDiagnosticAssignments = if ($inactiveNormalStop.Count -eq 1) { - @($inactiveTabFunctions[0].Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Extent.StartOffset -gt $inactiveQuietLoops[0].Extent.EndOffset -and - $node.Extent.StartOffset -lt $inactiveNormalStop[0].Extent.StartOffset -and - (Get-NamedCommands ` - -Ast $node.Right ` - -Name 'Get-AccessibilityOutputNotificationDiagnostic').Count -eq 1 -and - (Get-NamedMemberExpressions ` - -Ast $node.Right ` - -Name 'NotificationHistorySnapshot').Count -eq 1 - }, $true)) -} else { - @() -} -$inactiveFinalDiagnosticName = - if ($inactiveFinalDiagnosticAssignments.Count -eq 1) { - Get-VariableExpressionName ` - -Node $inactiveFinalDiagnosticAssignments[0].Left - } else { - '' - } -$inactiveFinalMatchedGuards = if ($inactiveNormalStop.Count -eq 1 -and - -not [string]::IsNullOrWhiteSpace($inactiveFinalDiagnosticName)) { - @( - Get-NamedMemberExpressions ` - -Ast $inactiveTabFunctions[0].Body ` - -Name 'matched' | - Where-Object { - if ((Get-ExpressionRootVariableName -Node $_) -ne - $inactiveFinalDiagnosticName -or - $_.Extent.StartOffset -le - $inactiveFinalDiagnosticAssignments[0].Extent.EndOffset -or - $_.Extent.StartOffset -ge - $inactiveNormalStop[0].Extent.StartOffset) { - return $false - } - $guard = $_ - while ($null -ne $guard -and - $guard -isnot - [System.Management.Automation.Language.IfStatementAst]) { - $guard = $guard.Parent - } - return $null -ne $guard -and - @($guard.FindAll({ - param($node) - $node -is - [System.Management.Automation.Language.ThrowStatementAst] - }, $true)).Count -ge 1 - } - ) -} else { - @() -} -$inactiveBoundaryConsecutive = $false -if ($inactiveFinalDiagnosticAssignments.Count -eq 1 -and - $inactiveFinalMatchedGuards.Count -eq 1 -and - $inactiveNormalStop.Count -eq 1) { - $inactiveBoundaryOwner = - Get-ContainingStatementBlock ` - -Node $inactiveFinalDiagnosticAssignments[0] - $inactiveBoundaryAssignmentStatement = - Get-DirectStatementBlockChild ` - -Node $inactiveFinalDiagnosticAssignments[0] ` - -StatementBlock $inactiveBoundaryOwner - $inactiveBoundaryGuardStatement = - $inactiveFinalMatchedGuards[0] - while ($null -ne $inactiveBoundaryGuardStatement -and - $inactiveBoundaryGuardStatement -isnot - [System.Management.Automation.Language.IfStatementAst]) { - $inactiveBoundaryGuardStatement = - $inactiveBoundaryGuardStatement.Parent - } - $inactiveBoundaryStopStatement = - Get-DirectStatementBlockChild ` - -Node $inactiveNormalStop[0] ` - -StatementBlock $inactiveBoundaryOwner - $inactiveBoundaryStatements = @($inactiveBoundaryOwner.Statements) - $inactiveBoundaryAssignmentIndex = -1 - $inactiveBoundaryGuardIndex = -1 - $inactiveBoundaryStopIndex = -1 - for ($index = 0; $index -lt $inactiveBoundaryStatements.Count; $index++) { - if ([object]::ReferenceEquals( - $inactiveBoundaryStatements[$index], - $inactiveBoundaryAssignmentStatement - )) { - $inactiveBoundaryAssignmentIndex = $index - } - if ([object]::ReferenceEquals( - $inactiveBoundaryStatements[$index], - $inactiveBoundaryGuardStatement - )) { - $inactiveBoundaryGuardIndex = $index - } - if ([object]::ReferenceEquals( - $inactiveBoundaryStatements[$index], - $inactiveBoundaryStopStatement - )) { - $inactiveBoundaryStopIndex = $index - } - } - $inactiveBoundaryConsecutive = - $inactiveBoundaryAssignmentIndex -ge 0 -and - $inactiveBoundaryGuardIndex -eq - ($inactiveBoundaryAssignmentIndex + 1) -and - $inactiveBoundaryStopIndex -eq - ($inactiveBoundaryGuardIndex + 1) -} -if ($inactiveNormalStop.Count -ne 1 -or - $inactiveFinalDiagnosticAssignments.Count -ne 1 -or - $inactiveFinalMatchedGuards.Count -ne 1 -or - -not $inactiveBoundaryConsecutive -or - (Get-NamedMemberExpressions -Ast $inactiveQuietBody -Name 'Refresh' -InvocationOnly).Count -ne 1 -or - (Get-NamedMemberExpressions -Ast $inactiveQuietBody -Name 'IsWindowResponsive' -InvocationOnly).Count -ne 1 -or - (Get-NamedMemberExpressions -Ast $inactiveQuietBody -Name 'NotificationHistorySnapshot').Count -ne 1 -or - (Get-NamedMemberExpressions -Ast $inactiveQuietBody -Name 'FocusedWindowFor' -InvocationOnly).Count -ne 1 -or - (Get-NamedMemberExpressions -Ast $inactiveQuietBody -Name 'matched').Count -ne 1 -or - (Get-NamedCommands -Ast $inactiveQuietBody -Name 'Start-Sleep').Count -ne 1 -or - (Get-NamedMemberExpressions -Ast $inactiveQuietBody -Name 'GetText' -InvocationOnly).Count -ne 0 -or - (Get-NamedMemberExpressions -Ast $inactiveQuietBody -Name 'DocumentRange').Count -ne 0 -or - -not ( - $inactiveStartCaptures[0].Extent.StartOffset -lt - $inactiveAckWaits[0].Extent.StartOffset -and - $inactiveAckWaits[0].Extent.StartOffset -lt - $inactiveQuietLoops[0].Extent.StartOffset -and - $inactiveQuietLoops[0].Extent.EndOffset -lt - $inactiveFinalDiagnosticAssignments[0].Extent.StartOffset -and - $inactiveFinalDiagnosticAssignments[0].Extent.StartOffset -lt - $inactiveFinalMatchedGuards[0].Extent.StartOffset -and - $inactiveFinalMatchedGuards[0].Extent.StartOffset -lt - $inactiveNormalStop[0].Extent.StartOffset -and - $inactiveNormalStop[0].Extent.StartOffset -lt - $inactiveFirstReads[0].Extent.StartOffset - )) { - throw 'Inactive-tab proof must keep capture active through a bounded marker-free responsive quiet interval, reject one final fresh snapshot immediately before capture stop, and perform zero TextPattern reads before the first refocused read.' -} -$tempLauncherFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name 'New-AccessibilityTempCmdLauncher' -) -$tempLauncherCalls = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'New-AccessibilityTempCmdLauncher' -) -if ($tempLauncherFunctions.Count -ne 1 -or $tempLauncherCalls.Count -ne 3) { - throw 'Warm, cold, and inactive evidence must share one temporary CMD launcher factory.' -} -$commandEchoFunctions = @( - Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name 'Wait-AccessibilityTerminalCommandEcho' -) -if ($commandEchoFunctions.Count -ne 1) { - throw 'Terminal command echo gating must define one helper.' -} -$commandEchoFunction = $commandEchoFunctions[0] -Assert-NoUnreachableStatements ` - -Ast $commandEchoFunction.Body ` - -Context 'Wait-AccessibilityTerminalCommandEcho' -$commandEchoLoops = @($commandEchoFunction.Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.DoWhileStatementAst] -}, $true)) -$commandEchoGetTextCalls = @( - Get-NamedMemberExpressions ` - -Ast $commandEchoFunction.Body ` - -Name 'GetText' ` - -InvocationOnly -) -$commandEchoFullCommandContains = @( - Get-NamedMemberExpressions ` - -Ast $commandEchoFunction.Body ` - -Name 'Contains' ` - -InvocationOnly | - Where-Object { - @($_.Arguments | Where-Object { - $_ -is [System.Management.Automation.Language.VariableExpressionAst] -and - ($_.VariablePath.UserPath -split ':')[-1] -eq 'Command' - }).Count -eq 1 - } -) -$commandEchoForbiddenInputCalls = @( - @( - Get-NamedMemberExpressions ` - -Ast $commandEchoFunction.Body ` - -Name 'SendUnicodeText' ` - -InvocationOnly - ) + @( - Get-NamedMemberExpressions ` - -Ast $commandEchoFunction.Body ` - -Name 'SendChord' ` - -InvocationOnly - ) + @( - Get-NamedCommands ` - -Ast $commandEchoFunction.Body ` - -Name 'Send-AccessibilityChord' - ) -) -if ($commandEchoLoops.Count -ne 1 -or - $commandEchoGetTextCalls.Count -ne 1 -or - $commandEchoGetTextCalls[0].Extent.StartOffset -lt - $commandEchoLoops[0].Extent.StartOffset -or - $commandEchoGetTextCalls[0].Extent.EndOffset -gt - $commandEchoLoops[0].Extent.EndOffset -or - (Get-NamedMemberExpressions -Ast $commandEchoFunction.Body -Name 'GetForegroundWindow' -InvocationOnly).Count -lt 2 -or - (Get-NamedMemberExpressions -Ast $commandEchoFunction.Body -Name 'FocusedWindowFor' -InvocationOnly).Count -lt 2 -or - (Get-NamedMemberExpressions -Ast $commandEchoFunction.Body -Name 'ForceForeground' -InvocationOnly).Count -ne 1 -or - (Get-NamedMemberExpressions -Ast $commandEchoFunction.Body -Name 'FromHandle' -InvocationOnly).Count -ne 2 -or - (Get-NamedMemberExpressions -Ast $commandEchoFunction.Body -Name 'SetFocus' -InvocationOnly).Count -ne 1 -or - (Get-NamedMemberExpressions -Ast $commandEchoFunction.Body -Name 'TryGetCurrentPattern' -InvocationOnly).Count -ne 1 -or - (Get-NamedMemberExpressions -Ast $commandEchoFunction.Body -Name 'Replace' -InvocationOnly).Count -lt 4 -or - $commandEchoFullCommandContains.Count -ne 1 -or - (Get-NamedCommands -Ast $commandEchoFunction.Body -Name 'Get-AccessibilityExceptionHResults').Count -ne 1 -or - (Get-NamedCommands -Ast $commandEchoFunction.Body -Name 'Test-AccessibilityTransientHResult').Count -ne 1 -or - $commandEchoForbiddenInputCalls.Count -ne 0) { - throw 'Terminal command echo gating must require the full normalized command, recover exact host/terminal focus without resending input, poll TextPattern, and reacquire transient providers.' -} -$stressBoundaryContract = [regex]::Match( - $accessibilityHarnessText, - '(?s)\$stressFirstMarker = "\$\{stressPrefix\}_1".*?\$stressFinalMarker = "\$\{stressPrefix\}_150".*?for /L %i in \(1,1,\$stressLineCount\) do @echo \$\{stressPrefix\}_%i.*?\$stressCommand\.Contains\(\$stressFirstMarker\).*?\$stressCommand\.Contains\(\$stressFinalMarker\)' -) -if (-not $stressBoundaryContract.Success -or $accessibilityHarnessText -match 'echo \$stressFirstMarker') { - throw 'Accessibility sustained-output boundaries must be generated only by command execution, never echoed literally in the typed command.' -} -if ($accessibilityHarnessText -notmatch '\$settingsElement\.Current\.Name -ne ''noctty settings''' -or - $accessibilityHarnessText -notmatch '\$settingsExpectedControls = \[ordered\]@\{' -or - $accessibilityHarnessText -notmatch 'settingsControlOverlapComparisons' -or - $accessibilityHarnessText -notmatch 'settingsContainmentChecks' -or - $accessibilityHarnessText -notmatch '\$settingsLayoutControls = @\(\$sectionButtons\) \+ @\(\$interactiveSettingsControls\)' -or - $accessibilityHarnessText -notmatch 'Automation\]::Compare\(\s*\$settingsSharedSectionContainer' -or - $accessibilityHarnessText -notmatch '\$containerSelection\.Current\.GetSelection\(\)' -or - $accessibilityHarnessText -notmatch '\$focusSection\.SetFocus\(\)\s*\$focusSectionSelection\.Select\(\)' -or - $accessibilityHarnessText -notmatch '\$settingsOpenTerminalHwnds = @\(\[NocttyAccessibilityNative\]::VisibleTerminalChildren' -or - $accessibilityHarnessText -notmatch '\$settingsOpenTerminalHwnds -notcontains \$settingsOpenFocusedHwnd') { - throw 'Accessibility settings evidence must assert root identity, named roles, shared selection semantics, peer overlap, and client containment.' -} -if ($accessibilityHarnessText -notmatch 'command palette unavailable no-match notification''[\s\S]*?\$paletteNativeFocusBeforeRecovery -ne \$paletteQueryHwnd[\s\S]*?ForceForeground\(\$process\.MainWindowHandle\)' -or - $accessibilityHarnessText -match 'command palette unavailable no-match notification''[\s\S]{0,1800}?\$paletteFocused\.SetFocus\(\)') { - throw 'Accessibility no-match recovery must prove native query focus before restoring only the foreground window.' -} -if ($accessibilityHarnessText -notmatch 'command palette query global UIA focus''[\s\S]*?\$paletteNativeFocusElement\.Current\.Name -ne ''Command palette query''[\s\S]*?ForceForeground\(\$process\.MainWindowHandle\)' -or - $accessibilityHarnessText -match 'command palette query global UIA focus''[\s\S]{0,1600}?\.SetFocus\(\)') { - throw 'Accessibility palette recovery must validate the native query target without manufacturing UIA focus.' -} -if ($accessibilityHarnessText -notmatch 'command palette native List recovery after zero matches''[\s\S]*?\$paletteNativeFocusBeforeRecovery -ne \$paletteQueryHwnd[\s\S]*?ForceForeground\(\$process\.MainWindowHandle\)' -or - $accessibilityHarnessText -match 'command palette native List recovery after zero matches''[\s\S]{0,1800}?\$paletteFocused\.SetFocus\(\)') { - throw 'Accessibility palette recovery must prove native query focus before restoring only foreground ownership.' -} -if ($accessibilityHarnessText -notmatch 'docked search query UIA focus''[\s\S]*?\$searchNativeFocusBeforeRecovery -ne \$searchNativeHwnd[\s\S]*?ForceForeground\(\$process\.MainWindowHandle\)' -or - $accessibilityHarnessText -match 'docked search query UIA focus''[\s\S]{0,1200}?\$searchQueryEdit\.SetFocus\(\)') { - throw 'Accessibility docked-search recovery must prove native query focus before restoring only foreground ownership.' -} -$settingsFocusWaits = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Wait-AccessibilityCondition' | - Where-Object { - Test-CommandHasStringArgument ` - -Command $_ ` - -Value 'settings section focus and selection ownership' - } -) -$settingsFocusContract = $false -if ($settingsFocusWaits.Count -eq 1) { - $settingsFocusCondition = - Get-CommandParameterArgument ` - -Command $settingsFocusWaits[0] ` - -Name 'Condition' - if ($settingsFocusCondition -is - [System.Management.Automation.Language.ScriptBlockExpressionAst]) { - $settingsFocusBody = $settingsFocusCondition.ScriptBlock.EndBlock - $settingsFocusWaitOwner = - Get-ContainingStatementBlock -Node $settingsFocusWaits[0] - $settingsFocusForegroundBranches = @($settingsFocusBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -and - (Get-NamedMemberExpressions ` - -Ast $node.Clauses[0].Item1 ` - -Name 'GetForegroundWindow' ` - -InvocationOnly).Count -ge 1 -and - (Get-NamedMemberExpressions ` - -Ast $node.Clauses[0].Item2 ` - -Name 'ForceForeground' ` - -InvocationOnly).Count -ge 1 - }, $true)) - $settingsFocusSetFocusCalls = @( - Get-NamedMemberExpressions ` - -Ast $settingsFocusBody ` - -Name 'SetFocus' ` - -InvocationOnly | - Where-Object { - [object]::ReferenceEquals( - (Get-ContainingStatementBlock -Node $_), - $settingsFocusWaitOwner - ) - } - ) - $settingsFocusSelectCalls = @( - Get-NamedMemberExpressions ` - -Ast $settingsFocusBody ` - -Name 'Select' ` - -InvocationOnly | - Where-Object { - [object]::ReferenceEquals( - (Get-ContainingStatementBlock -Node $_), - $settingsFocusWaitOwner - ) - } - ) - if ($settingsFocusForegroundBranches.Count -eq 1 -and - $settingsFocusSetFocusCalls.Count -eq 1 -and - $settingsFocusSelectCalls.Count -eq 1) { - $settingsFocusForegroundBranch = - $settingsFocusForegroundBranches[0] - $settingsFocusForceCalls = @( - Get-NamedMemberExpressions ` - -Ast $settingsFocusForegroundBranch.Clauses[0].Item2 ` - -Name 'ForceForeground' ` - -InvocationOnly - ) - $settingsFocusFalseReturns = - @($settingsFocusForegroundBranch.Clauses[0].Item2.FindAll({ - param($node) - $node -is - [System.Management.Automation.Language.ReturnStatementAst] -and - @($node.FindAll({ - param($child) - $child -is - [System.Management.Automation.Language.VariableExpressionAst] -and - (Get-VariableExpressionName -Node $child) -eq - 'false' - }, $true)).Count -eq 1 - }, $true)) - $settingsFocusPostconditions = @( - Get-NamedMemberExpressions ` - -Ast $settingsFocusBody ` - -Name 'FocusedWindowFor' ` - -InvocationOnly | - Where-Object { - $_.Extent.StartOffset -gt - $settingsFocusSelectCalls[0].Extent.StartOffset -and - [object]::ReferenceEquals( - (Get-ContainingStatementBlock -Node $_), - $settingsFocusWaitOwner - ) - } - ) - $settingsFocusHwndName = - if ($settingsFocusForceCalls.Count -eq 1 -and - $settingsFocusForceCalls[0].Arguments.Count -eq 1) { - Get-VariableExpressionName ` - -Node $settingsFocusForceCalls[0].Arguments[0] - } else { - '' - } - $settingsFocusConditionVariableNames = @( - $settingsFocusForegroundBranch.Clauses[0].Item1.FindAll({ - param($node) - $node -is - [System.Management.Automation.Language.VariableExpressionAst] - }, $true) | - ForEach-Object { Get-VariableExpressionName -Node $_ } - ) - $settingsFocusPostconditionHwndName = - if ($settingsFocusPostconditions.Count -eq 1 -and - $settingsFocusPostconditions[0].Arguments.Count -eq 1) { - Get-VariableExpressionName ` - -Node $settingsFocusPostconditions[0].Arguments[0] - } else { - '' - } - $settingsFocusContract = - $settingsFocusFalseReturns.Count -eq 1 -and - -not [string]::IsNullOrWhiteSpace($settingsFocusHwndName) -and - $settingsFocusConditionVariableNames -contains - $settingsFocusHwndName -and - $settingsFocusPostconditionHwndName -eq - $settingsFocusHwndName -and - $settingsFocusForceCalls[0].Extent.StartOffset -lt - $settingsFocusFalseReturns[0].Extent.StartOffset -and - $settingsFocusForegroundBranch.Extent.EndOffset -lt - $settingsFocusSetFocusCalls[0].Extent.StartOffset -and - $settingsFocusSetFocusCalls[0].Extent.StartOffset -lt - $settingsFocusSelectCalls[0].Extent.StartOffset -and - $settingsFocusSelectCalls[0].Extent.StartOffset -lt - $settingsFocusPostconditions[0].Extent.StartOffset - } - } -} -if (-not $settingsFocusContract -or - $accessibilityHarnessText -notmatch 'ForceForeground\(\$ownerSettingsHwnd\)[\s\S]*?PostMessageW\(\s*\$ownerSettingsHwnd[\s\S]*?settings conservative dirty-close focus' -or - $accessibilityHarnessText -match 'settings conservative dirty-close focus''[\s\S]{0,900}?(ForceForeground|SetFocus)\(') { - throw 'Accessibility settings focus evidence must restore only foreground ownership before retrying UIA target focus and selection.' -} -if ($accessibilityHarnessText -notmatch 'settings destruction and terminal focus restoration after idle soak''[\s\S]*?\$script:idleRestoreForegroundHwnd -eq \$idleTerminalHostHwnd[\s\S]*?\$script:idleRestoreFocusedHwnd -eq \$leftPane\.Hwnd' -or - $accessibilityHarnessText -match 'settings destruction and terminal focus restoration after idle soak''[\s\S]{0,1000}?ForceForeground\(') { - throw 'Accessibility idle-close evidence must observe product focus restoration without activating the expected host itself.' -} -if ($accessibilityHarnessText -match '(?m)^\s*\$matches\s*=' -or - $accessibilityHarnessText -notmatch '\$controlMatches = @\(\$interactiveSettingsControls \| Where-Object' -or - $accessibilityHarnessText -notmatch '\$controlMatches\.Count -ne 1') { - throw 'Accessibility settings evidence must not overwrite the automatic $matches variable.' -} -if ($win32SettingsText -notmatch 'const settings_header_control_count = 6;' -or - $win32SettingsText -notmatch 'const settings_clipped_control_count = settings_control_count - settings_header_control_count;' -or - $win32SettingsText -notmatch 'for \(0\.\.settings_clipped_control_count\) \|index\|' -or - $win32SettingsText -match 'for \(0\.\.27\) \|index\|') { - throw 'Settings viewport clipping must derive its content-control bound from the fixed header-control count.' -} -Assert-WorkflowContractAbsent ` - -Path $win32Settings ` - -Pattern '(?i)MessageBoxW|MB_YESNOCANCEL|MB_DEFBUTTON3' ` - -Description 'settings dirty close never falls back to an inaccessible modal message box' -Assert-TextContract ` - -Content $win32UiaWidgetsText ` - -Pattern '(?s)fn postButtonClicked\(hwnd: com\.HWND\).*?GetParent\(hwnd\).*?GetDlgCtrlID\(hwnd\).*?PostMessageW\(\s*parent,\s*WM_COMMAND,\s*@intCast\(control_id\).*?invoke_iface: com\.IInvokeProvider.*?self\.role == \.button and pattern_id == constants\.UIA_InvokePatternId.*?out\.\* = @ptrCast\(&self\.invoke_iface\).*?fn Invoke\(p: \*com\.IInvokeProvider\).*?return postButtonClicked\(self\.hwnd\)' ` - -Description 'settings buttons expose asynchronous foreground-independent native InvokePattern activation' ` - -Context $win32UiaWidgets -Assert-TextContract ` - -Content $win32UiaWidgetsText ` - -Pattern '(?s)fn sendButtonClicked\(hwnd: com\.HWND\).*?SendMessageTimeoutW\(.*?SMTO_BLOCK \| SMTO_ABORTIFHUNG.*?settings_selection_timeout_ms.*?UIA_E_ELEMENTNOTAVAILABLE.*?fn Select\(p: \*com\.ISelectionItemProvider\).*?const result = sendButtonClicked\(self\.hwnd\).*?if \(!self\.available\(\)\).*?self\.isSelected\(\)' ` - -Description 'settings section selection is synchronous, bounded, and postcondition checked' ` - -Context $win32UiaWidgets -Assert-TextContract ` - -Content $win32UiaWidgetsText ` - -Pattern '(?s)fn sendButtonClicked\(hwnd: com\.HWND\).*?SendMessageTimeoutW\(.*?WM_COMMAND.*?@bitCast\(@intFromPtr\(hwnd\)\).*?fn Select\(p: \*com\.ISelectionItemProvider\).*?sendButtonClicked\(self\.hwnd\)' ` - -Description 'settings SelectionItem Select routes the real child source HWND synchronously to the UI thread' ` - -Context $win32UiaWidgets -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)fn validatedSectionClickFocusTarget\(source: \?HWND, expected: \?HWND\).*?source_hwnd == expected_hwnd.*?if \(clickedSection\(id, notify\)\) \|section\|.*?validatedSectionClickFocusTarget\(.*?o\.sectionButton\(section\).*?_ = SetFocus\(button\);.*?o\.setActiveSection\(section\);' ` - -Description 'validated section clicks focus the real child on the UI thread before activation' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)fn settingsSectionButtonProc.*?if \(msg == WM_SETFOCUS\).*?provider\.raiseFocusChanged\(\)' ` - -Description 'real section child WM_SETFOCUS publishes the matching UIA focus event' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32UiaWidgetsText ` - -Pattern '(?s)fn hwndHasKeyboardFocus\(hwnd: com\.HWND\).*?GetWindowThreadProcessId\(hwnd, null\).*?GetGUIThreadInfo\(thread_id, &info\).*?IsChild\(hwnd, focused\).*?pub fn raiseFocusChanged\(self: \*SettingsControlProvider\).*?events\.raiseFocusChanged\(&self\.base\).*?UIA_HasKeyboardFocusPropertyId.*?hwndHasKeyboardFocus\(self\.hwnd\).*?pub fn raiseFocusChanged\(self: \*SettingsSectionProvider\)' ` - -Description 'custom settings providers expose and publish thread-correct keyboard focus' ` - -Context $win32UiaWidgets -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)const WM_SETTINGS_CLOSE_NOW: UINT = WM_APP \+ 0x100;.*?"Save and close".*?"Discard changes".*?"Keep editing".*?PostMessageW\(hwnd, WM_SETTINGS_CLOSE_NOW.*?WM_CLOSE => \{.*?showClosePrompt\(\).*?WM_SETTINGS_CLOSE_NOW => \{' ` - -Description 'settings dirty close is a posted, inline three-action workflow' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)id == BTN_CLOSE_KEEP_EDITING and lParam == 0.*?clickedButton\(id, notify, BTN_CLOSE_KEEP_EDITING\)' ` - -Description 'settings Keep editing separates synthetic Escape from native focus and click notifications' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)fn ensureControlVisible\(self: \*SettingsWindow.*?settingsContentViewportTop\(self, client_rect\).*?fn closePromptMeasuredHeight\(self: \*SettingsWindow.*?close_prompt_measure_width == pane_width.*?fn closePromptLayoutGeometry\(self: \*SettingsWindow.*?self\.px\(field_label_offset \+ 8\).*?fn settingsContentViewportTop\(self: \*SettingsWindow.*?closePromptLayoutGeometry\(self, pane_width\)\.stack_top.*?fn layoutChildren\(self: \*SettingsWindow.*?closePromptLayoutGeometry\(self, pane_width\)' ` - -Description 'settings focus viewport and layout share cached close-prompt geometry with field-label clearance' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)fn setSaveInFlight\(self: \*SettingsWindow.*?moveFocusBeforeDisablingMutableControls\(\).*?setMutableControlsEnabled\(!in_flight\).*?fn syncClosePromptText\(self: \*SettingsWindow.*?NotifyWinEvent\(EVENT_OBJECT_NAMECHANGE.*?fn showClosePrompt\(self: \*SettingsWindow.*?syncClosePromptText\(true\).*?fn cancelClosePrompt\(self: \*SettingsWindow.*?closePromptCanCancel\(self\.close_prompt_visible, self\.close_posted\)' ` - -Description 'settings save focus, prompt announcement, and posted-close cancellation remain safe' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)fn syncClosePromptText\(self: \*SettingsWindow.*?setWindowTextUtf8\(text, closePromptText\(self\.save_in_flight\)\).*?fn closePromptText\(save_in_flight: bool\) \[\]const u8.*?fn closePromptMeasuredHeight\(self: \*SettingsWindow.*?utf8ToW\(&text_w, closePromptText\(self\.save_in_flight\)\)' ` - -Description 'settings close-prompt display and measurement share one text source' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)pub fn open\(self: \*SettingsWindow\).*?pendingCloseReopenAction\(self\.close_posted, self\.close_after_save\).*?\.cancel_saved_close =>.*?self\.close_posted = false;.*?\.cancel_discard_close =>.*?self\.adoptCurrentConfig\(\).*?self\.refreshAllControls\(\).*?self\.close_posted = false;.*?self\.cancelClosePrompt\(\)' ` - -Description 'reopening settings cancels a stale posted close and honors an explicit discard' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)clickedButton\(id, notify, BTN_SAVE\).*?IsWindowEnabled\(button\).*?IsWindowVisible\(button\).*?saveCommandCanDispatch\(\s*o\.close_prompt_visible,\s*o\.save_in_flight,\s*o\.close_posted,\s*button_enabled,\s*button_visible,\s*\).*?o\.save\(\)' ` - -Description 'settings Save rechecks visibility, enabled state, and lifecycle guards at command dispatch' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)fn moveFocusBeforeDisablingMutableControls\(self: \*SettingsWindow\).*?self\.mutableControls\(\).*?self\.btn_save.*?self\.btn_close_save.*?self\.btn_close_discard.*?self\.btn_close_keep_editing.*?fn failApply\(self: \*SettingsWindow, apply_id: ApplyId\) bool.*?return false;.*?return true;.*?\.failed => \|err\| \{\s*if \(!self\.failApply\(apply_id\)\) return;\s*self\.setSaveInFlight\(false\)' ` - -Description 'async save evacuates every disabled focus source and rejects stale failed completions before UI mutation' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32SettingsText ` - -Pattern '(?s)fn settingsControlProc\(.*?msg == WM_SETFOCUS.*?provider\.raiseFocusChanged\(\).*?fn settingsSectionButtonProc\(.*?msg == WM_SETFOCUS.*?provider\.raiseFocusChanged\(\)' ` - -Description 'settings HWND focus transitions raise events from their matching custom providers' ` - -Context $win32Settings -Assert-TextContract ` - -Content $win32RuntimeText ` - -Pattern '(?s)fn overlayEditFrameRect\(.*?const min_edit_width = Host\.scaledBy\(24, dpi\).*?const bounded_width = @max\(0, width\).*?@max\(0, right - min_edit_width\)' ` - -Description 'transient overlay edits retain visible bounded width at extreme client sizes' ` - -Context $win32Runtime -Assert-TextContract ` - -Content $win32RuntimeText ` - -Pattern '(?s)if \(overlayEditFrameVisible\(self\.overlay_mode\)\) \{.*?drawRoundedRect\(hdc, edit_frame.*?fn overlayEditFrameVisible\(mode: HostOverlayMode\) bool \{\s*return mode != \.confirm;' ` - -Description 'confirm overlays never paint the hidden edit frame beneath compact actions' ` - -Context $win32Runtime -Assert-TextContract ` - -Content $win32RuntimeText ` - -Pattern '(?s)fn paletteRowTitleColor\(.*?if \(destructive\) return destructive_color;\s*if \(selected\) return selected_color;.*?paletteRowTitleColor\(true, true, 0x11, 0x22, 0x33\)' ` - -Description 'selected destructive palette actions retain the destructive foreground cue' ` - -Context $win32Runtime -Assert-TextContract ` - -Content $accessibilityHarnessText ` - -Pattern '(?s)settings inline dirty-close prompt.*?''#32770''.*?''Save changes before closing\?''.*?''Save and close''.*?''Discard changes''.*?''Keep editing''.*?settings conservative dirty-close focus.*?\$keepEditingInvoke\.Invoke\(\).*?settings keep-editing draft preservation.*?\$scrollbackValuePattern\.Current\.Value -eq \$draftScrollbackText.*?settings inline dirty-close prompt reopens.*?\$discardInvoke\.Invoke\(\)' ` - -Description 'accessibility evidence rejects modal close UI and proves conservative inline keep-editing/discard behavior' ` - -Context $accessibilityHarness -Assert-TextContract ` - -Content $accessibilityHarnessText ` - -Pattern '(?s)ResetAutomationFocusChangedCount\(\).*?AddAutomationFocusChangedEventHandler\(\$settingsFocusHandler\).*?settings conservative dirty-close focus.*?AutomationFocusChangedSenders.*?Automation\]::Compare\(\$_, \$ownerKeepEditingButton\).*?\$ownerKeepEditingButton\.Current\.HasKeyboardFocus.*?RemoveAutomationFocusChangedEventHandler\(\$settingsFocusHandler\).*?finally \{.*?\$settingsFocusRegistered.*?RemoveAutomationFocusChangedEventHandler' ` - -Description 'dirty-close evidence requires exact UIA focus event and provider focus without mutating the target' ` - -Context $accessibilityHarness -Assert-TextContract ` - -Content $accessibilityHarnessText ` - -Pattern '(?s)Invoke-AccessibilitySettingsCloseAction.*?-ActionName ''Save and close''.*?settings save-and-close completion.*?settings persisted config bytes.*?settings persistence verifier.*?Save and close did not survive a same-sandbox process relaunch.*?settings persistence baseline restoration.*?save_and_close_invoked.*?persisted_after_process_relaunch.*?original_value_restored' ` - -Description 'accessibility evidence invokes Save and close, proves same-sandbox process persistence, and restores the baseline' ` - -Context $accessibilityHarness -$themePersistenceFunctionNames = @( - 'Get-AccessibilityThemeProbe', - 'Set-AccessibilityThemeIndex', - 'Get-AccessibilityDwmUInt' -) -foreach ($functionName in $themePersistenceFunctionNames) { - if (@(Get-NamedFunctionDefinitions ` - -Ast $accessibilityHarnessAst ` - -Name $functionName).Count -ne 1) { - throw "Theme persistence evidence must define exactly one $functionName helper." - } -} -$themePersistenceVerifierCalls = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Get-AccessibilityThemeProbe' | - Where-Object { - Test-CommandHasStringArgument ` - -Command $_ ` - -Value 'settings persistence verifier' - } -) -$themePersistenceSelectionCalls = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Set-AccessibilityThemeIndex' | - Where-Object { - Test-CommandHasStringArgument ` - -Command $_ ` - -Value 'settings save probe Dark selection' - } -) -$themePersistenceDwmCalls = @( - Get-NamedCommands ` - -Ast $accessibilityHarnessAst ` - -Name 'Get-AccessibilityDwmUInt' | - Where-Object { - $description = Get-CommandParameterArgument ` - -Command $_ ` - -Name 'Description' - $description -is - [System.Management.Automation.Language.StringConstantExpressionAst] -and - $description.Value -in @('fresh Dark host', 'fresh Dark Settings') - } -) -$themePersistenceDwmAttributes = @( - $themePersistenceDwmCalls | - ForEach-Object { - $argument = Get-CommandParameterArgument -Command $_ -Name 'Attribute' - if ($argument -isnot - [System.Management.Automation.Language.ConstantExpressionAst]) { - throw 'Fresh Dark DWM persistence attributes must be static integers.' - } - [int]$argument.Value - } | - Sort-Object -) -$themePersistenceStringValues = @( - $accessibilityHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.StringConstantExpressionAst] - }, $true) | - ForEach-Object { $_.Value } -) -$themePersistenceEvidenceKeys = @( - 'persistence_theme_config_dark', - 'persistence_theme_fresh_process_index', - 'persistence_theme_dark_settings_pixel', - 'persistence_theme_dark_host_pixel', - 'persistence_theme_host_dwm_dark', - 'persistence_theme_settings_dwm_dark', - 'persistence_theme_host_backdrop', - 'persistence_theme_settings_backdrop', - 'persistence_theme_restored' -) -$themeSelectionIndex = if ($themePersistenceSelectionCalls.Count -eq 1) { - Get-CommandParameterArgument ` - -Command $themePersistenceSelectionCalls[0] ` - -Name 'Index' -} -if ($themePersistenceVerifierCalls.Count -ne 1 -or - $themePersistenceSelectionCalls.Count -ne 1 -or - $themeSelectionIndex -isnot - [System.Management.Automation.Language.ConstantExpressionAst] -or - [int]$themeSelectionIndex.Value -ne 3 -or - $themePersistenceDwmCalls.Count -ne 4 -or - (Compare-Object ` - -ReferenceObject @(20, 20, 38, 38) ` - -DifferenceObject $themePersistenceDwmAttributes ` - -SyncWindow 0).Count -ne 0 -or - $themePersistenceStringValues -notcontains - '(?m)^window-theme\s*=\s*dark\s*$' -or - @($themePersistenceEvidenceKeys | Where-Object { - $themePersistenceStringValues -notcontains $_ - }).Count -ne 0) { - throw 'Theme persistence evidence must save Dark, relaunch, verify exact visual/DWM state, and restore its baseline.' -} -Assert-TextContract ` - -Content $accessibilityHarnessText ` - -Pattern '(?s)function Start-AccessibilityProcessWithEnvironment.*?\$baseline = \[ordered\]@\{\}.*?SetEnvironmentVariable\(.*?try \{.*?Start-Process.*?finally \{.*?\$baseline\.GetEnumerator\(\).*?SetEnvironmentVariable\(.*?settingsPersistenceLayout = Get-InteractiveWin11SandboxLayout.*?settingsProbeEnvironment = Get-InteractiveWin11Environment.*?sandboxConfigPath = Join-Path \$settingsPersistenceLayout\.LocalAppData.*?Start-AccessibilityProcessWithEnvironment.*?-ArgumentList \$saveProbeArguments.*?-EnvironmentVariables \$settingsProbeEnvironment.*?Start-AccessibilityProcessWithEnvironment.*?-ArgumentList \$saveVerifyArguments.*?-EnvironmentVariables \$settingsProbeEnvironment' ` - -Description 'settings persistence probes use a dedicated sandbox and cannot trigger the primary accessibility process config watcher' ` - -Context $accessibilityHarness -$settingsPersistenceBlock = [regex]::Match( - $accessibilityHarnessText, - '(?s)\$saveProbe = Open-AccessibilitySettingsProbe.*?(?=\r?\n\s*Send-AccessibilityChord -Keys)' -) -if (-not $settingsPersistenceBlock.Success -or - $settingsPersistenceBlock.Value -notmatch '(?s)TryParse\(\s*\$saveScrollback\.Value\.Current\.Value,\s*\[ref\]\$settingsPersistenceOriginalScrollback.*?\$settingsPersistenceDraftText.*?SetValue\(\$settingsPersistenceDraftText\).*?SetValue\(\$settingsPersistenceOriginalScrollbackText\)' -or - $settingsPersistenceBlock.Value -match '\$originalScrollback|\$draftScrollbackText') { - throw 'Settings persistence evidence must derive draft and restore values only from its dedicated sandbox.' -} -Assert-TextContract ` - -Content $accessibilityHarnessText ` - -Pattern '(?s)function Restore-AccessibilityConfigBaseline.*?\[AllowNull\(\)\]\[AllowEmptyCollection\(\)\]\[byte\[\]\] \$Bytes.*?File\]::Delete\(\$Path\).*?File\]::Replace\(\$temporary, \$Path, \$backup\).*?File\]::Delete\(\$backup\).*?\$expectedBase64 = if \(\$null -eq \$Bytes\).*?ToBase64String\(\$restored\).*?settingsConfigBaselineExisted = \[System\.IO\.File\]::Exists.*?ReadAllBytes\(\$sandboxConfigPath\).*?restoredAssignments\.Count -ne 1.*?Restore-AccessibilityConfigBaseline.*?settingsConfigBaselineRestored = \$true.*?finally \{.*?settingsConfigBaselineCaptured -and -not \$settingsConfigBaselineRestored.*?Restore-AccessibilityConfigBaseline' ` - -Description 'settings persistence evidence restores the exact config baseline on success and failure' ` - -Context $accessibilityHarness -$restoreBaselineFunctions = @($accessibilityHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq 'Restore-AccessibilityConfigBaseline' -}, $true)) -if ($restoreBaselineFunctions.Count -ne 1) { - throw "Expected exactly one Restore-AccessibilityConfigBaseline definition; found $($restoreBaselineFunctions.Count)." -} -. ([scriptblock]::Create($restoreBaselineFunctions[0].Extent.Text)) -$restoreProbeDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('noctty-config-restore-' + [Guid]::NewGuid().ToString('N')) -$restoreProbePath = Join-Path $restoreProbeDirectory 'config.ghostty' -[System.IO.Directory]::CreateDirectory($restoreProbeDirectory) | Out-Null -try { - [System.IO.File]::WriteAllBytes($restoreProbePath, [byte[]](1, 2, 3)) - Restore-AccessibilityConfigBaseline -Path $restoreProbePath -Existed $false -Bytes ([byte[]]@()) - if ([System.IO.File]::Exists($restoreProbePath)) { - throw 'Accessibility baseline helper did not restore an absent file.' - } - - [System.IO.File]::WriteAllBytes($restoreProbePath, [byte[]](1, 2, 3)) - [byte[]]$restoreProbeEmptyBytes = @() - Restore-AccessibilityConfigBaseline -Path $restoreProbePath -Existed $true -Bytes $restoreProbeEmptyBytes - if (-not [System.IO.File]::Exists($restoreProbePath) -or [System.IO.File]::ReadAllBytes($restoreProbePath).Length -ne 0) { - throw 'Accessibility baseline helper did not restore a zero-byte file.' - } - - $restoreProbeBytes = [byte[]](0, 1, 127, 128, 255) - [System.IO.File]::WriteAllBytes($restoreProbePath, [byte[]](9, 9, 9)) - Restore-AccessibilityConfigBaseline -Path $restoreProbePath -Existed $true -Bytes $restoreProbeBytes - if ([Convert]::ToBase64String([System.IO.File]::ReadAllBytes($restoreProbePath)) -cne - [Convert]::ToBase64String($restoreProbeBytes)) { - throw 'Accessibility baseline helper did not restore nonempty bytes exactly.' - } -} -finally { - if ([System.IO.File]::Exists($restoreProbePath)) { [System.IO.File]::Delete($restoreProbePath) } - if ([System.IO.Directory]::Exists($restoreProbeDirectory)) { [System.IO.Directory]::Delete($restoreProbeDirectory) } -} -$paletteRebuildBlock = [regex]::Match( - $win32RuntimeText, - '(?s)fn rebuildPaletteList\(self: \*Host\) void \{.*?(?=\r?\n fn setPaletteListSelection)' -) -if (-not $paletteRebuildBlock.Success) { - throw 'Unable to isolate Host.rebuildPaletteList for notification-order verification.' -} -$paletteNoMatchIndex = $paletteRebuildBlock.Value.IndexOf('if (transition.announce_no_matches)') -$paletteRelayoutIndex = $paletteRebuildBlock.Value.IndexOf('if (transition.relayout)') -$palettePreHideEventOrder = $paletteRebuildBlock.Value -match - '(?s)if \(transition\.announce_no_matches\) \{.*?raiseStructureChanged\(&provider\.base, \.children_invalidated, null\);.*?structure_announced_before_layout = true;.*?paletteNoMatchNotificationTarget\(.*?\)\) \|target\| switch \(target\).*?\.list =>.*?raiseNotification\(&provider\.base, \.other, "No matches"\);.*?\.edit =>.*?raiseNotification\(&provider\.base, \.other, "No matches"\);.*?if \(transition\.relayout\)' -$paletteNotificationTargetBlock = [regex]::Match( - $win32RuntimeText, - '(?s)fn paletteNoMatchNotificationTarget\(.*?\) \?PaletteNoMatchNotificationTarget \{.*?(?=\r?\n\})' -) -$paletteNotificationTargetContract = $paletteNotificationTargetBlock.Success -and - $paletteNotificationTargetBlock.Value -match - '(?s)if \(list_available and list_visible\) return \.list;.*?if \(edit_available\) return \.edit;.*?if \(list_available\) return \.list;.*?return null;' -if ($paletteNoMatchIndex -lt 0 -or - $paletteRelayoutIndex -lt 0 -or - $paletteNoMatchIndex -ge $paletteRelayoutIndex -or - -not $palettePreHideEventOrder -or - -not $paletteNotificationTargetContract) { - throw 'Palette no-match UIA events must precede relayout and route notifications through the visible List or focused Edit fallback.' -} -Assert-TextContract ` - -Content $win32RuntimeText ` - -Pattern '(?s)fn paintPaletteList\(self: \*Host\) void \{.*?defer drawRectBorder\(hdc, rect, theme\.overlay_border.*?const columns = paletteRowColumns\(.*?if \(columns\.title\.right > columns\.title\.left\).*?if \(columns\.subtitle\.right > columns\.subtitle\.left\).*?paletteListRect\(width, list_x, padding, list_y, list_height\).*?self\.palette_list_visible_rows > 0 and has_list_width.*?overlayEditFrameRect\(.*?self\.current_dpi' ` - -Description 'palette rows, list bounds, visibility, and edit frame stay responsive and DPI-aware' ` - -Context $win32Runtime -if ($accessibilityHarnessText -notmatch '\$script:palette = \$palette' -or - $accessibilityHarnessText -notmatch '\$script:paletteUnavailableItems = @\(\$script:palette\.FindAll' -or - $accessibilityHarnessText -notmatch '\$script:palette = @\(\$root\.FindAll' -or - $accessibilityHarnessText -notmatch '(?s)if \(\$null -eq \$script:palette\) \{\s*\$script:palette = @\(\$root\.FindAll.*?if \(\$null -eq \$script:palette\) \{.*?\$script:paletteUnavailableItems = @\(\).*?\$queryStillFocused.*?NotificationCount -gt 0.*?return \$true.*?\$script:paletteUnavailableItems = @\(\$script:palette\.FindAll' -or - $accessibilityHarnessText -notmatch '\$script:paletteUnavailableFocused\.Current\.ControlType -eq \[System\.Windows\.Automation\.ControlType\]::Edit' -or - $accessibilityHarnessText -notmatch '\$script:paletteUnavailableFocused\.Current\.Name -eq ''Command palette query''' -or - $accessibilityHarnessText -notmatch '\$script:paletteUnavailableFocused\.Current\.HasKeyboardFocus' -or - $accessibilityHarnessText -notmatch '(?s)\$script:paletteUnavailableItems = @\(\$script:palette\.FindAll.*?\$script:paletteUnavailableFocused = \[System\.Windows\.Automation\.AutomationElement\]::FocusedElement.*?\$queryStillFocused = \$null -ne \$script:paletteUnavailableFocused -and\s*\$script:paletteUnavailableFocused\.Current\.ProcessId -eq \$process\.Id -and\s*\$script:paletteUnavailableFocused\.Current\.ControlType -eq \[System\.Windows\.Automation\.ControlType\]::Edit -and\s*\$script:paletteUnavailableFocused\.Current\.Name -eq ''Command palette query'' -and\s*\$script:paletteUnavailableFocused\.Current\.HasKeyboardFocus.*?return \$script:paletteUnavailableItems\.Count -eq 0 -and\s*\$queryStillFocused -and' -or - $accessibilityHarnessText -notmatch '-not \[NocttyAccessibilityNative\]::IsWindowVisible\(\$paletteNativeHwnd\)' -or - $accessibilityHarnessText -notmatch "-Description 'command palette native List recovery after zero matches'" -or - $accessibilityHarnessText -notmatch '\$paletteRecoveredSelectionPattern\.Current\.GetSelection\(\)' -or - $accessibilityHarnessText -notmatch '\$script:paletteRecoveredFocus\.Current\.HasKeyboardFocus' -or - $accessibilityHarnessText -notmatch '\$script:paletteRecoveryLastTransient' -or - $accessibilityHarnessText -notmatch '(?s)command palette native List recovery after zero matches.*?Test-AccessibilityTransientHResult -HResult \$_.*?\$transientHresult -eq 0x80040201.*?\$script:paletteRecovered = \$null' -or - $accessibilityHarnessText -notmatch 'function Get-AccessibilityExceptionHResults' -or - $accessibilityHarnessText -notmatch '\$results = \[System\.Collections\.Generic\.List\[int\]\]::new\(\)' -or - $accessibilityHarnessText -notmatch '\$cursor = \$cursor\.InnerException' -or - $accessibilityHarnessText -notmatch 'function Test-AccessibilityTransientHResult' -or - $accessibilityHarnessText -notmatch 'foreach \(\$knownTransient in @\(\[int\]0x80010001, \[int\]0x8001010A, \[int\]0x80040201\)\)' -or - $accessibilityHarnessText -notmatch 'Test-AccessibilityTransientHResult -HResult 0' -or - $accessibilityHarnessText -notmatch '\$hresults = @\(Get-AccessibilityExceptionHResults -Exception \$_\.Exception\)' -or - $accessibilityHarnessText -notmatch 'Test-AccessibilityTransientHResult -HResult \$_' -or - $accessibilityHarnessText -notmatch '\$null -eq \$transientHresult' -or - $accessibilityHarnessText -notmatch '\$transientHresult -eq 0x80010001 -or \$transientHresult -eq 0x8001010A' -or - $accessibilityHarnessText -notmatch '\$transientHresult -eq 0x80040201' -or - $accessibilityHarnessText -notmatch '(?s)if \(\$transientHresult -eq 0x80040201\) \{\s*\$script:palette = \$null\s*return \$false' -or - $accessibilityHarnessText -match '0x80131501') { - throw 'Accessibility palette recovery must unwrap exception chains, use script-scoped reacquisition, and retry only known UIA transient HRESULTs.' -} -$accessibilityHresultFunctions = @($accessibilityHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -in @('Get-AccessibilityExceptionHResults', 'Test-AccessibilityTransientHResult') -}, $true)) -if ($accessibilityHresultFunctions.Count -ne 2) { - throw 'Accessibility harness must define exactly one exception-chain helper and one transient-HRESULT classifier.' -} -foreach ($function in $accessibilityHresultFunctions) { - . ([scriptblock]::Create($function.Extent.Text)) -} -foreach ($knownHresult in @([int]0x80010001, [int]0x8001010A, [int]0x80040201)) { - $knownException = [Runtime.InteropServices.COMException]::new('known transient', $knownHresult) - $directChain = @(Get-AccessibilityExceptionHResults -Exception $knownException) - $wrappedChain = @(Get-AccessibilityExceptionHResults -Exception ([Exception]::new('wrapper', $knownException))) - if ($directChain.Count -ne 1 -or $directChain[0] -ne $knownHresult -or - $wrappedChain.Count -ne 2 -or $wrappedChain[-1] -ne $knownHresult -or - -not (Test-AccessibilityTransientHResult -HResult $directChain[0]) -or - -not (Test-AccessibilityTransientHResult -HResult $wrappedChain[-1])) { - throw ('Accessibility transient HRESULT semantics failed for 0x{0:X8}.' -f [BitConverter]::ToUInt32([BitConverter]::GetBytes($knownHresult), 0)) - } -} -if (Test-AccessibilityTransientHResult -HResult ([int]0x80131501)) { - throw 'Accessibility transient HRESULT classifier must reject the generic .NET wrapper result 0x80131501.' -} -if ($accessibilityHarnessText -notmatch 'ExpectedFocusedHwnd' -or - $accessibilityHarnessText -notmatch 'sustained output command'' -ExpectedFocusedHwnd \$leftPane\.Hwnd' -or - $accessibilityHarnessText -notmatch 'failed to remove terminal TextChanged handler') { - throw 'Accessibility input ownership and UIA cleanup must remain exact and fail closed.' -} -if ($accessibilityHarnessText -notmatch '\$idleTerminalHostHwnd = \$process\.MainWindowHandle' -or - $accessibilityHarnessText -notmatch 'ForceForeground\(\$idleTerminalHostHwnd\)' -or - $accessibilityHarnessText -notmatch '\$script:idleRestoreForegroundHwnd -eq \$idleTerminalHostHwnd' -or - $accessibilityHarnessText -notmatch '\$script:idleRestoreFocusedHwnd -eq \$leftPane\.Hwnd' -or - $accessibilityHarnessText -match '(?s)settings-open idle soak.*?ForceForeground\(\$process\.MainWindowHandle\)') { - throw 'Accessibility settings idle-soak focus checks must retain the terminal host HWND before Process.Refresh can recache the independent Settings window.' -} -$settingsSectionProviderContract = [regex]::Match( - $win32UiaWidgetsText, - '(?ms)^pub const SettingsSectionProvider = struct \{(?.*?)^\};\s*$' -) -if (-not $settingsSectionProviderContract.Success -or - $settingsSectionProviderContract.Groups['body'].Value -match 'SetFocus' -or - $settingsSectionProviderContract.Groups['body'].Value -notmatch 'container\.selected_index\.load\(\.acquire\)' -or - $settingsSectionProviderContract.Groups['body'].Value -notmatch 'sendButtonClicked\(self\.hwnd\)') { - throw 'Settings section UIA callbacks must synchronously marshal selection to the UI thread and retain atomic state.' -} -if ($win32UiaWidgetsText -match 'UiaRaiseAutomationEvent\(' -or - ([regex]::Matches($win32UiaWidgetsText, 'events\.raiseSelectionItemSelected\(')).Count -ne 2 -or - $win32UiaWidgetsText -notmatch 'events\.raiseSelectionItemSelected\(&row\.base\)' -or - $win32UiaWidgetsText -notmatch 'events\.raiseSelectionItemSelected\(&self\.base\)') { - throw 'Widget selection-item events must route through the shared UIA event helper.' -} -$timeoutFunctions = @($resolutionSourceAsts[0].Ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq 'Get-InteractiveWin11MessageTimeoutMs' -}, $true)) -$timeoutParameters = @($timeoutFunctions[0].Body.ParamBlock.Parameters) -$timeoutStatements = @($timeoutFunctions[0].Body.EndBlock.Statements) -$timeoutTraps = @($timeoutFunctions[0].Body.FindAll({ param($node) $node -is [System.Management.Automation.Language.TrapStatementAst] }, $true)) -if ($timeoutFunctions.Count -ne 1 -or $timeoutParameters.Count -ne 2 -or - $timeoutParameters[0].Extent.Text.Trim() -ne '[Parameter(Mandatory)] [DateTime] $Deadline' -or - $timeoutParameters[1].Extent.Text.Trim() -ne '[Parameter(Mandatory)] [string] $Description' -or - $null -ne $timeoutFunctions[0].Body.DynamicParamBlock -or $null -ne $timeoutFunctions[0].Body.BeginBlock -or - $null -ne $timeoutFunctions[0].Body.ProcessBlock -or $null -ne $timeoutFunctions[0].Body.CleanBlock -or - $timeoutTraps.Count -ne 0 -or $timeoutStatements.Count -ne 3 -or - $timeoutStatements[0].Extent.Text.Trim() -ne '$remainingMs = ($Deadline - [DateTime]::UtcNow).TotalMilliseconds' -or - $timeoutStatements[1].Extent.Text.Trim() -notmatch '(?s)^if \(\$remainingMs -le 0\) \{\s*throw "Deadline elapsed before sending \$Description\."\s*\}$' -or - $timeoutStatements[2].Extent.Text.Trim() -ne 'return [uint32][Math]::Min([double][uint32]::MaxValue, [Math]::Ceiling($remainingMs))') { - throw 'Message timeout helper must preserve its exact direct elapsed-deadline calculation and bounded return.' -} -$postMessageWrapperText = Get-PowerShellBlockText ` - -Content $interactiveWin11LibText ` - -HeaderPattern '^function Invoke-InteractiveWin11PostMessage' -$statefulPostedCommandText = Get-PowerShellBlockText ` - -Content $statefulWin11LibText ` - -HeaderPattern '^function Invoke-StatefulPostedCommand' -$postWrapperTokens = $null -$postWrapperErrors = $null -$postWrapperAst = [System.Management.Automation.Language.Parser]::ParseInput($postMessageWrapperText, [ref]$postWrapperTokens, [ref]$postWrapperErrors) -$postWrapperFunctions = @($postWrapperAst.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) -if ($postWrapperErrors.Count -ne 0 -or $postWrapperFunctions.Count -ne 1 -or - $postWrapperFunctions[0].Name -ne 'Invoke-InteractiveWin11PostMessage') { - throw 'Posted-message wrapper contract does not parse to the exact named function.' -} -$postWrapperFunction = $postWrapperFunctions[0] -$postWrapperParameters = @($postWrapperFunction.Body.ParamBlock.Parameters) -$postWrapperStatements = @($postWrapperFunction.Body.EndBlock.Statements) -$postWrapperTraps = @($postWrapperFunction.Body.FindAll({ param($node) $node -is [System.Management.Automation.Language.TrapStatementAst] }, $true)) -if ($postWrapperParameters.Count -ne 7 -or - $postWrapperParameters[0].Extent.Text.Trim() -ne '[Parameter(Mandatory)] [IntPtr] $Hwnd' -or - $postWrapperParameters[1].Extent.Text.Trim() -ne '[Parameter(Mandatory)] [uint32] $Message' -or - $postWrapperParameters[2].Extent.Text.Trim() -ne '[UIntPtr] $WParam = [UIntPtr]::Zero' -or - $postWrapperParameters[3].Extent.Text.Trim() -ne '[IntPtr] $LParam = [IntPtr]::Zero' -or - $postWrapperParameters[4].Extent.Text.Trim() -ne '[Parameter(Mandatory)] [DateTime] $Deadline' -or - $postWrapperParameters[5].Extent.Text.Trim() -ne '[Parameter(Mandatory)] [string] $Description' -or - $postWrapperParameters[6].Extent.Text.Trim() -ne '[Parameter(Mandatory)] [System.Diagnostics.Process] $Process' -or - $null -ne $postWrapperFunction.Body.DynamicParamBlock -or $null -ne $postWrapperFunction.Body.BeginBlock -or - $null -ne $postWrapperFunction.Body.ProcessBlock -or $null -ne $postWrapperFunction.Body.CleanBlock -or - $postWrapperTraps.Count -ne 0 -or $postWrapperStatements.Count -ne 6 -or - $postWrapperStatements[0].Extent.Text.Trim() -ne '$Process.Refresh()' -or - $postWrapperStatements[1].Extent.Text.Trim() -notmatch '(?s)^if \(\$Process\.HasExited\) \{\s*throw "Refusing to post \$Description because noctty already exited \(exit code \$\(\$Process\.ExitCode\)\)\."\s*\}$' -or - $postWrapperStatements[2].Extent.Text.Trim() -ne '[void](Assert-InteractiveWin11WindowOwner -Hwnd $Hwnd -Process $Process -Description $Description -Verb ''post'')' -or - $postWrapperStatements[3].Extent.Text.Trim() -ne '$lastError = 0' -or - $postWrapperStatements[4].Extent.Text.Trim() -ne 'if ($Deadline -le [DateTime]::UtcNow) { throw "Timed out waiting for $Description." }' -or - $postWrapperStatements[5].Extent.Text.Trim() -notmatch '(?s)^if \(-not \[InteractiveWin11MessageNativeV2\]::PostMessageWithError\(\$Hwnd, \$Message, \$WParam, \$LParam, \[ref\] \$lastError\)\) \{\s*\$detail = if \(\$lastError -eq 0\) \{ ''without a Win32 error'' \} else \{ "with Win32 error \$lastError" \}\s*throw "PostMessageW failed for \$Description hwnd=\$Hwnd \$detail\."\s*\}$') { - throw 'Posted-message wrapper must preserve its exact direct process, ownership, deadline, and native-post sequence.' -} -$statefulPostTokens = $null -$statefulPostErrors = $null -$statefulPostAst = [System.Management.Automation.Language.Parser]::ParseInput($statefulPostedCommandText, [ref]$statefulPostTokens, [ref]$statefulPostErrors) -$statefulPostFunctions = @($statefulPostAst.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) -if ($statefulPostErrors.Count -ne 0 -or $statefulPostFunctions.Count -ne 1 -or - $statefulPostFunctions[0].Name -ne 'Invoke-StatefulPostedCommand') { - throw 'Stateful posted-command contract does not parse to the exact named function.' -} -$statefulPostParameters = @($statefulPostFunctions[0].Parameters) -$statefulPostStatements = @($statefulPostFunctions[0].Body.EndBlock.Statements) -$statefulPostTraps = @($statefulPostFunctions[0].Body.FindAll({ param($node) $node -is [System.Management.Automation.Language.TrapStatementAst] }, $true)) -if ($statefulPostParameters.Count -ne 4 -or - $statefulPostParameters[0].Extent.Text.Trim() -ne '[IntPtr] $Hwnd' -or - $statefulPostParameters[1].Extent.Text.Trim() -ne '[int] $Id' -or - $statefulPostParameters[2].Extent.Text.Trim() -ne '[DateTime] $Deadline' -or - $statefulPostParameters[3].Extent.Text.Trim() -ne '[Parameter(Mandatory)] [System.Diagnostics.Process] $Process' -or - $null -ne $statefulPostFunctions[0].Body.DynamicParamBlock -or $null -ne $statefulPostFunctions[0].Body.BeginBlock -or - $null -ne $statefulPostFunctions[0].Body.ProcessBlock -or $null -ne $statefulPostFunctions[0].Body.CleanBlock -or - $statefulPostTraps.Count -ne 0 -or - $statefulPostStatements.Count -ne 1 -or - $statefulPostStatements[0].Extent.Text.Trim() -notmatch '(?s)^Invoke-InteractiveWin11PostMessage\s+`\s*-Hwnd \$Hwnd\s+`\s*-Message 0x0111\s+`\s*-WParam \(\[UIntPtr\]::new\(\[uint64\]\$Id\)\)\s+`\s*-Deadline \$Deadline\s+`\s*-Description "WM_COMMAND id=\$Id"\s+`\s*-Process \$Process$') { - throw 'Stateful posted-command wrapper must require and forward its exact deadline and process.' -} -$paletteThemeTokens = $null -$paletteThemeErrors = $null -$paletteThemeAst = [System.Management.Automation.Language.Parser]::ParseInput( - $paletteThemeHarnessText, - [ref]$paletteThemeTokens, - [ref]$paletteThemeErrors -) -if ($paletteThemeErrors.Count -ne 0) { throw "Palette theme harness does not parse: $($paletteThemeErrors[0].Message)" } -Assert-TextContract ` - -Content $paletteThemeHarnessText ` - -Pattern ([regex]::Escape('[int]$TimeoutSeconds = 60')) ` - -Description 'palette High Contrast transition budget remains Debug-build tolerant' ` - -Context $paletteThemeHarness -Assert-CommandResolutionContract -Ast $paletteThemeAst -Tokens $paletteThemeTokens -Context $paletteThemeHarness -ExpectedDotSources @( - ". (Join-Path `$repoRoot 'scripts\interactive-win11-lib.ps1')", - ". (Join-Path `$PSScriptRoot 'interactive-win11-stateful-lib.ps1')" -) -Assert-NoProtectedFunctionDefinitions -Ast $paletteThemeAst -Context $paletteThemeHarness -$paletteThemeTraps = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.TrapStatementAst] -}, $true)) -$paletteThemeFunctions = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -}, $true)) -$openThemeQueryFunctions = @($paletteThemeFunctions | Where-Object Name -eq 'Open-ThemeQuery') -$themePaletteDismissedFunctions = @($paletteThemeFunctions | Where-Object Name -eq 'Test-ThemePaletteDismissed') -$postHighContrastFunctions = @($paletteThemeFunctions | Where-Object Name -eq 'Invoke-PostHighContrastPresentationCanary') -$suppressedPreviewDiagnosticFunctions = @($paletteThemeFunctions | Where-Object Name -eq 'Write-SuppressedPreviewDiagnostic') -if ($paletteThemeFunctions.Count -ne 4 -or $openThemeQueryFunctions.Count -ne 1 -or - $themePaletteDismissedFunctions.Count -ne 1 -or $postHighContrastFunctions.Count -ne 1 -or - $suppressedPreviewDiagnosticFunctions.Count -ne 1) { - throw 'Palette theme harness must define only its exact query, dismissal, diagnostic, and post-High-Contrast presentation functions.' -} -$openThemeQueryParameters = @($openThemeQueryFunctions[0].Parameters) -if ($openThemeQueryParameters.Count -ne 4 -or - $openThemeQueryParameters[0].Extent.Text.Trim() -ne '[IntPtr]$HostHwnd' -or - $openThemeQueryParameters[1].Extent.Text.Trim() -ne '[string]$Query' -or - $openThemeQueryParameters[2].Extent.Text.Trim() -ne '[DateTime]$Deadline' -or - $openThemeQueryParameters[3].Extent.Text.Trim() -ne '$Process') { - throw 'Palette query opening must preserve its exact non-executable parameter contract.' -} -$openThemeQueryBody = $openThemeQueryFunctions[0].Body -if ($null -ne $openThemeQueryBody.DynamicParamBlock -or $null -ne $openThemeQueryBody.BeginBlock -or - $null -ne $openThemeQueryBody.ProcessBlock -or - $null -ne $openThemeQueryBody.CleanBlock -or $paletteThemeTraps.Count -ne 0) { - throw 'Palette query opening must not use alternate named blocks or traps that can bypass fail-closed deadline errors.' -} -$openThemeQueryStatements = @($openThemeQueryBody.EndBlock.Statements) -if ($openThemeQueryStatements.Count -ne 6 -or - $openThemeQueryStatements[0].Extent.Text.Trim() -ne 'Invoke-StatefulPostedCommand $HostHwnd 1901 $Deadline $Process' -or - $openThemeQueryStatements[1].Extent.Text.Trim() -ne '$script:PaletteThemeHost = $HostHwnd' -or - $openThemeQueryStatements[2].Extent.Text.Trim() -notmatch '(?s)^Wait-InteractiveWin11Until -Deadline \$Deadline -Description ''palette query edit'' -Process \$Process -Condition \{\s*@\(Get-StatefulChildren \$script:PaletteThemeHost \| Where-Object Id -eq 2002\)\.Count -gt 0\s*\}$' -or - $openThemeQueryStatements[3].Extent.Text.Trim() -ne '$edit = Get-StatefulChildren $HostHwnd | Where-Object Id -eq 2002 | Select-Object -First 1' -or - $openThemeQueryStatements[4].Extent.Text.Trim() -ne 'Set-StatefulEditText $HostHwnd $edit.Hwnd $Query $Deadline $Process' -or - $openThemeQueryStatements[5].Extent.Text.Trim() -ne 'return $edit.Hwnd') { - throw 'Palette query opening must preserve its complete fail-closed command, wait, edit, and return sequence.' -} -$postHighContrastParameters = @($postHighContrastFunctions[0].Parameters) -$postHighContrastBody = $postHighContrastFunctions[0].Body -$postHighContrastStatements = @($postHighContrastBody.EndBlock.Statements) -$postHighContrastTraps = @($postHighContrastBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.TrapStatementAst] -}, $true)) -if ($postHighContrastParameters.Count -ne 2 -or - $postHighContrastParameters[0].Extent.Text.Trim() -ne '[string]$Name' -or - $postHighContrastParameters[1].Extent.Text.Trim() -ne '[int]$ExpectedRgb' -or - $null -ne $postHighContrastBody.DynamicParamBlock -or $null -ne $postHighContrastBody.BeginBlock -or - $null -ne $postHighContrastBody.ProcessBlock -or $null -ne $postHighContrastBody.CleanBlock -or - $postHighContrastTraps.Count -ne 0 -or $postHighContrastStatements.Count -ne 3 -or - $postHighContrastStatements[0].Extent.Text.Trim() -ne '$lastError = $null' -or - $postHighContrastStatements[2].Extent.Text.Trim() -ne 'throw "Post-High-Contrast presentation did not recover after two fresh processes: $($lastError.Exception.Message)"') { - throw 'Post-High-Contrast presentation must preserve its exact canary parameter contract.' -} -$postHighContrastLoops = @($postHighContrastBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ForEachStatementAst] -}, $true)) -if ($postHighContrastLoops.Count -ne 1 -or - -not [object]::ReferenceEquals($postHighContrastStatements[1], $postHighContrastLoops[0]) -or - $postHighContrastLoops[0].Variable.VariablePath.UserPath -ne 'attempt' -or - $postHighContrastLoops[0].Condition.Extent.Text.Trim() -ne '1..2') { - throw 'Post-High-Contrast presentation must use one exact two-attempt loop.' -} -$postHighContrastLoopStatements = @($postHighContrastLoops[0].Body.Statements) -if ($postHighContrastLoopStatements.Count -ne 3 -or - $postHighContrastLoopStatements[0].Extent.Text.Trim() -ne '$canary = $null' -or - $postHighContrastLoopStatements[1].Extent.Text.Trim() -ne '$presentationProven = $false' -or - $postHighContrastLoopStatements[2] -isnot [System.Management.Automation.Language.TryStatementAst]) { - throw 'Post-High-Contrast presentation must guard each complete canary lifecycle with one try/catch.' -} -$postHighContrastTry = $postHighContrastLoopStatements[2] -$postHighContrastTryStatements = @($postHighContrastTry.Body.Statements) -if ($postHighContrastTry.CatchClauses.Count -ne 1 -or $null -ne $postHighContrastTry.Finally -or - $postHighContrastTryStatements.Count -ne 14 -or - $postHighContrastTryStatements[0].Extent.Text.Trim() -ne '$canary = Start-StatefulApp $layout $exe $repoRoot "$Name-$attempt"' -or - $postHighContrastTryStatements[1].Extent.Text.Trim() -ne '$runs.Add($canary)' -or - $postHighContrastTryStatements[2].Extent.Text.Trim() -ne '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -or - $postHighContrastTryStatements[3].Extent.Text.Trim() -ne '$canaryHost = Wait-StatefulHost $canary $deadline' -or - $postHighContrastTryStatements[4].Extent.Text.Trim() -notmatch '(?s)^Wait-InteractiveWin11Until -Deadline \$deadline -Description ''post-High-Contrast canary tab'' -Process \$canary\.Process -Condition \{\s*\(Get-StatefulTabCount \$canaryHost\) -eq 1\s*\}$' -or - $postHighContrastTryStatements[5].Extent.Text.Trim() -ne '$canarySurface = Wait-StatefulSurface $canaryHost $canary $deadline' -or - $postHighContrastTryStatements[6].Extent.Text.Trim() -ne 'Show-StatefulHost $canaryHost' -or - $postHighContrastTryStatements[7].Extent.Text.Trim() -ne '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -or - $postHighContrastTryStatements[8].Extent.Text.Trim() -ne '$stablePresentation = [Diagnostics.Stopwatch]::new()' -or - $postHighContrastTryStatements[9].Extent.Text.Trim() -notmatch '(?s)^Wait-InteractiveWin11Until -Deadline \$deadline -Description ''post-High-Contrast canary framebuffer'' -Process \$canary\.Process -Condition \{\s*if \(\(\(Get-StatefulPixel \$canarySurface\.Hwnd\) -band 0xFFFFFF\) -ne \$ExpectedRgb\) \{\s*\$stablePresentation\.Reset\(\)\s*return \$false\s*\}\s*if \(-not \$stablePresentation\.IsRunning\) \{ \$stablePresentation\.Start\(\) \}\s*return \$stablePresentation\.Elapsed -ge \[TimeSpan\]::FromSeconds\(2\)\s*\}$' -or - $postHighContrastTryStatements[10].Extent.Text.Trim() -ne '$presentationProven = $true' -or - $postHighContrastTryStatements[11].Extent.Text.Trim() -ne '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -or - $postHighContrastTryStatements[12].Extent.Text.Trim() -ne 'Close-StatefulHost $canaryHost $canary $deadline' -or - $postHighContrastTryStatements[13].Extent.Text.Trim() -ne 'return') { - throw 'Post-High-Contrast presentation must preserve its exact launch, readiness, framebuffer, and close sequence with three fresh deadlines.' -} -$postHighContrastCatchStatements = @($postHighContrastTry.CatchClauses[0].Body.Statements) -if ($postHighContrastCatchStatements.Count -ne 4 -or - $postHighContrastCatchStatements[0].Extent.Text.Trim() -ne '$lastError = $_' -or - $postHighContrastCatchStatements[1].Extent.Text.Trim() -notmatch '(?s)^if \(\$null -ne \$canary -and -not \$canary\.Process\.HasExited\) \{\s*try \{ Stop-InteractiveWin11Process -Process \$canary\.Process -Contained \}\s*catch \{\s*throw "Post-High-Contrast presentation attempt \$attempt failed: \$\(\$lastError\.Exception\.Message\); process cleanup also failed: \$\(\$_\.Exception\.Message\)"\s*\}\s*\}$' -or - $postHighContrastCatchStatements[2].Extent.Text.Trim() -ne 'if ($presentationProven) { return }' -or - $postHighContrastCatchStatements[3].Extent.Text.Trim() -ne 'if ($attempt -lt 2) { Write-Warning "Post-High-Contrast presentation attempt $attempt stalled; retrying with a fresh process." }') { - throw 'Post-High-Contrast presentation must preserve nullable failed-attempt cleanup and bounded retry reporting.' -} -$postHighContrastCalls = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Invoke-PostHighContrastPresentationCanary' -}, $true)) -$paletteMainTries = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.TryStatementAst] -and - $node.Parent -is [System.Management.Automation.Language.NamedBlockAst] -}, $true)) -$recoveryCanaryCalls = @($postHighContrastCalls | Where-Object { - $_.Extent.Text.Trim() -eq "Invoke-PostHighContrastPresentationCanary 'palette-theme-recovery-canary' `$draculaRgb" -}) -$restoreCanaryCalls = @($postHighContrastCalls | Where-Object { - $_.Extent.Text.Trim() -eq "Invoke-PostHighContrastPresentationCanary 'palette-theme-restore-canary' `$themeRgb" -}) -$markerRemoveCommands = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq 'Remove-Item -LiteralPath $hcRecoveryPath -Force -ErrorAction Stop' -}, $true)) -if ($paletteMainTries.Count -ne 1 -or $postHighContrastCalls.Count -ne 2 -or - $recoveryCanaryCalls.Count -ne 1 -or $restoreCanaryCalls.Count -ne 1 -or - $markerRemoveCommands.Count -ne 2) { - throw 'High Contrast recovery marker removal must follow one successful fresh-process presentation canary.' -} -$recoveryCallStatement = Get-DirectStatementBlockChild -Node $recoveryCanaryCalls[0] -StatementBlock $recoveryCanaryCalls[0].Parent.Parent -$recoveryRemoveCommands = @($markerRemoveCommands | Where-Object { - [object]::ReferenceEquals($_.Parent.Parent, $recoveryCanaryCalls[0].Parent.Parent) -}) -$recoveryBlockStatements = @($recoveryCanaryCalls[0].Parent.Parent.Statements) -if ($null -eq $recoveryCallStatement -or $recoveryRemoveCommands.Count -ne 1 -or - [Array]::IndexOf($recoveryBlockStatements, $recoveryRemoveCommands[0].Parent) -ne - ([Array]::IndexOf($recoveryBlockStatements, $recoveryCallStatement) + 1)) { - throw 'Interrupted High Contrast recovery must delete its marker immediately after a direct successful canary call.' -} -$hcRestoredIfs = @($paletteMainTries[0].Finally.Statements | Where-Object { - $_ -is [System.Management.Automation.Language.IfStatementAst] -and - $_.Clauses.Count -eq 1 -and $_.Clauses[0].Item1.Extent.Text.Trim() -eq '$hcRestored' -}) -$hcPresentationReadyIfs = @($paletteMainTries[0].Finally.Statements | Where-Object { - $_ -is [System.Management.Automation.Language.IfStatementAst] -and - $_.Clauses.Count -eq 1 -and $_.Clauses[0].Item1.Extent.Text.Trim() -eq '$hcPresentationReady' -}) -$presentationReadyAssignments = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and - $node.Left.VariablePath.UserPath -eq 'hcPresentationReady' -}, $true)) -$presentationReadyFalse = @($presentationReadyAssignments | Where-Object { - $_.Right.Extent.Text.Trim() -eq '$false' -}) -$presentationReadyTrue = @($presentationReadyAssignments | Where-Object { - $_.Right.Extent.Text.Trim() -eq '$true' -}) -$presentationReadyMutationCommands = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -match '(^|\\)((Set|New|Remove|Clear)-Variable|sv|nv|rv|clv)$' -and - $node.Extent.Text -match '(?i)hcPresentationReady' -}, $true)) -if ($hcRestoredIfs.Count -ne 1 -or $hcPresentationReadyIfs.Count -ne 1 -or - $presentationReadyAssignments.Count -ne 2 -or $presentationReadyMutationCommands.Count -ne 0 -or - $presentationReadyFalse.Count -ne 1 -or $presentationReadyTrue.Count -ne 1 -or - -not [object]::ReferenceEquals($presentationReadyFalse[0], $paletteMainTries[0].Finally.Statements[2]) -or - $hcRestoredIfs[0].Clauses[0].Item2.Statements.Count -ne 1 -or - $hcRestoredIfs[0].Clauses[0].Item2.Statements[0] -isnot [System.Management.Automation.Language.TryStatementAst]) { - throw 'High Contrast cleanup must initialize and gate one exact post-restore presentation proof.' -} -$restoreCanaryTry = $hcRestoredIfs[0].Clauses[0].Item2.Statements[0] -if ($restoreCanaryTry.Body.Statements.Count -ne 2 -or - -not [object]::ReferenceEquals($restoreCanaryTry.Body.Statements[0], $restoreCanaryCalls[0].Parent) -or - -not [object]::ReferenceEquals($restoreCanaryTry.Body.Statements[1], $presentationReadyTrue[0]) -or - $restoreCanaryTry.CatchClauses.Count -ne 1 -or $null -ne $restoreCanaryTry.Finally -or - $restoreCanaryTry.CatchClauses[0].Body.Statements.Count -ne 1 -or - $restoreCanaryTry.CatchClauses[0].Body.Statements[0].Extent.Text.Trim() -ne '[void]$cleanupErrors.Add("High Contrast post-restore presentation failed: $($_.Exception.Message)")') { - throw 'Restored High Contrast state must become presentation-ready only after the direct canary succeeds.' -} -$markerCleanupBody = $hcPresentationReadyIfs[0].Clauses[0].Item2 -if ($markerCleanupBody.Statements.Count -ne 1 -or - $markerCleanupBody.Statements[0] -isnot [System.Management.Automation.Language.TryStatementAst] -or - $markerCleanupBody.Statements[0].Body.Statements.Count -ne 1 -or - -not [object]::ReferenceEquals($markerCleanupBody.Statements[0].Body.Statements[0], - @($markerRemoveCommands | Where-Object { -not [object]::ReferenceEquals($_, $recoveryRemoveCommands[0]) })[0].Parent) -or - $markerCleanupBody.Statements[0].CatchClauses.Count -ne 1 -or - $markerCleanupBody.Statements[0].CatchClauses[0].Body.Statements.Count -ne 1 -or - $markerCleanupBody.Statements[0].CatchClauses[0].Body.Statements[0].Extent.Text.Trim() -ne '[void]$cleanupErrors.Add("High Contrast recovery marker cleanup failed: $($_.Exception.Message)")') { - throw 'High Contrast recovery marker cleanup must be directly and exclusively gated by successful presentation.' -} -$testCanaryMutationShape = { - param([string] $Content) - - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$tokens, [ref]$errors) - if ($errors.Count -ne 0) { return $false } - $functions = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq 'Invoke-PostHighContrastPresentationCanary' - }, $true)) - if ($functions.Count -ne 1) { return $false } - $loops = @($functions[0].Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ForEachStatementAst] - }, $true)) - if ($loops.Count -ne 1 -or $loops[0].Body.Statements.Count -ne 3 -or - $loops[0].Body.Statements[1].Extent.Text.Trim() -ne '$presentationProven = $false' -or - $loops[0].Body.Statements[2] -isnot [System.Management.Automation.Language.TryStatementAst]) { - return $false - } - $statements = @($loops[0].Body.Statements[2].Body.Statements) - return $statements.Count -eq 14 -and - $statements[0].Extent.Text.Trim() -eq '$canary = Start-StatefulApp $layout $exe $repoRoot "$Name-$attempt"' -and - $statements[1].Extent.Text.Trim() -eq '$runs.Add($canary)' -and - $statements[2].Extent.Text.Trim() -eq '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -and - $statements[7].Extent.Text.Trim() -eq '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -and - $statements[8].Extent.Text.Trim() -eq '$stablePresentation = [Diagnostics.Stopwatch]::new()' -and - $statements[10].Extent.Text.Trim() -eq '$presentationProven = $true' -and - $statements[11].Extent.Text.Trim() -eq '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -and - $statements[12].Extent.Text.Trim() -eq 'Close-StatefulHost $canaryHost $canary $deadline' -and - $statements[13].Extent.Text.Trim() -eq 'return' -} -$testMarkerMutationShape = { - param([string] $Content) - - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$tokens, [ref]$errors) - if ($errors.Count -ne 0) { return $false } - $assignments = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and - $node.Left.VariablePath.UserPath -eq 'hcPresentationReady' - }, $true)) - $trueAssignments = @($assignments | Where-Object { $_.Right.Extent.Text.Trim() -eq '$true' }) - $falseAssignments = @($assignments | Where-Object { $_.Right.Extent.Text.Trim() -eq '$false' }) - $mutationCommands = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -match '(^|\\)((Set|New|Remove|Clear)-Variable|sv|nv|rv|clv)$' -and - $node.Extent.Text -match '(?i)hcPresentationReady' - }, $true)) - return $assignments.Count -eq 2 -and $trueAssignments.Count -eq 1 -and - $falseAssignments.Count -eq 1 -and $mutationCommands.Count -eq 0 -} -if (-not (& $testCanaryMutationShape $paletteThemeHarnessText) -or - -not (& $testMarkerMutationShape $paletteThemeHarnessText)) { - throw 'High Contrast mutation probes do not recognize the protected production shape.' -} -$deadlineMutationPattern = [regex]::new( - '(?m)^(\s*Show-StatefulHost \$canaryHost\r?\n)\s*\$deadline = \[DateTime\]::UtcNow\.AddSeconds\(\$TimeoutSeconds\)\r?\n' -) -$missingDeadlineMutation = $deadlineMutationPattern.Replace($paletteThemeHarnessText, '$1', 1) -$earlyReturnPattern = [regex]::new( - '(?s)(function Invoke-PostHighContrastPresentationCanary.*?foreach \(\$attempt in 1\.\.2\).*?try \{\r?\n)' -) -$earlyReturnMutation = $earlyReturnPattern.Replace($paletteThemeHarnessText, '$1 return' + [Environment]::NewLine, 1) -$failOpenMarkerPattern = [regex]::new( - '(\[void\]\$cleanupErrors\.Add\("High Contrast post-restore presentation failed: \$\(\$_\.Exception\.Message\)"\))' -) -$failOpenMarkerMutation = $failOpenMarkerPattern.Replace( - $paletteThemeHarnessText, - '$hcPresentationReady = $true' + [Environment]::NewLine + ' $1', - 1 -) -$moduleMutatorMarkerMutation = $failOpenMarkerPattern.Replace( - $paletteThemeHarnessText, - 'Microsoft.PowerShell.Utility\Set-Variable hcPresentationReady $true' + [Environment]::NewLine + ' $1', - 1 -) -$aliasMutatorMarkerMutation = $failOpenMarkerPattern.Replace( - $paletteThemeHarnessText, - 'sv hcPresentationReady $true' + [Environment]::NewLine + ' $1', - 1 -) -if ($missingDeadlineMutation -eq $paletteThemeHarnessText -or - $earlyReturnMutation -eq $paletteThemeHarnessText -or - $failOpenMarkerMutation -eq $paletteThemeHarnessText -or - $moduleMutatorMarkerMutation -eq $paletteThemeHarnessText -or - $aliasMutatorMarkerMutation -eq $paletteThemeHarnessText -or - (& $testCanaryMutationShape $missingDeadlineMutation) -or - (& $testCanaryMutationShape $earlyReturnMutation) -or - (& $testMarkerMutationShape $failOpenMarkerMutation) -or - (& $testMarkerMutationShape $moduleMutatorMarkerMutation) -or - (& $testMarkerMutationShape $aliasMutatorMarkerMutation)) { - throw 'High Contrast contract failed to reject a deadline, dead-code, or fail-open marker mutation.' -} -$paletteOpenCalls = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Open-ThemeQuery' -}, $true)) -$paletteNormalOpenCalls = @($paletteOpenCalls | Where-Object { - $_.Extent.Text.Trim() -eq "Open-ThemeQuery `$hostHwnd '0x96f' `$deadline `$run.Process" -}) -$paletteHighContrastOpenCalls = @($paletteOpenCalls | Where-Object { - $_.Extent.Text.Trim() -eq "Open-ThemeQuery `$hcHost 'Dracula' `$deadline `$hcRun.Process" -}) -if ($paletteMainTries.Count -ne 1 -or $paletteOpenCalls.Count -ne 3 -or - $paletteNormalOpenCalls.Count -ne 2 -or $paletteHighContrastOpenCalls.Count -ne 1) { - throw 'Palette theme harness must preserve its three exact Open-ThemeQuery calls.' -} -foreach ($call in $paletteNormalOpenCalls) { - if (-not (Test-DirectStatementBlockChild -Node $call -StatementBlock $paletteMainTries[0].Body)) { - throw 'Normal palette query calls must be direct statements in the main try body.' - } -} -$paletteHighContrastCallIf = $null -foreach ($candidate in @($paletteThemeAst.FindAll({ param($node) $node -is [System.Management.Automation.Language.IfStatementAst] }, $true))) { - foreach ($clause in $candidate.Clauses) { - if ($clause.Item1.Extent.Text.Trim() -eq '$ExerciseHighContrast' -and - (Test-DirectStatementBlockChild -Node $paletteHighContrastOpenCalls[0] -StatementBlock $clause.Item2)) { - if ($null -ne $paletteHighContrastCallIf) { throw 'High Contrast palette query call appears in multiple control-flow blocks.' } - $paletteHighContrastCallIf = $candidate - } - } -} -if ($null -eq $paletteHighContrastCallIf -or - -not (Test-DirectStatementBlockChild -Node $paletteHighContrastCallIf -StatementBlock $paletteMainTries[0].Body)) { - throw 'High Contrast palette query call must be direct in its main-try ExerciseHighContrast block.' -} -$highContrastClause = @($paletteHighContrastCallIf.Clauses | Where-Object { - $_.Item1.Extent.Text.Trim() -eq '$ExerciseHighContrast' -and - (Test-DirectStatementBlockChild -Node $paletteHighContrastOpenCalls[0] -StatementBlock $_.Item2) -})[0] -$highContrastOpenStatement = Get-DirectStatementBlockChild ` - -Node $paletteHighContrastOpenCalls[0] ` - -StatementBlock $highContrastClause.Item2 -$highContrastOpenIndex = [Array]::IndexOf($highContrastClause.Item2.Statements, $highContrastOpenStatement) -if ($highContrastOpenIndex -lt 1 -or $highContrastOpenIndex -ge ($highContrastClause.Item2.Statements.Count - 1) -or - $highContrastClause.Item2.Statements[$highContrastOpenIndex - 1].Extent.Text.Trim() -ne '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -or - $highContrastClause.Item2.Statements[$highContrastOpenIndex + 1].Extent.Text.Trim() -ne '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)') { - throw 'High Contrast palette opening and suppression checks must each receive a fresh deadline.' -} -$highContrastStabilityWaits = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Wait-InteractiveWin11Until' -and - $node.Extent.Text -match "-Description '(?:stable High Contrast framebuffer|suppressed High Contrast theme preview)'" -}, $true)) -if ($highContrastStabilityWaits.Count -ne 2 -or - @($highContrastStabilityWaits | Where-Object { - $_.Extent.Text -match [regex]::Escape('[TimeSpan]::FromSeconds(2)') - }).Count -ne 2 -or - @($highContrastStabilityWaits | Where-Object { - $_.Extent.Text -match [regex]::Escape('$hcPresentation.Stable.Restart()') - }).Count -ne 1 -or - @($highContrastStabilityWaits | Where-Object { - $_.Extent.Text -match [regex]::Escape('$previewRgb -eq $draculaRgb') -and - $_.Extent.Text -match [regex]::Escape('$suppressedPreview.Stable.Restart()') -and - $_.Extent.Text -match [regex]::Escape('$suppressedPreview.TransitionCount++') - }).Count -ne 1 -or - $paletteThemeHarnessText -notmatch [regex]::Escape('SystemParametersInfo(0x42, $activeHc.cbSize, [ref]$activeHc, 0)') -or - $paletteThemeHarnessText -notmatch [regex]::Escape('($activeHc.dwFlags -band 1) -eq 0') -or - $paletteThemeHarnessText -notmatch [regex]::Escape('High Contrast preview framebuffer: baseline={0:x6}, settled-or-last={1:x6}, transitions={2}') -or - ([regex]::Matches($paletteThemeHarnessText, '(?m)^\s*Write-SuppressedPreviewDiagnostic \$hcPixel \$suppressedPreview\s*$')).Count -ne 4 -or - $paletteThemeHarnessText -notmatch '(?s)catch\s*\{\s*Write-SuppressedPreviewDiagnostic \$hcPixel \$suppressedPreview\s+throw\s*\}') { - throw 'High Contrast theme preview must reject Dracula, prove a stable settled framebuffer, verify active High Contrast, and report transitions before success or failure.' -} -$highContrastCloseCalls = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq 'Close-StatefulHost $hcHost $hcRun $deadline' -}, $true)) -$highContrastDismissCalls = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq 'Invoke-StatefulButton $hcHost 2004 $deadline $hcRun.Process' -}, $true)) -$highContrastDismissWaits = @($paletteThemeAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Wait-InteractiveWin11Until' -and - $node.Extent.Text -match "-Description 'High Contrast palette dismissal'" -and - $node.Extent.Text -match 'Test-ThemePaletteDismissed \$script:PaletteThemeHighContrastHost' -}, $true)) -if ($highContrastCloseCalls.Count -ne 1 -or $highContrastDismissCalls.Count -ne 1 -or - $highContrastDismissWaits.Count -ne 1 -or - -not (Test-DirectStatementBlockChild -Node $highContrastDismissCalls[0] -StatementBlock $highContrastClause.Item2) -or - -not (Test-DirectStatementBlockChild -Node $highContrastDismissWaits[0] -StatementBlock $highContrastClause.Item2) -or - $highContrastCloseCalls[0].Parent.Parent -isnot [System.Management.Automation.Language.StatementBlockAst] -or - $highContrastCloseCalls[0].Parent.Parent.Parent -isnot [System.Management.Automation.Language.TryStatementAst] -or - $highContrastCloseCalls[0].Parent.Parent.Statements.Count -ne 2 -or - $highContrastCloseCalls[0].Parent.Parent.Statements[0].Extent.Text.Trim() -ne '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -or - $highContrastCloseCalls[0].Parent.Parent.Parent.CatchClauses.Count -ne 1 -or - $highContrastCloseCalls[0].Parent.Parent.Parent.CatchClauses[0].Body.Extent.Text -notmatch - '(?s)Stop-InteractiveWin11Process -Process \$hcRun\.Process -Contained.*?Write-Warning') { - throw 'High Contrast palette dismissal must stay strict; only host-close timeout may become a warning after contained termination.' -} -if ($paletteThemeHarnessText -notmatch 'Invoke-StatefulButton \$hostHwnd 2004 \$deadline \$run\.Process' -or - $paletteThemeHarnessText -match 'Invoke-Stateful(?:Posted)?Command \$\w+ 2004' -or - $paletteThemeHarnessText -notmatch "-Description 'theme palette dismissal after preview'" -or - $themePaletteDismissedFunctions[0].Body.Extent.Text -notmatch '(?s)\.Id -ge 2001 -and \$_.Id -le 2006.*?\.Count -eq 0' -or - $paletteThemeHarnessText -notmatch '(?s)-Description ''theme palette dismissal after preview''.*?Test-ThemePaletteDismissed \$hostHwnd.*?\$deadline = \[DateTime\]::UtcNow\.AddSeconds\(\$TimeoutSeconds\)\s*Wait-InteractiveWin11Until -Deadline \$deadline -Description ''Dracula preview rollback''' -or - $paletteThemeHarnessText -notmatch '\$themeListRect\.Top -lt \$themeSurfaceRect\.Top' -or - $paletteThemeHarnessText -notmatch '(?s)Dracula preview rollback.*?\$deadline = \[DateTime\]::UtcNow\.AddSeconds\(\$TimeoutSeconds\)\s*\$edit = Open-ThemeQuery' -or - $paletteThemeHarnessText -notmatch 'Dismissal changed persisted theme instead of reverting preview') { - throw 'Palette theme validation must prove rich-result geometry and click the real Close button before framebuffer/config rollback.' -} -if ($win32RuntimeText -notmatch 'const list_y = overlay_y \+ self\.scaled\(host_overlay_height\);' -or - $win32RuntimeText -notmatch 'paletteListTransition\(' -or - $win32RuntimeText -notmatch 'if \(transition\.exposes_content\) \{\s*self\.invalidateVisibleSurfaceChildPaint\(true, false\);' -or - $win32RuntimeText -notmatch 'const was_palette = self\.overlay_mode == \.command_palette;' -or - $win32RuntimeText -notmatch 'if \(was_confirm or was_palette\)' -or - $win32RuntimeText -notmatch 'palette_catalog_retained_config' -or - $win32RuntimeText -notmatch 'palette_catalog_config_source' -or - $win32RuntimeText -notmatch '(?s)other != self and other\.overlay_mode == \.command_palette.*?other\.hideOverlay\(\);' -or - $win32RuntimeText -notmatch '(?s)\.config_change =>.*?if \(host\.overlay_mode == \.command_palette\) host\.hideOverlay\(\);.*?self\.config\.deinit\(\);\s*self\.config = config;' -or - $win32RuntimeText -notmatch 'self\.refreshPalettePresentation\(\);' -or - $win32RuntimeText -notmatch 'Make the window larger to show and activate palette results\.' -or - $win32RuntimeText -notmatch 'DT_LEFT \| DT_VCENTER \| DT_SINGLELINE \| DT_NOPREFIX \| DT_END_ELLIPSIS') { - throw 'Palette list layout must sit below feedback and repaint exposed terminal content on shrink, hide, and dismissal.' -} -$sessionRestoreTabSeedLoop = Get-PowerShellBlockText ` - -Content $sessionRestoreHarnessText ` - -HeaderPattern '^foreach \(\$targetTabCount in 2\.\.3\)' -$sessionLoopTokens = $null -$sessionLoopErrors = $null -$sessionLoopAst = [System.Management.Automation.Language.Parser]::ParseInput( - $sessionRestoreTabSeedLoop, - [ref]$sessionLoopTokens, - [ref]$sessionLoopErrors -) -if ($sessionLoopErrors.Count -ne 0) { throw "Session restore tab-seed loop does not parse: $($sessionLoopErrors[0].Message)" } -$sessionLoopNodes = @($sessionLoopAst.FindAll({ param($node) $true }, $true)) -$sessionDeadlineNodes = @($sessionLoopNodes | Where-Object { - $_ -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $_.Extent.Text.Trim() -eq '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -}) -$sessionCommandNodes = @($sessionLoopNodes | Where-Object { - $_ -is [System.Management.Automation.Language.CommandAst] -and - $_.Extent.Text.Trim() -eq 'Invoke-StatefulPostedCommand $hostHwnd 1904 $deadline $first.Process' -}) -$sessionWaitNodes = @($sessionLoopNodes | Where-Object { - $_ -is [System.Management.Automation.Language.CommandAst] -and - $_.Extent.Text.Trim() -match '(?s)^Wait-InteractiveWin11Until -Deadline \$deadline -Description "live tab count \$targetTabCount" -Process \$first\.Process -Condition \{\s*\(Get-StatefulTabCount \$hostHwnd\) -eq \$targetTabCount\s*\}$' -}) -$sessionBarrierNodes = @($sessionLoopNodes | Where-Object { - $_ -is [System.Management.Automation.Language.BinaryExpressionAst] -and - $_.Extent.Text.Trim() -eq '(Get-StatefulTabCount $hostHwnd) -eq $targetTabCount' -}) -$sessionReadyNodes = @($sessionLoopNodes | Where-Object { - $_ -is [System.Management.Automation.Language.CommandAst] -and - $_.Extent.Text.Trim() -eq 'Invoke-InteractiveWin11Message -Hwnd $hostHwnd -Message 0 -Deadline $deadline -Description "live tab $targetTabCount readiness barrier" -Process $first.Process' -}) -if ($sessionDeadlineNodes.Count -ne 1 -or $sessionCommandNodes.Count -ne 1 -or - $sessionWaitNodes.Count -ne 1 -or $sessionBarrierNodes.Count -ne 1 -or - $sessionReadyNodes.Count -ne 1) { - throw 'Session restore tab-seed loop must contain one exact deadline, queued command, count wait, equality barrier, and pump-readiness barrier per iteration.' -} -$sessionParsedSeedLoops = @($sessionLoopNodes | Where-Object { - $_ -is [System.Management.Automation.Language.ForEachStatementAst] -and - $_.Extent.Text -match '^foreach \(\$targetTabCount in 2\.\.3\)' -}) -if ($sessionParsedSeedLoops.Count -ne 1 -or $sessionParsedSeedLoops[0].Body.Statements.Count -ne 4) { - throw 'Session restore tab-seed contract must parse to one loop containing exactly four executable statements.' -} -foreach ($node in @($sessionDeadlineNodes[0], $sessionCommandNodes[0], $sessionWaitNodes[0], $sessionReadyNodes[0])) { - if (-not (Test-DirectStatementBlockChild -Node $node -StatementBlock $sessionParsedSeedLoops[0].Body)) { - throw 'Session restore tab-seed operations must be direct, executable statements in the loop body.' - } -} -$sessionSeedOffsets = @( - $sessionDeadlineNodes[0].Extent.StartOffset, - $sessionCommandNodes[0].Extent.StartOffset, - $sessionWaitNodes[0].Extent.StartOffset, - $sessionBarrierNodes[0].Extent.StartOffset, - $sessionReadyNodes[0].Extent.StartOffset -) -for ($i = 1; $i -lt $sessionSeedOffsets.Count; $i++) { - if ($sessionSeedOffsets[$i - 1] -ge $sessionSeedOffsets[$i]) { - throw 'Session restore tab-seed operations are not in fail-closed causal order.' - } -} -$sessionHarnessTokens = $null -$sessionHarnessErrors = $null -$sessionHarnessAst = [System.Management.Automation.Language.Parser]::ParseInput( - $sessionRestoreHarnessText, - [ref]$sessionHarnessTokens, - [ref]$sessionHarnessErrors -) -if ($sessionHarnessErrors.Count -ne 0) { throw "Session restore harness does not parse: $($sessionHarnessErrors[0].Message)" } -Assert-CommandResolutionContract -Ast $sessionHarnessAst -Tokens $sessionHarnessTokens -Context $sessionRestoreHarness -ExpectedDotSources @( - ". (Join-Path `$repoRoot 'scripts\interactive-win11-lib.ps1')", - ". (Join-Path `$PSScriptRoot 'interactive-win11-stateful-lib.ps1')" -) -Assert-NoProtectedFunctionDefinitions -Ast $sessionHarnessAst -Context $sessionRestoreHarness -$sessionTraps = @($sessionHarnessAst.FindAll({ param($node) $node -is [System.Management.Automation.Language.TrapStatementAst] }, $true)) -if ($sessionTraps.Count -ne 0) { throw 'Session restore harness must not use traps that swallow validation failures.' } -$sessionMainTries = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.TryStatementAst] -and - $node.Parent -is [System.Management.Automation.Language.NamedBlockAst] -}, $true)) -$sessionSeedLoops = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ForEachStatementAst] -and - $node.Extent.Text -match '^foreach \(\$targetTabCount in 2\.\.3\)' -}, $true)) -if ($sessionMainTries.Count -ne 1 -or $sessionSeedLoops.Count -ne 1 -or - -not (Test-DirectStatementBlockChild -Node $sessionSeedLoops[0] -StatementBlock $sessionMainTries[0].Body)) { - throw 'Session restore tab seeding must be one direct, executable loop in the main try body.' -} -$sessionInitialHostWaits = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Extent.Text.Trim() -eq '$hostHwnd = Wait-StatefulHost $first $deadline' -}, $true)) -$sessionInitialTabWaits = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq 'Wait-InteractiveWin11Until -Deadline $deadline -Description ''initial session-save tab'' -Process $first.Process -Condition { (Get-StatefulTabCount $hostHwnd) -eq 1 }' -}, $true)) -$sessionInitialReadyBarriers = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Extent.Text.Trim() -eq '$null = Invoke-InteractiveWin11Message -Hwnd $hostHwnd -Message 0 -Deadline $deadline -Description ''initial session-save host readiness barrier'' -Process $first.Process' -}, $true)) -if ($sessionInitialHostWaits.Count -ne 1 -or $sessionInitialTabWaits.Count -ne 1 -or - $sessionInitialReadyBarriers.Count -ne 1) { - throw 'Session restore must contain one exact initial host wait, exact tab-count wait, and nonmutating pump-readiness barrier.' -} -foreach ($node in @($sessionInitialHostWaits[0], $sessionInitialTabWaits[0], $sessionInitialReadyBarriers[0])) { - if (-not (Test-DirectStatementBlockChild -Node $node -StatementBlock $sessionMainTries[0].Body)) { - throw 'Session restore initial host, tab-count, and readiness waits must be direct, executable statements in the main try body.' - } -} -$sessionInitialStatements = @($sessionMainTries[0].Body.Statements) -$sessionInitialWaitIndex = -1 -$sessionInitialTabIndex = -1 -$sessionInitialReadyIndex = -1 -$sessionInitialSeedIndex = -1 -for ($i = 0; $i -lt $sessionInitialStatements.Count; $i++) { - if ([object]::ReferenceEquals($sessionInitialStatements[$i], $sessionInitialHostWaits[0])) { $sessionInitialWaitIndex = $i } - if ([object]::ReferenceEquals($sessionInitialStatements[$i], $sessionInitialTabWaits[0].Parent)) { $sessionInitialTabIndex = $i } - if ([object]::ReferenceEquals($sessionInitialStatements[$i], $sessionInitialReadyBarriers[0])) { $sessionInitialReadyIndex = $i } - if ([object]::ReferenceEquals($sessionInitialStatements[$i], $sessionSeedLoops[0])) { $sessionInitialSeedIndex = $i } -} -if ($sessionInitialTabIndex -ne ($sessionInitialWaitIndex + 1) -or - $sessionInitialReadyIndex -ne ($sessionInitialTabIndex + 1) -or - $sessionInitialSeedIndex -ne ($sessionInitialReadyIndex + 1)) { - throw 'Session restore must prove initial tab creation and host pump readiness on the startup deadline immediately before tab seeding.' -} -$sessionBurstCommands = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq 'Invoke-StatefulPostedCommand $explicitHost 1904 $deadline $explicit.Process' -}, $true)) -$sessionExplicitStarts = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq "Start-StatefulApp `$layout `$exe `$repoRoot 'session-explicit-command' @('-e', 'cmd.exe', '/k')" -}, $true)) -$sessionBurstWaits = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq "Wait-InteractiveWin11Until -Deadline `$deadline -Description 'asynchronous burst-created tabs' -Process `$explicit.Process -Condition { (Get-StatefulTabCount `$explicitHost) -eq 3 }" -}, $true)) -$sessionExplicitTabWaits = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq "Wait-InteractiveWin11Until -Deadline `$deadline -Description 'fresh explicit-command tab' -Process `$explicit.Process -Condition { (Get-StatefulTabCount `$explicitHost) -eq 1 }" -}, $true)) -$sessionExplicitHostWaits = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Extent.Text.Trim() -eq '$explicitHost = Wait-StatefulHost $explicit $deadline' -}, $true)) -$sessionBurstReady = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq "Invoke-InteractiveWin11Message -Hwnd `$explicitHost -Message 0 -Deadline `$deadline -Description 'asynchronous tab burst readiness barrier' -Process `$explicit.Process" -}, $true)) -$sessionExplicitCloses = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq 'Close-StatefulHost $explicitHost $explicit $deadline' -}, $true)) -$sessionExactDeadlines = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Extent.Text.Trim() -eq '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -}, $true)) -$sessionInvariantLoops = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ForEachStatementAst] -and - $node.Parent -is [System.Management.Automation.Language.NamedBlockAst] -and - $node.Extent.Text.Trim() -match '(?s)^foreach \(\$run in \$runs\) \{\s*if \(Select-String -LiteralPath \$run\.Stderr -SimpleMatch ''shell/native invariant failed'' -Quiet\) \{\s*throw "Shell/native invariant failure reported by \$\(\$run\.Stderr\)\."\s*\}\s*\}$' -}, $true)) -if ($sessionExplicitStarts.Count -ne 1 -or $sessionExplicitHostWaits.Count -ne 1 -or - $sessionExplicitTabWaits.Count -ne 1 -or $sessionBurstCommands.Count -ne 2 -or - $sessionBurstWaits.Count -ne 1 -or $sessionBurstReady.Count -ne 1 -or - $sessionExplicitCloses.Count -ne 1 -or $sessionInvariantLoops.Count -ne 1) { - throw 'Session restore validation must preserve the explicit-process burst phase, its exact-count/pump barriers, and invariant-log rejection.' -} -$sessionBurstDeadlines = @($sessionExactDeadlines | Where-Object { - (Test-DirectStatementBlockChild -Node $_ -StatementBlock $sessionMainTries[0].Body) -and - $_.Extent.StartOffset -gt $sessionExplicitTabWaits[0].Extent.EndOffset -and - $_.Extent.EndOffset -lt $sessionBurstCommands[0].Extent.StartOffset -}) -$sessionCloseDeadlines = @($sessionExactDeadlines | Where-Object { - (Test-DirectStatementBlockChild -Node $_ -StatementBlock $sessionMainTries[0].Body) -and - $_.Extent.StartOffset -gt $sessionBurstReady[0].Extent.EndOffset -and - $_.Extent.EndOffset -lt $sessionExplicitCloses[0].Extent.StartOffset -}) -if ($sessionBurstDeadlines.Count -ne 1 -or $sessionCloseDeadlines.Count -ne 1) { - throw 'Session restore asynchronous burst and close phases must each receive one fresh exact deadline.' -} -$sessionDirectBurstNodes = @( - $sessionExplicitStarts[0], - $sessionExplicitTabWaits[0], - $sessionBurstDeadlines[0], - $sessionBurstCommands[0], - $sessionBurstCommands[1], - $sessionBurstWaits[0], - $sessionBurstReady[0], - $sessionCloseDeadlines[0], - $sessionExplicitCloses[0] -) -foreach ($node in $sessionDirectBurstNodes) { - if (-not (Test-DirectStatementBlockChild -Node $node -StatementBlock $sessionMainTries[0].Body)) { - throw 'Session restore asynchronous burst operations must be direct, executable statements in the main try body.' - } -} -$sessionInvariantIfs = @($sessionInvariantLoops[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -}, $true)) -$sessionInvariantThrows = @($sessionInvariantLoops[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ThrowStatementAst] -and - $node.Extent.Text.Trim() -eq 'throw "Shell/native invariant failure reported by $($run.Stderr)."' -}, $true)) -$sessionPassCommands = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.Extent.Text.Trim() -eq 'Write-Host "interactive-win11 session-restore validation: PASS (state=$statePath)"' -}, $true)) -if ($sessionPassCommands.Count -ne 1) { - throw 'Session restore story must contain one exact PASS sentinel.' -} -$sessionBootstrapIfs = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -and - $node.Extent.Text -match '^if \(-not \$env:NOCTTY_INTERACTIVE_WIN11_SESSION_RESTORE_BOOTSTRAPPED\)' -}, $true)) -if ($sessionBootstrapIfs.Count -ne 1) { - throw 'Session restore story must contain one exact bootstrap guard.' -} -$sessionSnapshotFunctions = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq 'Get-SessionAutomationSnapshot' -and - $node.Extent.Text -match '^function Get-SessionAutomationSnapshot\(\[string\]\$Name, \[DateTime\]\$Deadline\)' -}, $true)) -if ($sessionSnapshotFunctions.Count -ne 1) { - throw 'Session restore story must contain one exact automation snapshot helper.' -} -$sessionSnapshotRetryLoops = @($sessionSnapshotFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.ForEachStatementAst] -and - $node.Extent.Text -match '^foreach \(\$attempt in 1\.\.3\)' -}, $true)) -$sessionSnapshotWaitIfs = @($sessionSnapshotFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -and - $node.Extent.Text -match '^if \(-not \$query\.WaitForExit\(\$remainingMs\)\)' -}, $true)) -$sessionSnapshotOutputIfs = @($sessionSnapshotFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -and - $node.Extent.Text -match '^if \(\(Test-Path \$out\) -and \(Get-Item \$out\)\.Length -gt 0\)' -}, $true)) -$sessionSnapshotExitIfs = @($sessionSnapshotFunctions[0].FindAll({ - param($node) - $node -is [System.Management.Automation.Language.IfStatementAst] -and - $node.Extent.Text -match '^if \(\$queryExitCode -ne 0\)' -}, $true)) -if ($sessionSnapshotRetryLoops.Count -ne 1 -or $sessionSnapshotWaitIfs.Count -ne 1 -or - $sessionSnapshotExitIfs.Count -ne 1 -or $sessionSnapshotOutputIfs.Count -ne 1 -or - -not [object]::ReferenceEquals($sessionSnapshotRetryLoops[0].Parent, $sessionSnapshotFunctions[0].Body.EndBlock) -or - -not (Test-DirectStatementBlockChild -Node $sessionSnapshotWaitIfs[0] -StatementBlock $sessionSnapshotRetryLoops[0].Body) -or - -not (Test-DirectStatementBlockChild -Node $sessionSnapshotExitIfs[0] -StatementBlock $sessionSnapshotRetryLoops[0].Body) -or - -not (Test-DirectStatementBlockChild -Node $sessionSnapshotOutputIfs[0] -StatementBlock $sessionSnapshotRetryLoops[0].Body)) { - throw 'Session restore automation snapshot control flow must remain directly inside one retry loop.' -} -$sessionSnapshotFunctionStatements = @($sessionSnapshotFunctions[0].Body.EndBlock.Statements) -if ($sessionSnapshotFunctionStatements.Count -ne 5 -or - $sessionSnapshotFunctionStatements[0].Extent.Text.Trim() -ne '$cli = Join-Path (Split-Path -Parent $exe) ''noctty.com''' -or - $sessionSnapshotFunctionStatements[1].Extent.Text.Trim() -ne 'if (-not (Test-Path -LiteralPath $cli)) { throw "Missing automation CLI shim: $cli" }' -or - $sessionSnapshotFunctionStatements[2].Extent.Text.Trim() -ne '$lastError = ''''' -or - -not [object]::ReferenceEquals($sessionSnapshotFunctionStatements[3], $sessionSnapshotRetryLoops[0]) -or - $sessionSnapshotFunctionStatements[4].Extent.Text.Trim() -ne 'throw "Session automation query failed after three attempts: $lastError"') { - throw 'Session restore automation snapshot helper setup, retry, and terminal failure are not in causal order.' -} -$sessionSnapshotLoopStatements = @($sessionSnapshotRetryLoops[0].Body.Statements) -$sessionSnapshotCausalPrefix = @( - '$out = Join-Path $layout.Logs "$Name-$attempt.json"', - '$err = Join-Path $layout.Logs "$Name-$attempt.stderr.log"', - '$query = Start-Process -FilePath $cli -ArgumentList @(''+list-windows'', "--class=$instanceClass") -WorkingDirectory $repoRoot -RedirectStandardOutput $out -RedirectStandardError $err -PassThru', - '$queryHandle = $query.Handle', - '$remainingMs = [Math]::Max(0, [int]($Deadline - [DateTime]::UtcNow).TotalMilliseconds)' -) -if ($sessionSnapshotLoopStatements.Count -ne 12) { - throw 'Session restore automation snapshot retry loop is missing causal statements.' -} -for ($i = 0; $i -lt $sessionSnapshotCausalPrefix.Count; $i++) { - if ($sessionSnapshotLoopStatements[$i].Extent.Text.Trim() -ne $sessionSnapshotCausalPrefix[$i]) { - throw 'Session restore automation snapshot path, launch, and deadline statements are not in causal order.' - } -} -if (-not [object]::ReferenceEquals($sessionSnapshotLoopStatements[5], $sessionSnapshotWaitIfs[0]) -or - $sessionSnapshotLoopStatements[6].Extent.Text.Trim() -ne '$query.Refresh()' -or - $sessionSnapshotLoopStatements[7].Extent.Text.Trim() -ne '$queryExitCode = Get-InteractiveWin11ProcessExitCode -Process $query -ProcessHandle $queryHandle' -or - -not [object]::ReferenceEquals($sessionSnapshotLoopStatements[8], $sessionSnapshotExitIfs[0]) -or - $sessionSnapshotExitIfs[0].Clauses[0].Item2.Statements.Count -ne 3 -or - $sessionSnapshotExitIfs[0].Clauses[0].Item2.Statements[0].Extent.Text.Trim() -ne '$lastError = if ((Test-Path $err) -and (Get-Item $err).Length -gt 0) { "exit ${queryExitCode}: $(Get-Content $err -Raw)" } else { "exit $queryExitCode" }' -or - $sessionSnapshotExitIfs[0].Clauses[0].Item2.Statements[1].Extent.Text.Trim() -ne 'Start-Sleep -Milliseconds 250' -or - $sessionSnapshotExitIfs[0].Clauses[0].Item2.Statements[2].Extent.Text.Trim() -ne 'continue' -or - -not [object]::ReferenceEquals($sessionSnapshotLoopStatements[9], $sessionSnapshotOutputIfs[0]) -or - $sessionSnapshotLoopStatements[10].Extent.Text.Trim() -ne '$lastError = if ((Test-Path $err) -and (Get-Item $err).Length -gt 0) { Get-Content $err -Raw } else { ''empty stdout'' }' -or - $sessionSnapshotLoopStatements[11].Extent.Text.Trim() -ne 'Start-Sleep -Milliseconds 250') { - throw 'Session restore automation snapshot must wait, refresh, require a zero signed exit code, then parse fresh output.' -} -$sessionRunsAssignments = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left.Extent.Text -eq '$runs' -and - $node.Extent.EndOffset -le $sessionPassCommands[0].Extent.EndOffset -}, $true)) -$sessionInstanceClassAssignments = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Extent.Text.Trim() -eq '$instanceClass = "noctty-interactive-$($layout.SandboxId)"' -}, $true)) -$sessionTopLevelStatements = @($sessionHarnessAst.EndBlock.Statements) -$sessionRunsInitializationIndex = -1 -$sessionInstanceClassIndex = -1 -$sessionSnapshotFunctionIndex = -1 -$sessionMainTryIndex = -1 -$sessionInvariantIndex = -1 -$sessionPassIndex = -1 -for ($i = 0; $i -lt $sessionTopLevelStatements.Count; $i++) { - if ($sessionRunsAssignments.Count -eq 1 -and [object]::ReferenceEquals($sessionTopLevelStatements[$i], $sessionRunsAssignments[0])) { $sessionRunsInitializationIndex = $i } - if ($sessionInstanceClassAssignments.Count -eq 1 -and [object]::ReferenceEquals($sessionTopLevelStatements[$i], $sessionInstanceClassAssignments[0])) { $sessionInstanceClassIndex = $i } - if ([object]::ReferenceEquals($sessionTopLevelStatements[$i], $sessionSnapshotFunctions[0])) { $sessionSnapshotFunctionIndex = $i } - if ([object]::ReferenceEquals($sessionTopLevelStatements[$i], $sessionMainTries[0])) { $sessionMainTryIndex = $i } - if ([object]::ReferenceEquals($sessionTopLevelStatements[$i], $sessionInvariantLoops[0])) { $sessionInvariantIndex = $i } - if ([object]::ReferenceEquals($sessionTopLevelStatements[$i], $sessionPassCommands[0].Parent)) { $sessionPassIndex = $i } -} -$sessionControlTransfers = @($sessionHarnessAst.FindAll({ - param($node) - ($node -is [System.Management.Automation.Language.ReturnStatementAst] -or - $node -is [System.Management.Automation.Language.BreakStatementAst] -or - $node -is [System.Management.Automation.Language.ContinueStatementAst] -or - $node -is [System.Management.Automation.Language.ExitStatementAst]) -and - $node.Extent.EndOffset -le $sessionPassCommands[0].Extent.EndOffset -}, $true)) -$sessionFunctionDefinitions = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Extent.EndOffset -le $sessionPassCommands[0].Extent.EndOffset -}, $true)) -$sessionRunsInvocations = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and - $node.Expression.Extent.Text -eq '$runs' -and - $node.Extent.EndOffset -le $sessionPassCommands[0].Extent.EndOffset -}, $true) | Sort-Object { $_.Extent.StartOffset }) -$sessionExpectedRunsInvocations = @( - '$runs.Add($first)', - '$runs.Add($explicit)', - '$runs.Add($second)', - '$runs.Add($third)' -) -$sessionExpectedStartAssignments = @( - '$first = Start-StatefulApp $layout $exe $repoRoot ''session-save'' @(''--single-instance=true'')', - '$explicit = Start-StatefulApp $layout $exe $repoRoot ''session-explicit-command'' @(''-e'', ''cmd.exe'', ''/k'')', - '$second = Start-StatefulApp $layout $exe $repoRoot ''session-restore'' @(''--single-instance=true'')', - '$third = Start-StatefulApp $layout $exe $repoRoot ''session-corrupt''' -) -$sessionStartAssignments = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Extent.Text.Trim() -in $sessionExpectedStartAssignments -}, $true) | Sort-Object { $_.Extent.StartOffset }) -$sessionAllStartCommands = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Start-StatefulApp' -and - $node.Extent.EndOffset -le $sessionPassCommands[0].Extent.EndOffset -}, $true) | Sort-Object { $_.Extent.StartOffset }) -$sessionEapAssignments = @($sessionHarnessAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and - $node.Left.Extent.Text -eq '$ErrorActionPreference' -and - $node.Extent.EndOffset -le $sessionPassCommands[0].Extent.EndOffset -}, $true)) -$sessionForbiddenTermination = @($sessionHarnessAst.FindAll({ - param($node) - (($node -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and - $node.Expression -is [System.Management.Automation.Language.TypeExpressionAst] -and - $node.Expression.TypeName.FullName -in @('Environment', 'System.Environment') -and - $node.Member.Extent.Text -eq 'Exit') -or - ($node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -match '(^|\\)(Stop-Process|taskkill(?:\.exe)?|Invoke-Expression|iex)$')) -and - $node.Extent.EndOffset -le $sessionPassCommands[0].Extent.EndOffset -}, $true)) -$sessionBootstrapBody = $sessionBootstrapIfs[0].Clauses[0].Item2 -$sessionSnapshotWaitBody = $sessionSnapshotWaitIfs[0].Clauses[0].Item2 -$sessionSnapshotExitBody = $sessionSnapshotExitIfs[0].Clauses[0].Item2 -$sessionSnapshotOutputBody = $sessionSnapshotOutputIfs[0].Clauses[0].Item2 -$sessionBootstrapExits = @($sessionControlTransfers | Where-Object { - $_ -is [System.Management.Automation.Language.ExitStatementAst] -and - $_.Extent.Text.Trim() -eq 'exit $code' -and - [object]::ReferenceEquals($_.Parent, $sessionBootstrapBody) -and - [object]::ReferenceEquals($_, $sessionBootstrapBody.Statements[-1]) -}) -$sessionSnapshotContinues = @($sessionControlTransfers | Where-Object { - $_ -is [System.Management.Automation.Language.ContinueStatementAst] -and - $_.Extent.Text.Trim() -eq 'continue' -and - (([object]::ReferenceEquals($_.Parent, $sessionSnapshotWaitBody) -and - [object]::ReferenceEquals($_, $sessionSnapshotWaitBody.Statements[-1])) -or - ([object]::ReferenceEquals($_.Parent, $sessionSnapshotExitBody) -and - [object]::ReferenceEquals($_, $sessionSnapshotExitBody.Statements[-1]))) -}) -$sessionSnapshotReturns = @($sessionControlTransfers | Where-Object { - $_ -is [System.Management.Automation.Language.ReturnStatementAst] -and - $_.Extent.Text.Trim() -eq 'return Get-Content $out -Raw | ConvertFrom-Json' -and - $sessionSnapshotOutputBody.Statements.Count -eq 1 -and - [object]::ReferenceEquals($_.Parent, $sessionSnapshotOutputBody) -and - [object]::ReferenceEquals($_, $sessionSnapshotOutputBody.Statements[0]) -}) -if ($sessionRunsInvocations.Count -eq $sessionExpectedRunsInvocations.Count) { - for ($i = 0; $i -lt $sessionExpectedRunsInvocations.Count; $i++) { - if ($sessionRunsInvocations[$i].Extent.Text.Trim() -ne $sessionExpectedRunsInvocations[$i]) { - throw 'Session restore invariant evidence set was mutated or reordered.' - } - } -} -if ($sessionStartAssignments.Count -eq $sessionExpectedStartAssignments.Count -and - $sessionRunsInvocations.Count -eq $sessionExpectedRunsInvocations.Count -and - $sessionAllStartCommands.Count -eq $sessionExpectedStartAssignments.Count) { - $sessionMainTryStatements = @($sessionMainTries[0].Body.Statements) - for ($i = 0; $i -lt $sessionExpectedStartAssignments.Count; $i++) { - if ($sessionStartAssignments[$i].Extent.Text.Trim() -ne $sessionExpectedStartAssignments[$i] -or - -not [object]::ReferenceEquals($sessionAllStartCommands[$i].Parent.Parent, $sessionStartAssignments[$i]) -or - -not (Test-DirectStatementBlockChild -Node $sessionStartAssignments[$i] -StatementBlock $sessionMainTries[0].Body) -or - -not (Test-DirectStatementBlockChild -Node $sessionRunsInvocations[$i] -StatementBlock $sessionMainTries[0].Body)) { - throw 'Session restore process launch and evidence registration must be direct main-story statements.' - } - $startIndex = -1 - $addIndex = -1 - for ($statementIndex = 0; $statementIndex -lt $sessionMainTryStatements.Count; $statementIndex++) { - if ([object]::ReferenceEquals($sessionMainTryStatements[$statementIndex], $sessionStartAssignments[$i])) { $startIndex = $statementIndex } - if ([object]::ReferenceEquals($sessionMainTryStatements[$statementIndex], $sessionRunsInvocations[$i].Parent.Parent)) { $addIndex = $statementIndex } - } - if ($addIndex -ne ($startIndex + 1)) { - throw 'Session restore must register each launched process immediately for cleanup and invariant scanning.' - } - if ($i -eq 0 -and ( - $startIndex -le 0 -or - $sessionMainTryStatements[$startIndex - 1].Extent.Text.Trim() -ne '$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)' -or - $sessionInitialWaitIndex -ne ($addIndex + 1))) { - throw 'Session restore initial launch, evidence registration, and host wait must share one original startup deadline without intervening work.' - } - if ($i -eq 1) { - $explicitHostStatement = Get-DirectStatementBlockChild -Node $sessionExplicitHostWaits[0] -StatementBlock $sessionMainTries[0].Body - $explicitTabStatement = Get-DirectStatementBlockChild -Node $sessionExplicitTabWaits[0] -StatementBlock $sessionMainTries[0].Body - $burstDeadlineStatement = Get-DirectStatementBlockChild -Node $sessionBurstDeadlines[0] -StatementBlock $sessionMainTries[0].Body - $explicitHostIndex = [Array]::IndexOf($sessionMainTryStatements, $explicitHostStatement) - $explicitTabIndex = [Array]::IndexOf($sessionMainTryStatements, $explicitTabStatement) - $burstDeadlineIndex = [Array]::IndexOf($sessionMainTryStatements, $burstDeadlineStatement) - if ($explicitHostIndex -ne ($addIndex + 1) -or $explicitTabIndex -ne ($explicitHostIndex + 1) -or - $burstDeadlineIndex -ne ($explicitTabIndex + 1)) { - throw 'Session explicit launch must proceed directly through host/tab readiness into the fresh burst deadline.' - } - } - } -} -if ($sessionInvariantIfs.Count -ne 1 -or $sessionInvariantThrows.Count -ne 1 -or - $sessionFunctionDefinitions.Count -ne 1 -or - -not [object]::ReferenceEquals($sessionFunctionDefinitions[0], $sessionSnapshotFunctions[0]) -or - $sessionRunsAssignments.Count -ne 1 -or - $sessionRunsAssignments[0].Extent.Text.Trim() -ne '$runs = [Collections.Generic.List[object]]::new()' -or - $sessionInstanceClassAssignments.Count -ne 1 -or - $sessionRunsInvocations.Count -ne $sessionExpectedRunsInvocations.Count -or - $sessionStartAssignments.Count -ne $sessionExpectedStartAssignments.Count -or - $sessionAllStartCommands.Count -ne $sessionExpectedStartAssignments.Count -or - $sessionEapAssignments.Count -ne 1 -or - $sessionEapAssignments[0].Extent.Text.Trim() -ne '$ErrorActionPreference = ''Stop''' -or - -not [object]::ReferenceEquals($sessionEapAssignments[0], $sessionTopLevelStatements[0]) -or - $sessionForbiddenTermination.Count -ne 0 -or - $sessionControlTransfers.Count -ne 4 -or $sessionBootstrapExits.Count -ne 1 -or - $sessionSnapshotContinues.Count -ne 2 -or $sessionSnapshotReturns.Count -ne 1 -or - $sessionInstanceClassIndex -ne ($sessionRunsInitializationIndex + 1) -or - $sessionSnapshotFunctionIndex -ne ($sessionInstanceClassIndex + 1) -or - $sessionMainTryIndex -ne ($sessionSnapshotFunctionIndex + 1) -or - $sessionInvariantIndex -ne ($sessionMainTryIndex + 1) -or - $sessionPassIndex -ne ($sessionInvariantIndex + 1) -or - $sessionPassIndex -ne ($sessionTopLevelStatements.Count - 1) -or - $sessionInvariantLoops[0].Extent.StartOffset -le $sessionMainTries[0].Extent.EndOffset -or - $sessionInvariantLoops[0].Extent.EndOffset -ge $sessionPassCommands[0].Extent.StartOffset) { - throw 'Session restore story must reach invariant rejection after cleanup and before the PASS sentinel without an early control transfer.' -} -$sessionBurstOffsets = @( - $sessionExplicitStarts[0].Extent.StartOffset, - $sessionExplicitTabWaits[0].Extent.StartOffset, - $sessionBurstDeadlines[0].Extent.StartOffset -) -for ($i = 1; $i -lt $sessionBurstOffsets.Count; $i++) { - if ($sessionBurstOffsets[$i - 1] -ge $sessionBurstOffsets[$i]) { - throw 'Session restore asynchronous burst operations are not in fail-closed causal order.' - } -} -$sessionBurstSequenceNodes = @( - $sessionBurstDeadlines[0], - $sessionBurstCommands[0], - $sessionBurstCommands[1], - $sessionBurstWaits[0], - $sessionBurstReady[0], - $sessionCloseDeadlines[0], - $sessionExplicitCloses[0] -) -$sessionBurstMainStatements = @($sessionMainTries[0].Body.Statements) -$sessionBurstStatementIndices = foreach ($node in $sessionBurstSequenceNodes) { - $statement = Get-DirectStatementBlockChild -Node $node -StatementBlock $sessionMainTries[0].Body - if ($null -eq $statement) { throw 'Session restore burst sequence contains a nested operation.' } - [Array]::IndexOf($sessionBurstMainStatements, $statement) -} -for ($i = 1; $i -lt $sessionBurstStatementIndices.Count; $i++) { - if ($sessionBurstStatementIndices[$i] -ne ($sessionBurstStatementIndices[$i - 1] + 1)) { - throw 'Session restore burst deadline, posts, proof barriers, and close must remain adjacent statements.' - } -} -$releasePreflightStep = Get-YamlStepBlock ` - -Content $releaseWorkflowText ` - -Name 'Release preflight' ` - -Source $releaseWorkflow -$readinessPreflightStep = Get-YamlStepBlock ` - -Content $readinessWorkflowText ` - -Name 'Validate release configuration' ` - -Source $readinessWorkflow -$releaseInteractiveEvidenceStep = Get-YamlStepBlock ` - -Content $releaseWorkflowText ` - -Name 'Require successful Test workflow for release SHA' ` - -Source $releaseWorkflow -$releasePreflightExpected = @{ - Version = '$env:RELEASE_VERSION' - RequireSigning = '$true' - RequirePackageManagers = '$true' -} -$releasePreflightScriptSha256 = - '11cef0f8ad78ad165cb61aa6a06a74cca36e523a84a2d2162a6d9b940c426065' -$readinessPreflightScriptSha256 = - '9e08d1fca871d8eb340ee88057e66b4472cef074eb0cab4ff728537ed439c7dc' -$releaseInteractiveEvidenceScriptSha256 = - '2bc265c1052c000e5f8ea599d2f5e2002dec3d3dc893bff0b8ecd72da739bb92' -$releaseInteractiveEvidenceStepSha256 = - '88a9d8d525eca9bb31327fdad515af39488a63b418222036b82edf94db945b6f' -$releasePreflightStepSha256 = - '0535add72682e9ee85894835766355e537d5a5a03174ed656e6365954d7f16eb' -$readinessPreflightStepSha256 = - '021214f70c1b21adcc770f9e96f66daf1ada2f9eae4180daf3958236941b05c9' -$releaseWorkflowSha256 = - '48322e91a5f0b006f1f01a427dd5abb92e714e230379431514393d50c90c81d9' -$readinessWorkflowSha256 = - '01cd56ba5049d3b74e89f329e3111de1161ab56bdfbfabf835536510139221cc' -# Full-file pins deliberately make every workflow edit a semantic-review event, -# including triggers, permissions, inherited job metadata, and unprotected steps. -$commonWorkflowBoundaryMutations = @( - @{ - Label = 'WinGet package redirect' - Target = ' WINGET_PACKAGE_IDENTIFIER: ${{ vars.WINGET_PACKAGE_IDENTIFIER }}' - Replacement = ' WINGET_PACKAGE_IDENTIFIER: attacker.forged.package' - }, - @{ - Label = 'Scoop repository redirect' - Target = ' SCOOP_BUCKET_REPO: ${{ vars.SCOOP_BUCKET_REPO }}' - Replacement = ' SCOOP_BUCKET_REPO: attacker/forged-bucket' - }, - @{ - Label = 'Scoop branch redirect' - Target = ' SCOOP_BUCKET_BRANCH: ${{ vars.SCOOP_BUCKET_BRANCH }}' - Replacement = ' SCOOP_BUCKET_BRANCH: forged-release' - }, - @{ - Label = 'runner redirect' - Target = ' runs-on: windows-latest' - Replacement = ' runs-on: [self-hosted, forged-release]' - }, - @{ - Label = 'environment redirect' - Target = ' environment: release' - Replacement = ' environment: forged-release' - } -) -$protectedWorkflowSpecs = @( - [pscustomobject]@{ - Context = $releaseWorkflow - Content = $releaseWorkflowText - ExpectedSha256 = $releaseWorkflowSha256 - ProtectedSteps = @( - @{ - Name = 'Require successful Test workflow for release SHA' - StepSha256 = $releaseInteractiveEvidenceStepSha256 - BodySha256 = $releaseInteractiveEvidenceScriptSha256 - }, - @{ - Name = 'Release preflight' - StepSha256 = $releasePreflightStepSha256 - BodySha256 = $releasePreflightScriptSha256 - } - ) - Mutations = @($commonWorkflowBoundaryMutations) + @( - @{ - Label = 'release permission reduction' - Target = ' contents: write' - Replacement = ' contents: read' - } - ) - }, - [pscustomobject]@{ - Context = $readinessWorkflow - Content = $readinessWorkflowText - ExpectedSha256 = $readinessWorkflowSha256 - ProtectedSteps = @( - @{ - Name = 'Validate release configuration' - StepSha256 = $readinessPreflightStepSha256 - BodySha256 = $readinessPreflightScriptSha256 - } - ) - Mutations = @($commonWorkflowBoundaryMutations) + @( - @{ - Label = 'readiness permission escalation' - Target = ' contents: read' - Replacement = ' contents: write' - } - ) - } -) -foreach ($spec in $protectedWorkflowSpecs) { - if ((Get-CanonicalTextSha256 -Text $spec.Content) -cne - $spec.ExpectedSha256) { - throw "Protected workflow changed: $($spec.Context)" - } - $canonicalWorkflow = ConvertTo-CanonicalText -Text $spec.Content - foreach ($mutation in $spec.Mutations) { - if (@([regex]::Matches( - $canonicalWorkflow, - [regex]::Escape($mutation.Target) - )).Count -ne 1) { - throw "Protected workflow mutation target is not unique: $($spec.Context) :: $($mutation.Label)" - } - $mutantWorkflow = $canonicalWorkflow.Replace( - $mutation.Target, - $mutation.Replacement - ) - if ((Get-CanonicalTextSha256 -Text $mutantWorkflow) -ceq - $spec.ExpectedSha256) { - throw "Protected workflow accepted full-file mutation: $($spec.Context) :: $($mutation.Label)" - } - foreach ($protectedStep in $spec.ProtectedSteps) { - $mutantStep = Get-YamlStepBlock ` - -Content $mutantWorkflow ` - -Name $protectedStep.Name ` - -Source "$($spec.Context) :: $($mutation.Label)" - $mutantBody = Get-YamlLiteralRunScript ` - -Content $mutantStep ` - -Source "$($spec.Context) :: $($mutation.Label) :: $($protectedStep.Name)" - if ((Get-CanonicalTextSha256 -Text $mutantStep) -cne - $protectedStep.StepSha256 -or - (Get-CanonicalTextSha256 -Text $mutantBody) -cne - $protectedStep.BodySha256) { - throw "Full-file mutation unexpectedly changed a protected step: $($spec.Context) :: $($mutation.Label)" - } - } - } -} -$protectedStepEnvelopeSpecs = @( - [pscustomobject]@{ - Name = 'Require successful Test workflow for release SHA' - Context = "$releaseWorkflow :: Require successful Test workflow for release SHA" - StepText = $releaseInteractiveEvidenceStep - ExpectedSha256 = $releaseInteractiveEvidenceStepSha256 - Mutations = @( - @{ - Label = 'GH_REPOSITORY redirect' - Target = ' GH_REPOSITORY: ${{ github.repository }}' - Replacement = ' GH_REPOSITORY: attacker/forged-evidence' - } - ) - }, - [pscustomobject]@{ - Name = 'Release preflight' - Context = "$releaseWorkflow :: Release preflight" - StepText = $releasePreflightStep - ExpectedSha256 = $releasePreflightStepSha256 - Mutations = @( - @{ - Label = 'release prerelease override' - Target = ' RELEASE_PRERELEASE: ${{ steps.meta.outputs.prerelease }}' - Replacement = ' RELEASE_PRERELEASE: true' - }, - @{ - Label = 'release version override' - Target = ' RELEASE_VERSION: ${{ steps.meta.outputs.version }}' - Replacement = ' RELEASE_VERSION: 0.0.0' - } - ) - }, - [pscustomobject]@{ - Name = 'Validate release configuration' - Context = "$readinessWorkflow :: Validate release configuration" - StepText = $readinessPreflightStep - ExpectedSha256 = $readinessPreflightStepSha256 - Mutations = @( - @{ - Label = 'readiness version override' - Target = ' RELEASE_VERSION: ${{ inputs.version }}' - Replacement = ' RELEASE_VERSION: 0.0.0' - }, - @{ - Label = 'readiness package-manager override' - Target = ' REQUIRE_PACKAGE_MANAGERS: ${{ inputs.require_package_managers }}' - Replacement = ' REQUIRE_PACKAGE_MANAGERS: false' - } - ) - } -) -foreach ($spec in $protectedStepEnvelopeSpecs) { - if (-not (Test-YamlStepEnvelopeDigest ` - -StepText $spec.StepText ` - -ExpectedSha256 $spec.ExpectedSha256)) { - throw "Protected workflow step envelope changed: $($spec.Context)" - } - $canonicalStep = ConvertTo-CanonicalText -Text $spec.StepText - $canonicalBody = Get-YamlLiteralRunScript ` - -Content $canonicalStep ` - -Source $spec.Context - $nameLine = " - name: $($spec.Name)" - $mutations = @( - @{ - Label = 'continue-on-error' - Target = $nameLine - Replacement = "$nameLine`n continue-on-error: true" - }, - @{ - Label = 'if false' - Target = $nameLine - Replacement = "$nameLine`n if: `${{ false }}" - }, - @{ - Label = 'GH_TOKEN redirect' - Target = ' GH_TOKEN: ${{ github.token }}' - Replacement = ' GH_TOKEN: ${{ secrets.FORGED_GH_TOKEN }}' - } - ) + @($spec.Mutations) - foreach ($mutation in $mutations) { - if (@([regex]::Matches( - $canonicalStep, - [regex]::Escape($mutation.Target) - )).Count -ne 1) { - throw "Protected step envelope mutation target is not unique: $($spec.Context) :: $($mutation.Label)" - } - $mutantStep = $canonicalStep.Replace( - $mutation.Target, - $mutation.Replacement - ) - $mutantBody = Get-YamlLiteralRunScript ` - -Content $mutantStep ` - -Source "$($spec.Context) :: $($mutation.Label)" - if ($mutantBody -cne $canonicalBody -or - (Test-YamlStepEnvelopeDigest ` - -StepText $mutantStep ` - -ExpectedSha256 $spec.ExpectedSha256)) { - throw "Protected workflow step envelope accepted metadata mutation: $($spec.Context) :: $($mutation.Label)" - } - } -} -Assert-NamedReleasePreflightSplat ` - -StepText $releasePreflightStep ` - -ExpectedExpressions $releasePreflightExpected ` - -ExpectedScriptSha256 $releasePreflightScriptSha256 ` - -Context "$releaseWorkflow :: Release preflight" -Assert-NamedReleasePreflightSplat ` - -StepText $readinessPreflightStep ` - -ExpectedExpressions @{ - Version = '$env:RELEASE_VERSION' - RequireSigning = '$true' - RequirePackageManagers = - '$env:REQUIRE_PACKAGE_MANAGERS -eq ''true''' - } ` - -ExpectedScriptSha256 $readinessPreflightScriptSha256 ` - -Context "$readinessWorkflow :: Validate release configuration" - -$releasePreflightScript = Get-YamlLiteralRunScript ` - -Content $releasePreflightStep ` - -Source "$releaseWorkflow :: Release preflight" -# Intentional protected-step edits require semantic review plus body/envelope -# digest updates; formatting changes are contract changes too. -$releasePreflightWhitespaceMutant = $releasePreflightScript.Replace( - '$preflightArgs = @{', - '$preflightArgs = @{' -) -if ($releasePreflightWhitespaceMutant -ceq $releasePreflightScript -or - (Test-NamedReleasePreflightSplat ` - -ScriptText $releasePreflightWhitespaceMutant ` - -ExpectedExpressions $releasePreflightExpected ` - -ExpectedScriptSha256 $releasePreflightScriptSha256)) { - throw 'Release-preflight script digest accepted a whitespace mutation.' -} -$releasePreflightInvocationTarget = - './scripts/release-preflight.ps1 @preflightArgs' -if (-not $releasePreflightScript.Contains($releasePreflightInvocationTarget)) { - throw 'Release-preflight mutation insertion target is missing.' -} -$invalidSplatMutants = @{ - 'later signing mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$preflightArgs.RequireSigning = $false -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'flat argument array' = @' -$preflightArgs = @('-Version', $env:RELEASE_VERSION, '-RequireSigning', '-RequirePackageManagers') -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'extra invocation argument' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -./scripts/release-preflight.ps1 @preflightArgs -RequireSigning -'@ - 'missing key' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true -} -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'extra key' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true - RequireAccessibilityEvidence = $true -} -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Set-Variable mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -Set-Variable -Name preflightArgs -Value @{} -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'stored provider path Set-Item mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$providerPath = 'variable:preflightArgs' -Set-Item -Path $providerPath -Value @{} -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Set-Item alias mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -si -Path variable:preflightArgs -Value @{} -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Copy-Item alias mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -cpi -Path variable:preflightArgs -Destination variable:shadow -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Move-Item alias mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -mv -Path variable:preflightArgs -Destination variable:shadow -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Rename-Item alias mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -rni -Path variable:preflightArgs -NewName shadow -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'New-Item alias mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -ni -Path variable:preflightArgs -Value @{} -Force -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'module-qualified New-Item stored provider path' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$providerPath = 'variable:preflightArgs' -Microsoft.PowerShell.Management\New-Item -Path $providerPath -Value @{} -Force -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Get-ChildItem alias stored provider mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$providerPath = 'variable:preflightArgs' -$providerItem = gci -Path $providerPath -$providerItem.Value = @{} -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Get-Content stored provider mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$providerPath = 'variable:preflightArgs' -$providerItem = Get-Content -Path $providerPath -$providerItem.Value = @{} -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'wrapped ExecutionContext mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$ec = @($ExecutionContext)[0] -$ec.SessionState.PSVariable.Set('preflightArgs', @{}) -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'SessionState PSVariable mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$ExecutionContext.SessionState.PSVariable.Set('preflightArgs', @{}) -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Invoke-Expression mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -iex '$preflightArgs = @{}' -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'dynamic call mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$mutator = 'Set-Variable' -& $mutator -Name preflightArgs -Value @{} -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'static dot-source mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -. ./scripts/replace-release-evidence.ps1 -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'static call-operator mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -& Write-Output harmless -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'stored reflection mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -$scriptBlockType = {}.GetType() -$createMethod = $scriptBlockType.GetMethod('Create', [type[]]@([string])) -$payload = $createMethod.Invoke($null, @('Set-Variable -Scope 1 -Name preflightArgs -Value @{}')) -$payload.Invoke() -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'DefaultRunspace state proxy mutation' = @' -$preflightArgs = @{ - Version = $env:RELEASE_VERSION - RequireSigning = $true - RequirePackageManagers = $true -} -[runspace]::DefaultRunspace.SessionStateProxy.SetVariable('preflightArgs', @{}) -./scripts/release-preflight.ps1 @preflightArgs -'@ - 'Tee-Object variable mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "Write-Output @{} | Tee-Object -Variable preflightArgs | Out-Null`n$releasePreflightInvocationTarget" - ) - 'OutVariable mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "Write-Output @{} -OutVariable preflightArgs | Out-Null`n$releasePreflightInvocationTarget" - ) - 'OutVariable prefix mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "Write-Output @{} -OutV preflightArgs | Out-Null`n$releasePreflightInvocationTarget" - ) - 'PipelineVariable mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "Write-Output @{} -PipelineVariable preflightArgs | Out-Null`n$releasePreflightInvocationTarget" - ) - 'PipelineVariable prefix mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "Write-Output @{} -PipelineV preflightArgs | Out-Null`n$releasePreflightInvocationTarget" - ) - 'ErrorVariable mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "Write-Error forged -ErrorVariable preflightArgs -ErrorAction SilentlyContinue`n$releasePreflightInvocationTarget" - ) - 'PSObject member dispatch mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "`$dispatch = [pscustomobject]@{ mutate = { Set-Variable -Scope 1 -Name preflightArgs -Value @{} } }`n`$dispatch.PSObject.Properties['mutate'].Value.Invoke()`n$releasePreflightInvocationTarget" - ) - 'Add-Type static method mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "Add-Type -TypeDefinition 'public static class PreflightMutator { public static void Set() {} }'`n[PreflightMutator]::Set()`n$releasePreflightInvocationTarget" - ) - 'Assembly Load mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "[Reflection.Assembly]::Load([Convert]::FromBase64String('AA=='))`n$releasePreflightInvocationTarget" - ) - 'ForEach-Object MemberName dispatch mutation' = - $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "[PSObject].Assembly | ForEach-Object -MemberName ('Get' + 'Type') -ArgumentList 'System.Management.Automation.ScriptBlock'`n$releasePreflightInvocationTarget" - ) - 'PSCmdlet session-state mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "function Invoke-PreflightMutation { [CmdletBinding()] param(); `$PSCmdlet.SessionState.PSVariable.Set('script:preflightArgs', @{}) }; Invoke-PreflightMutation`n$releasePreflightInvocationTarget" - ) - 'splatted PipelineVariable mutation' = $releasePreflightScript.Replace( - $releasePreflightInvocationTarget, - "`$writeParams = @{ PipelineVariable = 'preflightArgs' }; Write-Output @{} @writeParams`n$releasePreflightInvocationTarget" - ) -} -foreach ($mutant in $invalidSplatMutants.GetEnumerator()) { - if (Test-NamedReleasePreflightSplat ` - -ScriptText $mutant.Value ` - -ExpectedExpressions $releasePreflightExpected ` - -ExpectedScriptSha256 $releasePreflightScriptSha256) { - throw "Named release-preflight splat contract accepted mutant: $($mutant.Key)" - } -} - -$releaseInteractiveEvidenceScript = Get-YamlLiteralRunScript ` - -Content $releaseInteractiveEvidenceStep ` - -Source "$releaseWorkflow :: Require successful Test workflow for release SHA" -if (-not (Test-ReleaseInteractiveResultSelectionContract ` - -ScriptText $releaseInteractiveEvidenceScript)) { - throw 'Release interactive evidence must select one composite result without arbitrary first-match fallback.' -} -$countGateText = 'if ($resultFiles.Count -ne 1) { continue }' -$countGateIndex = $releaseInteractiveEvidenceScript.IndexOf( - $countGateText, - [StringComparison]::Ordinal -) -if ($countGateIndex -lt 0) { - throw 'Release result-selection count gate mutation target is missing.' -} -$countBeforeFilterScript = $releaseInteractiveEvidenceScript.Remove( - $countGateIndex, - $countGateText.Length -) -$resultFilterIndex = $countBeforeFilterScript.IndexOf( - '$resultFiles = @(', - [StringComparison]::Ordinal -) -if ($resultFilterIndex -lt 0) { - throw 'Release result-selection filter mutation target is missing.' -} -$countBeforeFilterScript = $countBeforeFilterScript.Insert( - $resultFilterIndex, - "$countGateText`n" -) -$filterAfterCountScript = @' -$resultFiles = @( - Get-ChildItem -LiteralPath $artifactRoot -Filter result.json -File -Recurse -) -if ($resultFiles.Count -ne 1) { continue } -$resultFiles = @( - $resultFiles | Where-Object { - try { - (Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json).scenario_id -eq 'windows.interactive-win11.composite' - } - catch { $false } - } -) -$resultPath = $resultFiles[0].FullName -$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json -'@ -$noCatchScript = [regex]::Replace( - $releaseInteractiveEvidenceScript, - '(?ms)try\s*\{\s*(?\(Get-Content -LiteralPath \$_\.FullName -Raw \| ConvertFrom-Json\)\.scenario_id -eq ''windows\.interactive-win11\.composite'')\s*\}\s*catch\s*\{\s*\$false\s*\}', - '${predicate}' -) -$invalidResultSelectionMutants = @{ - 'zero-only count' = $releaseInteractiveEvidenceScript.Replace( - '$resultFiles.Count -ne 1', - '$resultFiles.Count -eq 0' - ) - 'wrong scenario' = $releaseInteractiveEvidenceScript.Replace( - 'windows.interactive-win11.composite', - 'windows.interactive-win11.other' - ) - 'arbitrary first match' = - $releaseInteractiveEvidenceScript + "`nSelect-Object -First 1" - 'count before filter' = $countBeforeFilterScript - 'filter after count' = $filterAfterCountScript - 'missing catch' = $noCatchScript - 'fail-open catch' = $releaseInteractiveEvidenceScript.Replace( - 'catch { $false }', - 'catch { $true }' - ) - 'nested count gate' = $releaseInteractiveEvidenceScript.Replace( - 'if ($resultFiles.Count -ne 1) { continue }', - 'if ($false) { if ($resultFiles.Count -ne 1) { continue } }' - ) - 'nested result path' = $releaseInteractiveEvidenceScript.Replace( - '$resultPath = $resultFiles[0].FullName', - 'if ($false) { $resultPath = $resultFiles[0].FullName }' - ) - 'nested result parse' = $releaseInteractiveEvidenceScript.Replace( - '$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json', - 'if ($false) { $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json }' - ) -} -foreach ($mutant in $invalidResultSelectionMutants.GetEnumerator()) { - if (Test-ReleaseInteractiveResultSelectionContract -ScriptText $mutant.Value) { - throw "Release result-selection contract accepted mutant: $($mutant.Key)" - } -} -if (-not (Test-ReleaseInteractiveSuccessPredicates ` - -ScriptText $releaseInteractiveEvidenceScript ` - -ExpectedScriptSha256 $releaseInteractiveEvidenceScriptSha256)) { - throw 'Release interactive evidence must require successful exact-head candidates and a passing result.' -} -$runsSourceTarget = '$runs = @($runsJson | ConvertFrom-Json)' -$otherJsonSourceScript = $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - '$runs = @($otherJson | ConvertFrom-Json)' -) -$matchingSourceTarget = '$matching = @($runs | Where-Object {' -$otherRunsSourceScript = $releaseInteractiveEvidenceScript.Replace( - $matchingSourceTarget, - '$matching = @($otherRuns | Where-Object {' -) -$matchingCountTarget = 'if ($matching.Count -eq 0) {' -$shaAssignmentTarget = '$sha = (git rev-parse HEAD).Trim()' -$resultAssignmentTarget = - '$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json' -$artifactFloorTarget = - 'if (@($result.artifacts).Count -eq 0) { continue }' -$hashAcceptanceTarget = 'if ($result.status -eq ''pass'' -and' -$evidenceGuardTarget = 'if (-not $evidenceRun) {' -$resultDirAssignmentTarget = - '$resultDir = Split-Path -Parent $resultPath' -$resultDirJoinTarget = - 'Join-Path $resultDir ([string]$artifact.path)' -$evidenceWriteTarget = - 'Write-Host "Release SHA $sha is covered by interactive Test run $($evidenceRun.databaseId)."' -$missingMetadataGuardTarget = - 'if (-not $artifact.sha256 -or -not $artifact.path) {' -$containmentGuardTarget = - 'if (-not $artifactPath.StartsWith($artifactRootFull, [StringComparison]::OrdinalIgnoreCase) -or' + - "`n" + - ' -not (Test-Path -LiteralPath $artifactPath -PathType Leaf)) {' -$hashMismatchGuardTarget = - 'if ($actualHash -ne ([string]$artifact.sha256).ToLowerInvariant()) {' -if ($otherJsonSourceScript -ceq $releaseInteractiveEvidenceScript -or - $otherRunsSourceScript -ceq $releaseInteractiveEvidenceScript -or - -not $releaseInteractiveEvidenceScript.Contains($matchingCountTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($shaAssignmentTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($resultAssignmentTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($artifactFloorTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($hashAcceptanceTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($evidenceGuardTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($resultDirAssignmentTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($resultDirJoinTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($evidenceWriteTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($missingMetadataGuardTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($containmentGuardTarget) -or - -not $releaseInteractiveEvidenceScript.Contains($hashMismatchGuardTarget)) { - throw 'Release success-predicate source-binding mutation target is missing.' -} -$addTypeEvidenceMutation = - "Add-Type -TypeDefinition 'public static class EvidenceMutator { public static void Set() {} }'`n[EvidenceMutator]::Set()" -$memberDispatchEvidenceMutation = - "[PSObject].Assembly | ForEach-Object -MemberName ('Get' + 'Type') -ArgumentList 'System.Management.Automation.ScriptBlock'" -$invalidSuccessSourceMutants = @{ - 'runs JSON source substitution' = $otherJsonSourceScript - 'runs JSON source comment decoy' = - $otherJsonSourceScript + "`n# $runsSourceTarget" - 'runs JSON source dead-code decoy' = - $otherJsonSourceScript + - "`n" + - 'if ($false) { $runs = @($runsJson | ConvertFrom-Json) }' - 'matching source substitution' = $otherRunsSourceScript - 'matching source comment decoy' = - $otherRunsSourceScript + "`n# $matchingSourceTarget" - 'matching source dead-code decoy' = - $otherRunsSourceScript + - "`n" + - 'if ($false) { $matching = @($runs | Where-Object { $_.headSha -eq $sha -and $_.status -eq "completed" -and $_.conclusion -eq "success" }) }' - 'live nested runs JSON reassignment' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "if (`$true) { `$runsJson = `$otherJson }`n$runsSourceTarget" - ) - 'dead nested runs JSON reassignment' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "if (`$false) { `$runsJson = `$otherJson }`n$runsSourceTarget" - ) - 'live nested runs reassignment' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "$runsSourceTarget`nif (`$true) { `$runs = @(`$otherJson | ConvertFrom-Json) }" - ) - 'dead nested runs reassignment' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "$runsSourceTarget`nif (`$false) { `$runs = @(`$otherJson | ConvertFrom-Json) }" - ) - 'live nested matching reassignment' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "if (`$true) { `$matching = @(`$otherRuns) }`n$matchingCountTarget" - ) - 'dead nested matching reassignment' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "if (`$false) { `$matching = @(`$otherRuns) }`n$matchingCountTarget" - ) - 'indexed runs JSON mutation' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "`$runsJson[0] = 'x'`n$runsSourceTarget" - ) - 'indexed runs mutation' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "$runsSourceTarget`n`$runs[0] = `$otherRun" - ) - 'indexed matching mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$matching[0] = `$otherRun`n$matchingCountTarget" - ) - 'runs method mutation' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "$runsSourceTarget`n`$runs.Clear()" - ) - 'matching member decoy' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$null = `$matching.Count`n$matchingCountTarget" - ) - 'runs JSON unexpected read' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "`$null = `$runsJson`n$runsSourceTarget" - ) - 'Set-Variable runs mutation' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "$runsSourceTarget`nSet-Variable -Name runs -Value @(`$otherRun)" - ) - 'SessionState PSVariable runs mutation' = - $releaseInteractiveEvidenceScript.Replace( - $runsSourceTarget, - "$runsSourceTarget`n`$ExecutionContext.SessionState.PSVariable.Set('runs', @(`$otherRun))" - ) - 'qualified Set-Variable mutation' = - $releaseInteractiveEvidenceScript + - "`nMicrosoft.PowerShell.Utility\Set-Variable -Name runs -Value @(`$otherRun)" - 'Set-Variable alias mutation' = - $releaseInteractiveEvidenceScript + - "`nsv -Name runs -Value @(`$otherRun)" - 'Set-Variable set alias mutation' = - $releaseInteractiveEvidenceScript + - "`nset -Name runs -Value @(`$otherRun)" - 'New-Variable mutation' = - $releaseInteractiveEvidenceScript + - "`nNew-Variable -Name runs -Value @(`$otherRun)" - 'New-Variable alias mutation' = - $releaseInteractiveEvidenceScript + - "`nnv -Name runs -Value @(`$otherRun)" - 'Remove-Variable mutation' = - $releaseInteractiveEvidenceScript + - "`nRemove-Variable -Name runs" - 'Remove-Variable alias mutation' = - $releaseInteractiveEvidenceScript + - "`nrv -Name runs" - 'Clear-Variable mutation' = - $releaseInteractiveEvidenceScript + - "`nClear-Variable -Name runs" - 'Clear-Variable alias mutation' = - $releaseInteractiveEvidenceScript + - "`nclv -Name runs" - 'Invoke-Expression mutation' = - $releaseInteractiveEvidenceScript + - "`niex '`$runs = @(`$otherRun)'" - 'ScriptBlock Create mutation' = - $releaseInteractiveEvidenceScript + - "`n[ScriptBlock]::Create('`$runs = @(`$otherRun)').Invoke()" - 'dynamic Set-Variable call mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$mutator = 'Set-Variable'`n& `$mutator -Name matching -Value @(`$otherRun)`n$matchingCountTarget" - ) - 'Get-Command call mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "& (Get-Command Set-Variable) -Name matching -Value @(`$otherRun)`n$matchingCountTarget" - ) - 'module-qualified stored command mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$mutator = 'Microsoft.PowerShell.Utility\Set-Variable'`n& `$mutator -Name matching -Value @(`$otherRun)`n$matchingCountTarget" - ) - 'static dot-source mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - ". ./scripts/replace-release-evidence.ps1`n$matchingCountTarget" - ) - 'static call-operator mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "& Write-Output harmless`n$matchingCountTarget" - ) - 'stored reflection mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$scriptBlockType = {}.GetType()`n`$createMethod = `$scriptBlockType.GetMethod('Create', [type[]]@([string]))`n`$payload = `$createMethod.Invoke(`$null, @('Set-Variable -Scope 1 -Name matching -Value @()'))`n`$payload.Invoke()`n$matchingCountTarget" - ) - 'DefaultRunspace state proxy mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "[runspace]::DefaultRunspace.SessionStateProxy.SetVariable('matching', @(`$otherRun))`n$matchingCountTarget" - ) - 'Tee-Object variable mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "Write-Output ([pscustomobject]@{ databaseId = 4242 }) | Tee-Object -Variable matching | Out-Null`n$matchingCountTarget" - ) - 'OutVariable mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "Write-Output ([pscustomobject]@{ databaseId = 4242 }) -OutVariable matching | Out-Null`n$matchingCountTarget" - ) - 'OutVariable prefix mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "Write-Output ([pscustomobject]@{ databaseId = 4242 }) -OutV matching | Out-Null`n$matchingCountTarget" - ) - 'PipelineVariable mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "Write-Output ([pscustomobject]@{ databaseId = 4242 }) -PipelineVariable matching | Out-Null`n$matchingCountTarget" - ) - 'PipelineVariable prefix mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "Write-Output ([pscustomobject]@{ databaseId = 4242 }) -PipelineV matching | Out-Null`n$matchingCountTarget" - ) - 'ErrorVariable mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "Write-Error forged -ErrorVariable matching -ErrorAction SilentlyContinue`n$matchingCountTarget" - ) - 'PSObject member dispatch mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$dispatch = [pscustomobject]@{ mutate = { Set-Variable -Scope 1 -Name matching -Value @() } }`n`$dispatch.PSObject.Properties['mutate'].Value.Invoke()`n$matchingCountTarget" - ) - 'Add-Type static method mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - $addTypeEvidenceMutation + "`n" + $matchingCountTarget - ) - 'Assembly Load mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "[Reflection.Assembly]::Load([Convert]::FromBase64String('AA=='))`n$matchingCountTarget" - ) - 'ForEach-Object MemberName dispatch mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - $memberDispatchEvidenceMutation + "`n" + $matchingCountTarget - ) - 'PSCmdlet session-state mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "function Invoke-EvidenceMutation {`n [CmdletBinding()]`n param()`n `$PSCmdlet.SessionState.PSVariable.Set('script:matching', @())`n}`nInvoke-EvidenceMutation`n$matchingCountTarget" - ) - 'splatted OutVariable mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$writeParams = @{ OutVariable = 'matching' }`nWrite-Output ([pscustomobject]@{ databaseId = 4242 }) @writeParams`n$matchingCountTarget" - ) - 'stored provider path Set-Item mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$providerPath = 'variable:matching'`nSet-Item -Path `$providerPath -Value @(`$otherRun)`n$matchingCountTarget" - ) - 'stored provider path Get-Item access' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$providerPath = 'variable:matching'`n`$providerItem = Get-Item -Path `$providerPath`n$matchingCountTarget" - ) - 'Clear-Item alias mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "cli -Path variable:matching`n$matchingCountTarget" - ) - 'Remove-Item alias mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "ri -Path variable:matching`n$matchingCountTarget" - ) - 'Copy-Item alias mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "cpi -Path variable:matching -Destination variable:other`n$matchingCountTarget" - ) - 'Move-Item alias mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "mv -Path variable:matching -Destination variable:other`n$matchingCountTarget" - ) - 'Rename-Item alias mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "rni -Path variable:matching -NewName other`n$matchingCountTarget" - ) - 'ItemProperty alias mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "sp -Path variable:matching -Name Value -Value @(`$otherRun)`n$matchingCountTarget" - ) - 'Content alias mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "sc -Path variable:matching -Value 'forged'`n$matchingCountTarget" - ) - 'extra New-Item alias mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "ni -Path variable:matching -Value @(`$otherRun) -Force`n$matchingCountTarget" - ) - 'module-qualified New-Item stored provider path' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$providerPath = 'variable:matching'`nMicrosoft.PowerShell.Management\New-Item -Path `$providerPath -Value @(`$otherRun) -Force`n$matchingCountTarget" - ) - 'Get-ChildItem alias stored provider mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$providerPath = 'variable:matching'`n`$providerItem = gci -Path `$providerPath`n`$providerItem.Value = @(`$otherRun)`n$matchingCountTarget" - ) - 'Get-Content stored provider mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$providerPath = 'variable:matching'`n`$providerItem = Get-Content -Path `$providerPath`n`$providerItem.Value = @(`$otherRun)`n$matchingCountTarget" - ) - 'wrapped ExecutionContext mutation' = - $releaseInteractiveEvidenceScript.Replace( - $matchingCountTarget, - "`$ec = @(`$ExecutionContext)[0]`n`$ec.SessionState.PSVariable.Set('matching', @(`$otherRun))`n$matchingCountTarget" - ) - 'count-preserving evidenceRun ref substitution' = - $releaseInteractiveEvidenceScript.Replace( - $evidenceWriteTarget, - "`$evidenceRunRef = [ref]`$evidenceRun`nWrite-Host `"Release SHA `$sha is covered by an interactive Test run.`"" - ) - 'count-preserving resultDir ref substitution' = - $releaseInteractiveEvidenceScript.Replace( - $resultDirAssignmentTarget, - "$resultDirAssignmentTarget`n`$resultDirRef = [ref]`$resultDir" - ).Replace( - $resultDirJoinTarget, - 'Join-Path $artifactRoot ([string]$artifact.path)' - ) - 'missing metadata guard neutralized' = - $releaseInteractiveEvidenceScript.Replace( - $missingMetadataGuardTarget, - 'if ($false) {' - ) - 'containment guard neutralized' = - $releaseInteractiveEvidenceScript.Replace( - $containmentGuardTarget, - 'if ($false) {' - ) - 'hash mismatch guard neutralized' = - $releaseInteractiveEvidenceScript.Replace( - $hashMismatchGuardTarget, - 'if ($false) {' - ) - 'forged result assignment' = - $releaseInteractiveEvidenceScript.Replace( - $resultAssignmentTarget, - "$resultAssignmentTarget`nif (`$true) { `$result = [pscustomobject]@{ status = 'pass'; implementation_commit = `$sha; workflow_run_id = `$run.databaseId; artifacts = @() } }" - ) - 'forged SHA assignment' = - $releaseInteractiveEvidenceScript.Replace( - $shaAssignmentTarget, - "$shaAssignmentTarget`nif (`$true) { `$sha = 'forged' }" - ) - 'forged hashes-bound assignment' = - $releaseInteractiveEvidenceScript.Replace( - $hashAcceptanceTarget, - "if (`$true) { `$hashesBound = `$true }`n$hashAcceptanceTarget" - ) - 'forged evidence-run assignment' = - $releaseInteractiveEvidenceScript.Replace( - $evidenceGuardTarget, - "if (`$true) { `$evidenceRun = [pscustomobject]@{ databaseId = 1 } }`n$evidenceGuardTarget" - ) - 'result member mutation' = - $releaseInteractiveEvidenceScript.Replace( - $resultAssignmentTarget, - "$resultAssignmentTarget`n`$result.status = 'pass'" - ) - 'vacuous artifact floor' = - $releaseInteractiveEvidenceScript.Replace( - $artifactFloorTarget, - 'if (@($result.artifacts).Count -lt 0) { continue }' - ) -} -foreach ($mutant in $invalidSuccessSourceMutants.GetEnumerator()) { - if (Test-ReleaseInteractiveSuccessPredicates ` - -ScriptText $mutant.Value ` - -ExpectedScriptSha256 $releaseInteractiveEvidenceScriptSha256) { - throw "Release success-predicate contract accepted source mutant: $($mutant.Key)" - } -} -$successPredicateMutationSpecs = @( - [pscustomobject]@{ - Label = 'GitHub success filter' - Active = '--status success' - Removed = '--status queued' - Inverted = '--status failure' - Dead = 'if ($false) { ''--status success'' }' - }, - [pscustomobject]@{ - Label = 'exact candidate head' - Active = '$_.headSha -eq $sha' - Removed = '$true' - Inverted = '$_.headSha -ne $sha' - Dead = 'if ($false) { $_.headSha -eq $sha }' - }, - [pscustomobject]@{ - Label = 'completed candidate' - Active = '$_.status -eq "completed"' - Removed = '$true' - Inverted = '$_.status -ne "completed"' - Dead = 'if ($false) { $_.status -eq "completed" }' - }, - [pscustomobject]@{ - Label = 'successful candidate' - Active = '$_.conclusion -eq "success"' - Removed = '$true' - Inverted = '$_.conclusion -ne "success"' - Dead = 'if ($false) { $_.conclusion -eq "success" }' - }, - [pscustomobject]@{ - Label = 'passing result' - Active = '$result.status -eq ''pass''' - Removed = '$true' - Inverted = '$result.status -ne ''pass''' - Dead = 'if ($false) { $result.status -eq ''pass'' }' - }, - [pscustomobject]@{ - Label = 'evidence implementation SHA' - Active = '$result.implementation_commit -eq $sha' - Removed = '$true' - Inverted = '$result.implementation_commit -ne $sha' - Dead = 'if ($false) { $result.implementation_commit -eq $sha }' - }, - [pscustomobject]@{ - Label = 'evidence workflow run' - Active = - '[string]$result.workflow_run_id -eq [string]$run.databaseId' - Removed = '$true' - Inverted = - '[string]$result.workflow_run_id -ne [string]$run.databaseId' - Dead = - 'if ($false) { [string]$result.workflow_run_id -eq [string]$run.databaseId }' - }, - [pscustomobject]@{ - Label = 'evidence artifact hashes' - Active = '$hashesBound' - Removed = '$true' - Inverted = '-not $hashesBound' - Dead = 'if ($false) { $hashesBound }' - } -) -foreach ($spec in $successPredicateMutationSpecs) { - $targetIndex = $releaseInteractiveEvidenceScript.LastIndexOf( - $spec.Active, - [StringComparison]::Ordinal - ) - if ($targetIndex -lt 0) { - throw "Release success-predicate mutation target is missing: $($spec.Label)" - } - $prefix = $releaseInteractiveEvidenceScript.Substring(0, $targetIndex) - $suffix = $releaseInteractiveEvidenceScript.Substring( - $targetIndex + $spec.Active.Length - ) - $removedScript = $prefix + $spec.Removed + $suffix - $invertedScript = $prefix + $spec.Inverted + $suffix - $mutants = @( - [pscustomobject]@{ - Label = "$($spec.Label) inversion" - Script = $invertedScript - }, - [pscustomobject]@{ - Label = "$($spec.Label) comment decoy" - Script = $removedScript + "`n# $($spec.Active)" - }, - [pscustomobject]@{ - Label = "$($spec.Label) dead-code decoy" - Script = $removedScript + "`n$($spec.Dead)" - } - ) - foreach ($mutant in $mutants) { - if (Test-ReleaseInteractiveSuccessPredicates ` - -ScriptText $mutant.Script ` - -ExpectedScriptSha256 $releaseInteractiveEvidenceScriptSha256) { - throw "Release success-predicate contract accepted mutant: $($mutant.Label)" - } - } -} -Assert-TextContract ` - -Content $releaseInteractiveEvidenceStep ` - -Pattern '(?ms)gh run list.*?--workflow Test.*?--commit \$sha.*?\$_\.name -eq ''Windows 11 Interactive Composite''.*?\$_\.conclusion -eq ''success''.*?\$artifact\.sha256.*?\$artifact\.path.*?Get-FileHash.*?\$result\.implementation_commit -eq \$sha.*?\$result\.workflow_run_id.*?\$hashesBound.*?exact-SHA, hash-bound evidence' ` - -Description 'release remains gated on successful exact-SHA hash-bound interactive evidence' ` - -Context "$releaseWorkflow :: Require successful Test workflow for release SHA" -Assert-TextContract ` - -Content $releasePreflightStep ` - -Pattern '(?ms)check-release-copy\.ps1 -ExpectedVersion.*?\r?\n\s+if \(\$LASTEXITCODE -ne 0\) \{ exit \$LASTEXITCODE \}' ` - -Description 'release preflight propagates release-copy failures' ` - -Context "$releaseWorkflow :: Release preflight" -Assert-TextContract ` - -Content $readinessPreflightStep ` - -Pattern '(?ms)check-release-copy\.ps1 -ExpectedVersion.*?\r?\n\s+if \(\$LASTEXITCODE -ne 0\) \{ exit \$LASTEXITCODE \}' ` - -Description 'release readiness propagates release-copy failures' ` - -Context "$readinessWorkflow :: Validate release configuration" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $releaseWorkflowText -Name 'Verify published release copy and assets' -Source $releaseWorkflow) ` - -Pattern '(?ms)env:\s+GH_TOKEN: \$\{\{ github\.token \}\}.*?CheckRemoteLatest.*?if \(\$LASTEXITCODE -ne 0\) \{ exit \$LASTEXITCODE \}.*?verify-published-release\.ps1 -Version \$env:RELEASE_VERSION.*?if \(\$LASTEXITCODE -ne 0\) \{ exit \$LASTEXITCODE \}' ` - -Description 'post-publish remote verification authenticates gh and fails closed on copy and byte/signature checks' ` - -Context "$releaseWorkflow :: Verify published release copy and assets" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $releaseWorkflowText -Name 'Publish GitHub Release' -Source $releaseWorkflow) ` - -Pattern '(?ms)GH_REPO: \$\{\{ github\.repository \}\}.*?gh release view \$tag --repo \$env:GH_REPO.*?gh release create \$tag --repo \$env:GH_REPO.*?if \(\$LASTEXITCODE -ne 0\).*?gh release edit \$tag --repo \$env:GH_REPO.*?if \(\$LASTEXITCODE -ne 0\).*?gh release upload \$tag --repo \$env:GH_REPO.*?if \(\$LASTEXITCODE -ne 0\)' ` - -Description 'GitHub release commands pin the fork and mutations fail closed' ` - -Context "$releaseWorkflow :: Publish GitHub Release" -$defenderScanStep = Get-YamlStepBlock ` - -Content $releaseWorkflowText ` - -Name 'Scan Windows release artifacts with Microsoft Defender' ` - -Source $releaseWorkflow -Assert-TextContract ` - -Content $defenderScanStep ` - -Pattern '(?ms)Get-MpComputerStatus -ErrorAction Stop.*?AMServiceEnabled.*?AntivirusEnabled.*?AMRunningMode -ne "Normal".*?-replace ''-\\d\+\$'', ''''.*?-as \[version\].*?Sort-Object Version -Descending.*?MpCmdRun\.exe.*?-SignatureUpdate.*?if \(\$LASTEXITCODE -ne 0\).*?noctty/noctty\.com.*?noctty/noctty\.exe.*?noctty/ghostty-vt\.dll.*?Get-WindowsPackageArchitectures.*?-Kind setup.*?noctty-release-verify-\$arch.*?scanPaths\.Count -ne 8.*?-Scan -ScanType 3 -File \$scanPath -DisableRemediation -ReturnHR.*?if \(\$LASTEXITCODE -ne 0\)' ` - -Description 'release scans installers and portable PE payloads with active current Microsoft Defender and fails closed' ` - -Context "$releaseWorkflow :: Scan Windows release artifacts with Microsoft Defender" -$signedArtifactStepIndex = $releaseWorkflowText.IndexOf(' - name: Verify signed release artifacts') -$defenderScanStepIndex = $releaseWorkflowText.IndexOf(' - name: Scan Windows release artifacts with Microsoft Defender') -$publishReleaseStepIndex = $releaseWorkflowText.IndexOf(' - name: Publish GitHub Release') -if ($signedArtifactStepIndex -lt 0 -or - $defenderScanStepIndex -le $signedArtifactStepIndex -or - $publishReleaseStepIndex -le $defenderScanStepIndex) { - throw 'Microsoft Defender scanning must run after artifact verification and before release publication.' -} -$signedArtifactStep = Get-YamlStepBlock ` - -Content $releaseWorkflowText ` - -Name 'Verify signed release artifacts' ` - -Source $releaseWorkflow -$signingTrustConsumers = @( - [pscustomobject]@{ - Context = $publishedReleaseVerifier - Content = $publishedReleaseVerifierText - ImportPattern = '^\.\s+\(\s*Join-Path\s+\$PSScriptRoot\s+[''"]signing-trust\.ps1[''"]\s*\)$' - ExpectedPath = '$Path' - PinPattern = 'Get-CertificateSpkiSha256.*?\$AllowedPins -notcontains \$pin' - }, - [pscustomobject]@{ - Context = $windowsPackager - Content = $windowsPackagerText - ImportPattern = '^\.\s+\(\s*Join-Path\s+\$PSScriptRoot\s+[''"]signing-trust\.ps1[''"]\s*\)$' - ExpectedPath = '$PathToCheck' - PinPattern = 'SignerCertificate\.Thumbprint -ne \$SigningConfig\.CertificateThumbprint' - }, - [pscustomobject]@{ - Context = "$releaseWorkflow :: Verify signed release artifacts" - Content = ((Get-YamlLiteralRunScript ` - -Content $signedArtifactStep ` - -Source "$releaseWorkflow :: Verify signed release artifacts") -replace '\$\{\{[^}]+\}\}', 'GITHUB_EXPRESSION') - ImportPattern = '^\.\s+\(\s*Join-Path\s+\$PWD\s+[''"]scripts[/\\]signing-trust\.ps1[''"]\s*\)$' - ExpectedPath = '$Path' - PinPattern = 'Get-CertificateSpkiSha256.*?\$allowedPins -notcontains \$pin' - } -) -$signingTrustSources = @( - [pscustomobject]@{ Context = $signingTrust; Content = $signingTrustText; IsCanonical = $true } -) + @($signingTrustConsumers | ForEach-Object { - [pscustomobject]@{ - Context = $_.Context - Content = $_.Content - IsCanonical = $false - ImportPattern = $_.ImportPattern - ExpectedPath = $_.ExpectedPath - PinPattern = $_.PinPattern - } -}) -foreach ($source in $signingTrustSources) { - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput( - $source.Content, - [ref]$tokens, - [ref]$errors - ) - if ($errors.Count -ne 0) { - throw "Signing-trust contract source does not parse: $($source.Context) ($($errors[0].Message))" - } - $definitions = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - ($node.Name -replace '^(?i)(?:global|script|local|private):', '') -eq 'Test-SelfSignedTrustStatus' - }, $true)) - if ($source.IsCanonical) { - if ($definitions.Count -ne 1 -or - -not [object]::ReferenceEquals($definitions[0].Parent, $ast.EndBlock)) { - throw 'scripts/signing-trust.ps1 must own exactly one top-level Test-SelfSignedTrustStatus definition.' - } - $trustStatusBody = $definitions[0].Body.Extent.Text - if ($trustStatusBody -match 'StatusMessage' -or - $trustStatusBody -notmatch 'SignatureStatus\]::UnknownError' -or - $trustStatusBody -notmatch 'VerifyEmbeddedSignatureAndFileHash' -or - $trustStatusBody -notmatch '\[string\]\s+\$Path') { - throw 'UnknownError self-signed trust must require a file path and cryptographic verification, never localized status text.' - } - continue - } - $imports = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.InvocationOperator -eq [System.Management.Automation.Language.TokenKind]::Dot -and - $node.Extent.Text.Trim() -match $source.ImportPattern -and - [object]::ReferenceEquals($node.Parent.Parent, $ast.EndBlock) - }, $true)) - $trustStatusCalls = @($ast.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Test-SelfSignedTrustStatus' - }, $true)) - $expectedPathPattern = [regex]::Escape($source.ExpectedPath) - $consumerFunction = if ($trustStatusCalls.Count -eq 1) { $trustStatusCalls[0].Parent } else { $null } - while ($null -ne $consumerFunction -and - $consumerFunction -isnot [System.Management.Automation.Language.FunctionDefinitionAst]) { - $consumerFunction = $consumerFunction.Parent - } - $authenticodeCalls = if ($null -ne $consumerFunction) { - @($consumerFunction.Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Get-AuthenticodeSignature' - }, $true)) - } - else { @() } - $pinEvidence = if ($null -ne $consumerFunction) { - @($consumerFunction.Body.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -or - $node -is [System.Management.Automation.Language.BinaryExpressionAst] - }, $true) | ForEach-Object { $_.Extent.Text }) -join "`n" - } - else { '' } - if ($definitions.Count -ne 0 -or $imports.Count -ne 1 -or - $trustStatusCalls.Count -ne 1 -or - $null -eq $consumerFunction -or - $trustStatusCalls[0].Extent.Text -notmatch "(?s)-Path\s+$expectedPathPattern(?:\s|\)|$)" -or - $authenticodeCalls.Count -ne 1 -or - $authenticodeCalls[0].Extent.Text -notmatch "(?s)-LiteralPath\s+$expectedPathPattern(?:\s|\)|$)" -or - $pinEvidence -notmatch "(?s)$($source.PinPattern)") { - throw "Authenticode self-signed trust classification must be imported once, not redefined: $($source.Context)" - } -} -$signingTestTokens = $null -$signingTestErrors = $null -$signingTestAst = [System.Management.Automation.Language.Parser]::ParseInput( - $signingTrustTestText, - [ref]$signingTestTokens, - [ref]$signingTestErrors -) -$signingTestInitializers = @($signingTestAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Initialize-NocttyAuthenticodeVerifier' -}, $true)) -$firstDirectVerifierIndex = $signingTrustTestText.IndexOf( - '[NocttyAuthenticodeVerifier]::VerifyEmbeddedSignatureAndFileHash($signedPath)' -) -if ($signingTestErrors.Count -ne 0 -or - $signingTestInitializers.Count -ne 1 -or - $firstDirectVerifierIndex -lt 0 -or - $signingTestInitializers[0].Extent.StartOffset -ge $firstDirectVerifierIndex) { - throw 'Signing policy tests must explicitly initialize the direct Authenticode verifier before its first assertion.' -} -foreach ($contract in @( - @{ Pattern = '(?s)SignedCms\s+signedCms\s*=.*?signedCms\.CheckSignature\(true\)'; Description = 'self-signed verifier validates the embedded PKCS#7 signature without machine trust' }, - @{ Pattern = '(?s)dwProvFlags\s*=\s*WtdRevocationCheckNone\s*\|\s*WtdHashOnlyFlag\s*\|.*?WinVerifyTrust.*?==\s*0'; Description = 'self-signed verifier binds the signed digest to the actual PE bytes' }, - @{ Pattern = 'CryptQueryObject'; Description = 'self-signed verifier reads the embedded Authenticode message from the target file' }, - @{ Pattern = '(?s)out IntPtr phCertStore,\s*out IntPtr phMsg,\s*IntPtr ppvContext\);.*?CertQueryContentFlagPkcs7SignedEmbed,.*?out certificateStore,\s*out message,\s*IntPtr\.Zero\)'; Description = 'embedded PKCS#7 query declines the content-type-specific ppvContext output' }, - @{ Pattern = '(?s)StructureToPtr.*?fileInfoMarshalled = true.*?DestroyStructure\(fileInfoPointer, typeof\(WintrustFileInfo\)\).*?FreeCoTaskMem\(fileInfoPointer\)'; Description = 'self-signed verifier releases nested path marshalling before its outer buffer' } -)) { - Assert-TextContract ` - -Content $signingTrustText ` - -Pattern $contract.Pattern ` - -Description $contract.Description ` - -Context $signingTrust -} -foreach ($contract in @( - @{ Pattern = '(?s)VerifyEmbeddedSignatureAndFileHash\(\$signedPath\).*?Direct Authenticode verifier rejected an intact signed PE'; Description = 'signing policy test directly accepts an intact signed PE' }, - @{ Pattern = '(?s)VerifyEmbeddedSignatureAndFileHash\(\$bodyTamperedPath\).*?Direct Authenticode verifier accepted a PE with a modified signed body'; Description = 'signing policy test directly rejects modified signed PE bytes' }, - @{ Pattern = '(?s)VerifyEmbeddedSignatureAndFileHash\(\$signatureTamperedPath\).*?Direct Authenticode verifier accepted a PE with a modified PKCS#7 signature'; Description = 'signing policy test directly rejects modified embedded signature bytes' }, - @{ Pattern = 'Set-AuthenticodeSignature'; Description = 'signing policy test exercises a real Authenticode-signed PE' } -)) { - Assert-TextContract ` - -Content $signingTrustTestText ` - -Pattern $contract.Pattern ` - -Description $contract.Description ` - -Context $signingTrustTest -} -foreach ($contract in @( - @{ Pattern = '\$expectedNames\.Count -ne 8'; Description = 'published verifier requires the exact eight-asset set' }, - @{ Pattern = '(?s)\$missing = .*?\$unexpected = .*?\$missing\.Count -gt 0 -or \$unexpected\.Count -gt 0.*?asset set mismatch'; Description = 'published verifier rejects missing and unexpected assets' }, - @{ Pattern = '(?s)\$digest -notmatch.*?\$actualHash -ne \$digest\.Substring\(7\)\.ToLowerInvariant\(\).*?digest mismatch'; Description = 'published verifier compares downloaded bytes with GitHub SHA-256 digests' }, - @{ Pattern = 'SequenceEqual'; Description = 'published verifier preserves byte-identical legacy x64 checksum alias' }, - @{ Pattern = '(?s)\$checksums\.Count -ne \$expectedChecksumNames\.Count.*?\$checksums\.Contains\(\$_\).*?\$checksums\[\$name\] -ne \$actualHash'; Description = 'published verifier enforces exact checksum names, count, and hashes' }, - @{ Pattern = '(?s)\$signatureEvidence\.Add\(\(Assert-PublishedSignature.*?Setup \$architecture.*?foreach \(\$relativePath.*?\$signatureEvidence\.Add\(\(Assert-PublishedSignature.*?\$signatureEvidence\.Count -ne 8'; Description = 'published verifier validates exactly eight downloaded Authenticode signatures' }, - @{ Pattern = '(?s)Get-CertificateSpkiSha256.*?\$AllowedPins -notcontains \$pin.*?\$thumbprints\.Count -ne 1 -or \$pins\.Count -ne 1'; Description = 'published verifier binds every downloaded signer to one updater SPKI' }, - @{ Pattern = "noctty/noctty\.com'.*?noctty/noctty\.exe'.*?noctty/ghostty-vt\.dll'"; Description = 'published verifier checks every packaged runtime PE for both architectures' } - @{ Pattern = '(?s)finally \{.*?\$createdTempDirectory.*?\$DownloadDirectory\.StartsWith\(\$tempRoot.*?for \(\$attempt = 1; \$attempt -le 3; \$attempt\+\+\).*?Remove-Item .*?-ErrorAction Stop.*?Write-Warning'; Description = 'published verifier guards, retries, and reports temporary cleanup' } -)) { - Assert-TextContract ` - -Content $publishedReleaseVerifierText ` - -Pattern $contract.Pattern ` - -Description $contract.Description ` - -Context $publishedReleaseVerifier -} -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $testWorkflowText -Name 'Remote release copy checks' -Source $testWorkflow) ` - -Pattern '(?ms)env:\s+GH_TOKEN: \$\{\{ github\.token \}\}.*?CheckRemoteLatest' ` - -Description 'scheduled remote verification authenticates gh' ` - -Context "$testWorkflow :: Remote release copy checks" -Assert-TextContract ` - -Content (Get-YamlStepBlock ` - -Content (Get-YamlJobText -Content $testWorkflowText -Name 'windows' -Source $testWorkflow) ` - -Name 'Setup Zig' ` - -Source "$testWorkflow :: windows") ` - -Pattern '(?ms)with:\s+version: 0\.15\.2\s+.*?use-cache: false' ` - -Description 'hosted Windows tests cannot restore failed Zig build caches' ` - -Context "$testWorkflow :: windows :: Setup Zig" -Assert-TextContract ` - -Content (Get-YamlStepBlock ` - -Content (Get-YamlJobText -Content $testWorkflowText -Name 'windows-portable-smoke' -Source $testWorkflow) ` - -Name 'Setup Zig' ` - -Source "$testWorkflow :: windows-portable-smoke") ` - -Pattern '(?ms)with:\s+version: 0\.15\.2\s+.*?use-cache: false' ` - -Description 'portable smoke cannot restore failed Zig build caches' ` - -Context "$testWorkflow :: windows-portable-smoke :: Setup Zig" -Assert-TextContract ` - -Content (Get-YamlStepBlock ` - -Content (Get-YamlJobText -Content $releaseWorkflowText -Name 'windows-release' -Source $releaseWorkflow) ` - -Name 'Setup Zig' ` - -Source "$releaseWorkflow :: windows-release") ` - -Pattern '(?ms)with:\s+version: 0\.15\.2\s+.*?use-cache: false' ` - -Description 'release builds cannot restore failed Zig build caches' ` - -Context "$releaseWorkflow :: windows-release :: Setup Zig" -Assert-TextContract ` - -Content (Get-YamlStepBlock ` - -Content (Get-YamlJobText -Content $testWorkflowText -Name 'windows-interactive' -Source $testWorkflow) ` - -Name 'Setup Zig' ` - -Source "$testWorkflow :: windows-interactive") ` - -Pattern '(?ms)with:\s+version: 0\.15\.2\s+.*?use-cache: false' ` - -Description 'ephemeral interactive retries cannot restore failed Zig build caches' ` - -Context "$testWorkflow :: windows-interactive :: Setup Zig" -Assert-TextContract ` - -Content (Get-YamlJobText -Content $testWorkflowText -Name 'windows-interactive' -Source $testWorkflow) ` - -Pattern '(?m)^\s*timeout-minutes:\s+60\s*$' ` - -Description 'full interactive validation has enough job budget for the accessibility soak' ` - -Context "$testWorkflow :: windows-interactive" -$interactiveRunStep = Get-YamlStepBlock ` - -Content (Get-YamlJobText -Content $testWorkflowText -Name 'windows-interactive' -Source $testWorkflow) ` - -Name 'Run interactive Win11 composite' ` - -Source "$testWorkflow :: windows-interactive" -Assert-TextContract ` - -Content $interactiveRunStep ` - -Pattern '(?ms)env:\s+ZIG_GLOBAL_CACHE_DIR: \$\{\{ runner\.temp \}\}\\zig-global-cache\s+ZIG_LOCAL_CACHE_DIR: \$\{\{ runner\.temp \}\}\\zig-local-cache' ` - -Description 'interactive builds use clean per-job Zig caches' ` - -Context "$testWorkflow :: windows-interactive :: Run interactive Win11 composite" -$interactiveRunScript = Get-YamlLiteralRunScript ` - -Content $interactiveRunStep ` - -Source "$testWorkflow :: windows-interactive :: Run interactive Win11 composite" -$expectedInteractiveRunScript = @' -$ErrorActionPreference = 'Stop' -$quick = '${{ github.event_name }}' -eq 'pull_request' -if ($quick) { - ./test/windows/interactive-win11-pr-smoke.ps1 -Rebuild -ResetState - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - ./test/windows/interactive-win11-shaders.ps1 -Rebuild -ResetState - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -} else { - ./test/windows/flagship/Invoke-InteractiveWin11.ps1 -Rebuild -ResetState -IncludeForegroundHarness - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - ./test/windows/interactive-win11-shaders.ps1 -Rebuild -ResetState - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - ./test/windows/interactive-win11-accessibility.ps1 -ResetState -TimeoutSeconds 120 -IdleSoakSeconds 600 - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - ./test/windows/interactive-win11-palette-theme.ps1 -ResetState -ExerciseHighContrast - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - ./test/windows/interactive-win11-session-restore.ps1 -ResetState - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -} -'@ -$expectedInteractiveRunScript = ($expectedInteractiveRunScript -replace '\r\n?', "`n").TrimEnd([char[]]"`n") -if ($interactiveRunScript -cne $expectedInteractiveRunScript) { - throw 'Interactive workflow run script drifted from its exact fail-closed source snapshot.' -} -Assert-WorkflowContract ` - -Path $shaderHarness ` - -Pattern 'zig build -Demit-exe=true -Dcustom-shaders=true' ` - -Description 'shader harness rebuilds the executable with custom shader support' -Assert-WorkflowContract ` - -Path $shaderHarness ` - -Pattern "(?s)custom shaders: enabled.*?R -ge 220.*?G -le 40.*?B -ge 220" ` - -Description 'shader harness verifies both the compiled capability and visible magenta output' -Assert-WorkflowContract ` - -Path $shaderHarness ` - -Pattern '(?ms)Show-StatefulHost \$hostHwnd\r?\n\s+\$graphics\.CopyFromScreen.*?Show-StatefulHost \$hostHwnd\r?\n\s+\$argb = Get-StatefulPixel' ` - -Description 'shader harness refocuses the host immediately before both screen-pixel captures' -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $testWorkflowText -Name 'Verify default source-build shader mode' -Source $testWorkflow) ` - -Pattern '(?ms)noctty\.com \+version.*?Build Config.*?custom shaders: disabled' ` - -Description 'default source build executes the CLI and reports custom shaders disabled' ` - -Context "$testWorkflow :: Verify default source-build shader mode" -Assert-WorkflowContract ` - -Path (Join-Path $repoRoot 'scripts\dev-windows.cmd') ` - -Pattern '(?s)if "%ZIG_GLOBAL_CACHE_DIR%"=="" set "ZIG_GLOBAL_CACHE_DIR=.*?if "%ZIG_LOCAL_CACHE_DIR%"=="" set "ZIG_LOCAL_CACHE_DIR=' ` - -Description 'Windows bootstrap preserves caller-provided Zig cache isolation' -Assert-WorkflowContract ` - -Path (Join-Path $repoRoot 'scripts\dev-windows.ps1') ` - -Pattern '(?s)IsNullOrWhiteSpace\(\$env:ZIG_GLOBAL_CACHE_DIR\).*?IsNullOrWhiteSpace\(\$env:ZIG_LOCAL_CACHE_DIR\)' ` - -Description 'PowerShell Windows bootstrap preserves caller-provided Zig cache isolation' -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $testWorkflowText -Name 'Upload interactive evidence' -Source $testWorkflow) ` - -Pattern '(?ms)include-hidden-files: true.*?github\.workspace.*?\.sandbox/win11/\*\*/logs/\*\*' ` - -Description 'interactive evidence upload includes the actual hidden sandbox log tree' ` - -Context "$testWorkflow :: Upload interactive evidence" -Assert-WorkflowContract ` - -Path $interactivePrSmoke ` - -Pattern "(?ms)if \(\`$harness -eq 'interactive-win11-undo\.ps1'\) \{\s*\`$harnessArgs \+= @\('-TimeoutSeconds', '35'\)\s*\}" ` - -Description 'interactive PR smoke preserves the documented undo harness timeout' -Assert-WorkflowContract ` - -Path $interactivePrSmoke ` - -Pattern "(?ms)for \(\`$attempt = 1; \`$attempt -le 2; \`$attempt\+\+\).*?\`$cacheHydrationMiss = \`$buildText -match 'FileNotFound' -and \`$buildText -match 'zig-global-cache'.*?retrying once with fresh temp cache directories" ` - -Description 'interactive PR smoke retries exactly the transient Zig cache hydration miss' -Assert-WorkflowContract ` - -Path $interactivePrSmoke ` - -Pattern "(?ms)\`$originalErrorActionPreference = \`$ErrorActionPreference\s*try \{\s*\`$ErrorActionPreference = 'Continue'\s*\`$buildOutput = @\(& \(Join-Path \`$repoRoot 'scripts\\dev-windows\.cmd'\) zig build -Demit-exe=true 2>&1\)\s*\`$buildExitCode = \`$LASTEXITCODE\s*\}\s*finally \{\s*\`$ErrorActionPreference = \`$originalErrorActionPreference\s*\}" ` - -Description 'interactive PR smoke captures native build stderr without bypassing exit-code retry logic on PowerShell 7.0' -foreach ($paletteActionHarness in @($newTabHarness, $undoHarness, $resizeHarness)) { - $tokens = $null - $errors = $null - $paletteActionAst = [System.Management.Automation.Language.Parser]::ParseFile( - $paletteActionHarness, - [ref] $tokens, - [ref] $errors - ) - if ($errors.Count -ne 0) { - throw "Palette action harness does not parse: $paletteActionHarness ($($errors[0].Message))" - } - $paletteActionFunctions = @($paletteActionAst.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq 'Invoke-CommandPaletteAction' - }, $true)) - if ($paletteActionFunctions.Count -ne 1) { - throw "Palette action harness must define exactly one Invoke-CommandPaletteAction helper: $paletteActionHarness" - } - $paletteActionBody = $paletteActionFunctions[0].Body - $paletteSubmitCalls = @($paletteActionBody.FindAll({ - param($node) - $node -is [System.Management.Automation.Language.CommandAst] -and - $node.GetCommandName() -eq 'Invoke-InteractiveWin11Message' -and - $node.Extent.Text -match '-Hwnd\s+\$edit\.Hwnd' -and - $node.Extent.Text -match '-Message\s+(?:\$wmChar|0x0102)' -and - $node.Extent.Text -match '-WParam\s+\(\[UIntPtr\]\(\[uint64\](?:\$vkReturn|0x0D)\)\)' -and - $node.Extent.Text -match '-Description\s+[''\"]palette WM_CHAR Enter[''\"]' - }, $true)) - if ($paletteSubmitCalls.Count -ne 1) { - throw "Palette action helper must submit exactly once through WM_CHAR Enter on its edit HWND: $paletteActionHarness" - } - if ($paletteActionBody.Extent.Text -match '(?i)paletteConfirmCommandId|\b2003\b') { - throw "Palette action helper cannot reference the hidden accept-button command ID: $paletteActionHarness" - } -} -Assert-WorkflowContract ` - -Path (Join-Path $repoRoot 'src\renderer\Thread.zig') ` - -Pattern '(?s)fn armCursorTimerIfDead\(.*?cursor_c\.state\(\) != \.dead.*?cursor_h\.run\(' ` - -Description 'cursor blink completion is rearmed only after libxev releases it' -Assert-WorkflowContractAbsent ` - -Path (Join-Path $repoRoot 'src\renderer\Thread.zig') ` - -Pattern '(?i)cursor_c_cancel|cursor_h\.(?:cancel|reset)\s*\(' ` - -Description 'cursor blink never cancels or resets an IOCP-owned completion' -Assert-WorkflowContract ` - -Path $interactivePrSmoke ` - -Pattern "(?ms)if \(\`$attempt -eq 2 -and \`$env:RUNNER_TEMP\).*?zig-global-cache-pr-smoke-retry-\`$PID.*?zig-local-cache-pr-smoke-retry-\`$PID" ` - -Description 'interactive PR smoke moves the retry to fresh runner-temp Zig cache directories' -Assert-WorkflowContract ` - -Path $interactivePrSmoke ` - -Pattern "(?ms)finally \{\s*\`$env:ZIG_GLOBAL_CACHE_DIR = \`$originalZigGlobalCache\s*\`$env:ZIG_LOCAL_CACHE_DIR = \`$originalZigLocalCache\s*\}" ` - -Description 'interactive PR smoke restores caller-provided Zig cache directories after rebuild retry' -Assert-WorkflowContract ` - -Path $accessibilityHarness ` - -Pattern '\[Math\]::Max\(90, \(\$TimeoutSeconds \* 3\) \+ \$IdleSoakSeconds \+ 60\)' ` - -Description 'accessibility harness budgets all three timeout-bearing launch phases plus idle soak' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '\[DateTimeOffset\]::TryParse\(' ` - -Description 'accessibility evidence timestamp is semantically validated' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '\$provenance\.runner_name -ne \$result\.environment\.runner_name' ` - -Description 'runner provenance is bound to the interactive result' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '\$provenance\.runner_name -ne \$interactiveJob\[0\]\.runner_name' ` - -Description 'runner provenance is bound to the GitHub job' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '\$provenanceRunAttempt -ne \[int\]\$run\.run_attempt' ` - -Description 'runner provenance is bound to the GitHub run attempt' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '\[string\]\$provenance\.user -match .*SYSTEM' ` - -Description 'service-account runner provenance is rejected' -Assert-WorkflowContract ` - -Path $runnerProvenanceChecker ` - -Pattern "(?m)^\`$minimumRunnerVersion = \[version\]'2\.327\.1'\s*$" ` - -Description 'interactive evidence enforces the upload-artifact runner floor' -Assert-WorkflowContractAbsent ` - -Path $runnerProvenanceChecker ` - -Pattern '(?m)^\s*\[version\]\s+\$MinimumRunnerVersion\b' ` - -Description 'interactive runner floor cannot be lowered by a parameter' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern "(?m)^\s*\`$minimumRunnerVersion = \[version\]'2\.327\.1'\s*$" ` - -Description 'accessibility evidence pins the upload-artifact runner floor' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '(?m)^#requires -Version 7\.1\s*$' ` - -Description 'accessibility evidence requires PowerShell 7.1 or newer' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '\$provenance = Get-Content -LiteralPath \$provenancePaths\[0\]\.FullName -Raw \| ConvertFrom-Json -NoEnumerate' ` - -Description 'accessibility evidence preserves the JSON root kind' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '\$provenance\.GetType\(\) -ne \[System\.Management\.Automation\.PSCustomObject\]' ` - -Description 'accessibility evidence requires runner provenance to be a JSON object' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern "schema_version -ne 'noctty\.interactive-runner-provenance\.v1'" ` - -Description 'accessibility evidence rejects unsupported runner provenance schemas' -Assert-WorkflowContract ` - -Path $runnerProvenanceChecker ` - -Pattern 'NOCTTY_EXPECTED_CHECKOUT_SHA' ` - -Description 'interactive provenance can verify an exact PR head checkout instead of only GITHUB_SHA' -Assert-WorkflowContract ` - -Path $runnerProvenanceChecker ` - -Pattern 'commit = \$expectedCommit' ` - -Description 'interactive provenance records the exact expected tested commit' -Assert-WorkflowContract ` - -Path $testWorkflow ` - -Pattern "ref: \\\$\\{\\{ github\\.event_name == 'pull_request' && github\\.event\\.pull_request\\.head\\.sha \\|\\| github\\.sha \\}\\}" ` - -Description 'Test workflow checkouts use the immutable PR head SHA for pull requests' -Assert-WorkflowContract ` - -Path (Join-Path $repoRoot '.github\workflows\windows-arm64.yml') ` - -Pattern "ref: \\\$\\{\\{ github\\.event_name == 'pull_request' && github\\.event\\.pull_request\\.head\\.sha \\|\\| github\\.sha \\}\\}" ` - -Description 'ARM64 workflow checkout uses the immutable PR head SHA for pull requests' -Assert-WorkflowContract ` - -Path (Join-Path $repoRoot '.github\workflows\windows-arm64.yml') ` - -Pattern '(?ms)Run ARM64 CLI smoke.*?Build Config.*?custom shaders: enabled.*?Run packaged ARM64 CLI smoke.*?Build Config.*?custom shaders: enabled' ` - -Description 'native and packaged ARM64 CLI smokes retain version output and shader capability coverage' -Assert-WorkflowContract ` - -Path $runnerProvenanceChecker ` - -Pattern '\$runnerVersion -lt \$minimumRunnerVersion' ` - -Description 'interactive evidence rejects outdated runners' -Assert-WorkflowContract ` - -Path $accessibilityChecker ` - -Pattern '\$provenanceRunnerVersion -lt \$minimumRunnerVersion' ` - -Description 'accessibility evidence rejects outdated interactive runners' -$objectRoot = ConvertFrom-Json -InputObject '{"value":1}' -NoEnumerate -$arrayRoot = ConvertFrom-Json -InputObject '[{"value":1}]' -NoEnumerate -if ($objectRoot.GetType() -ne [System.Management.Automation.PSCustomObject] -or - $arrayRoot.GetType() -eq [System.Management.Automation.PSCustomObject]) { - throw 'PowerShell JSON root-kind preservation does not satisfy the evidence contract.' -} -Assert-WorkflowContract ` - -Path $releaseCopyChecker ` - -Pattern '\$global:LASTEXITCODE\s*=\s*0\s*$' ` - -Description 'release-copy success explicitly clears native exit state' -Assert-WorkflowContract ` - -Path $releasePreflight ` - -Pattern '\$minimumValidityDays -lt 180(?!\d)' ` - -Description 'signer-validity overrides cannot lower the 180-day floor' -Assert-WorkflowContract ` - -Path $windowsPackager ` - -Pattern '(?ms)& zig build .*?\r?\n\s+if \(\$LASTEXITCODE -ne 0\) \{\s*throw "Zig build failed with exit code \$LASTEXITCODE\."' ` - -Description 'Windows packaging fails closed when its native Zig build fails' -Assert-WorkflowContract ` - -Path $windowsPackager ` - -Pattern '(?ms)foreach \(\$runtimeFile in \$runtimeFiles\).*?Assert-PeMachine.*?if \(\$Architecture -eq "x64"\).*?check-windows-x64-baseline\.ps1.*?-Path \$runtimePath' ` - -Description 'Windows packaging checks every x64 runtime PE for baseline compatibility' -Assert-WorkflowContract ` - -Path $windowsPackager ` - -Pattern '(?ms)Assert-WindowsBuildCapabilitiesManifest.*?Packaging arch.*?\$hostArchitecture -eq \$Architecture.*?noctty\.com.*?\+version.*?custom shaders: enabled.*?hash-bound \$Architecture build manifest' ` - -Description 'Windows packaging verifies hash-bound shader capability for every target and executes native packages' -Assert-WorkflowContract ` - -Path $windowsBuildCapabilities ` - -Pattern '(?ms)Get-FileHash.*?custom_shaders = \$true.*?custom_shaders -ne \$true.*?Get-FileHash.*?\$actualHash -cne \[string\] \$hashProperty\.Value' ` - -Description 'build capability manifests bind shader support to every runtime artifact hash' -Assert-WorkflowContract ` - -Path (Join-Path $repoRoot 'scripts\check-windows-x64-baseline.ps1') ` - -Pattern '(?ms)Get-Command llvm-objdump\.exe.*?\$objdumpTimeoutMs = 120000.*?\$objdumpKillTimeoutMs = 5000.*?\$streamCopyTimeoutMs = 30000.*?WaitForExit\(\$objdumpTimeoutMs\).*?\$objdumpProcess\.Kill\(\).*?WaitForExit\(\$objdumpKillTimeoutMs\).*?llvm-objdump did not exit after termination.*?WaitAll\(.*?\$streamCopyTimeoutMs.*?llvm-objdump stream cleanup timed out' ` - -Description 'Windows x64 baseline disassembly is time-bounded and kills a timed-out tool' -Assert-TextContract ` - -Content (Get-PowerShellBlockText -Content (Get-Content -LiteralPath $releasePreflight -Raw) -HeaderPattern '^function\s+Assert-WingetArchitectureCoverage(?=\s|\{)') ` - -Pattern '(?ms)Assert-WingetArchitectureCoverage.*?Architecture:.*?arm64,x64' ` - -Description 'stable preflight requires public WinGet x64 and arm64 bootstrap' ` - -Context "$releasePreflight :: Assert-WingetArchitectureCoverage" -Assert-TextContract ` - -Content (Get-PowerShellBlockText -Content (Get-Content -LiteralPath $releasePreflight -Raw) -HeaderPattern '^if \(\$RequirePackageManagers\)') ` - -Pattern '(?ms)Assert-WingetArchitectureCoverage\s+`\r?\n\s+-ManifestPath' ` - -Description 'package-manager preflight invokes the WinGet architecture gate' ` - -Context "$releasePreflight :: RequirePackageManagers" - -foreach ($siteScript in @( - $sitePayloadBuilder, - $siteHeaderContract, - $siteDeploymentHeadGate, - $cloudflarePagesVerifier -)) { - $siteScriptTokens = $null - $siteScriptErrors = $null - [void][Management.Automation.Language.Parser]::ParseFile( - $siteScript, - [ref]$siteScriptTokens, - [ref]$siteScriptErrors - ) - if ($siteScriptErrors.Count -ne 0) { - throw "Site deployment script does not parse: $siteScript ($($siteScriptErrors[0].Message))" - } - Assert-TextContract ` - -Content (Get-Content -LiteralPath $siteScript -Raw) ` - -Pattern '(?m)^#requires -Version 7\.3\s*$' ` - -Description 'workflow-owned site deployment scripts declare their PowerShell 7.3 floor' ` - -Context $siteScript -} - -Assert-TextContract ` - -Content $siteDeployWorkflowText ` - -Pattern '(?ms)^on:\s+push:\s+branches:\s+- main\s+release:\s+types:\s+- published\s+workflow_dispatch:\s*$' ` - -Description 'site deployment catches every main advance plus published releases and manual dispatch' ` - -Context $siteDeployWorkflow -Assert-WorkflowContractAbsent ` - -Path $siteDeployWorkflow ` - -Pattern '(?m)^\s+paths(?:-ignore)?:' ` - -Description 'site deployment cannot strand a site change behind a later path-filtered main advance' -Assert-WorkflowContractAbsent ` - -Path $siteDeployWorkflow ` - -Pattern '(?m)^\s*(?:pull_request|pull_request_target):' ` - -Description 'site deployment never runs for pull requests' -Assert-TextContract ` - -Content $siteDeployWorkflowText ` - -Pattern '(?ms)^permissions:\s+contents: read\s+deployments: write\s+.*?^concurrency:\s+group: cloudflare-pages-production\s+cancel-in-progress: false\s*$' ` - -Description 'site deployment uses minimum repository permissions and serialized non-canceling production concurrency' ` - -Context $siteDeployWorkflow -Assert-TextContract ` - -Content (Get-YamlJobText -Content $siteDeployWorkflowText -Name 'deploy' -Source $siteDeployWorkflow) ` - -Pattern '(?m)^\s+environment: cloudflare-pages-production\s*$' ` - -Description 'site deployment is protected by the production environment' ` - -Context "$siteDeployWorkflow :: deploy" -Assert-TextContract ` - -Content (Get-YamlJobText -Content $siteDeployWorkflowText -Name 'deploy' -Source $siteDeployWorkflow) ` - -Pattern "(?m)^\s+if: github\.event_name != 'release' \|\| github\.event\.release\.prerelease == false\s*$" ` - -Description 'stable site publication ignores prerelease events' ` - -Context "$siteDeployWorkflow :: deploy" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $siteDeployWorkflowText -Name 'Checkout exact event commit' -Source $siteDeployWorkflow) ` - -Pattern '(?ms)uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd.*?ref: \$\{\{ github\.event_name == ''release'' && github\.event\.repository\.default_branch \|\| github\.sha \}\}.*?persist-credentials: false' ` - -Description 'site checkout uses exact event commits for pushes and current main for release publication' ` - -Context "$siteDeployWorkflow :: checkout" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $siteDeployWorkflowText -Name 'Resolve exact deployment commit' -Source $siteDeployWorkflow) ` - -Pattern '(?ms)id: source.*?\$sha = git rev-parse HEAD.*?\$LASTEXITCODE -ne 0.*?\$sha = \(\[string\]\(\$sha -join "`n"\)\)\.Trim\(\).*?\^\[0-9a-f\]\{40\}\$.*?"sha=\$sha" >> \$env:GITHUB_OUTPUT' ` - -Description 'site deployment fails closed before recording the full resolved source SHA' ` - -Context "$siteDeployWorkflow :: source" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $siteDeployWorkflowText -Name 'Validate committed site bundle and copy' -Source $siteDeployWorkflow) ` - -Pattern '(?ms)GH_TOKEN:.*?github\.token.*?RELEASE_TAG:.*?github\.event\.release\.tag_name.*?check-site-copy\.ps1.*?node scripts/build-site-assets\.mjs --check.*?get-site-header-contract\.ps1.*?GITHUB_EVENT_NAME -eq ''release''.*?RELEASE_TAG -cnotmatch ''\^v\(\?\\d\+\\\.\\d\+\\\.\\d\+\)\$''.*?check-release-copy\.ps1.*?-ExpectedVersion \$Matches\.version.*?-CheckRemoteLatest.*?else \{.*?check-release-copy\.ps1 -CheckRemoteLatest.*?LASTEXITCODE' ` - -Description 'site copy is checked before the deterministic asset gate and release copy binds to the published semver tag' ` - -Context "$siteDeployWorkflow :: site validation" -$siteActionUses = [regex]::Matches( - $siteDeployWorkflowText, - '(?m)^\s*uses:\s+(?[^\s@]+)@(?[^\s#]+)' -) -if ($siteActionUses.Count -ne 4 -or - @($siteActionUses | Where-Object { - $_.Groups['ref'].Value -cnotmatch '^[0-9a-f]{40}$' - }).Count -ne 0) { - throw 'Every site deployment action must use a full immutable commit SHA.' -} -$wranglerUses = @($siteActionUses | Where-Object { - $_.Groups['action'].Value -ceq 'cloudflare/wrangler-action' -}) -if ($wranglerUses.Count -ne 2 -or - @($wranglerUses | Where-Object { - $_.Groups['ref'].Value -cne 'ebbaa1584979971c8614a24965b4405ff95890e0' - }).Count -ne 0) { - throw 'Canary and production must use the pinned official Wrangler action v4 commit.' -} -foreach ($deployStepName in @( - 'Deploy exact payload to canary', - 'Deploy identical payload to production' -)) { - $deployStep = Get-YamlStepBlock ` - -Content $siteDeployWorkflowText ` - -Name $deployStepName ` - -Source $siteDeployWorkflow - Assert-TextContract ` - -Content $deployStep ` - -Pattern '(?ms)wranglerVersion: 4\.114\.0.*?workingDirectory: \$\{\{ steps\.payload\.outputs\.wrangler_directory \}\}.*?pages deploy "\$\{\{ steps\.payload\.outputs\.directory \}\}".*?--project-name=noctty.*?--commit-hash="\$\{\{ steps\.source\.outputs\.sha \}\}".*?--commit-dirty=false' ` - -Description 'isolated Wrangler deploys the same clean exact-commit payload with the required version and visible diagnostics' ` - -Context "$siteDeployWorkflow :: $deployStepName" - if ($deployStep -match '(?m)^\s*quiet:') { - throw "Cloudflare deployment diagnostics must remain visible ($siteDeployWorkflow :: $deployStepName)" - } -} -foreach ($phase in @('canary', 'production')) { - Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $siteDeployWorkflowText -Name "Require exact origin main before $phase" -Source $siteDeployWorkflow) ` - -Pattern "(?ms)require-site-deployment-head\.ps1.*?-ExpectedSha \`$env:DEPLOY_SHA.*?-DefaultBranch \`$env:DEFAULT_BRANCH.*?-Phase $phase" ` - -Description "site deployment invokes the shared exact-main gate before $phase" ` - -Context "$siteDeployWorkflow :: $phase head gate" -} -Assert-TextContract ` - -Content (Get-Content -LiteralPath $siteDeploymentHeadGate -Raw) ` - -Pattern '(?ms)GITHUB_REPOSITORY -cne ''amanthanvi/noctty''.*?git remote get-url origin.*?git fetch --force --no-tags origin.*?git rev-parse HEAD.*?refs/remotes/origin/\$DefaultBranch.*?\$head -cne \$ExpectedSha.*?git status --porcelain=v1 --untracked-files=all' ` - -Description 'the shared site gate binds both phases to a clean exact fork-local main head' ` - -Context $siteDeploymentHeadGate -Assert-TextContract ` - -Content (Get-Content -LiteralPath $siteDeploymentHeadGate -Raw) ` - -Pattern '(?ms)\$originOutput = git remote get-url origin.*?\$LASTEXITCODE -ne 0.*?\$headOutput = git rev-parse HEAD.*?\$LASTEXITCODE -ne 0.*?\$originHeadOutput = git rev-parse.*?\$LASTEXITCODE -ne 0.*?\$status = @\(git status.*?\$LASTEXITCODE -ne 0.*?\$status\.Count -ne 0' ` - -Description 'every deployment gate git query checks its own exit status before consuming output' ` - -Context $siteDeploymentHeadGate -$sitePayloadStep = Get-YamlStepBlock ` - -Content $siteDeployWorkflowText ` - -Name 'Build deterministic deploy payload twice' ` - -Source $siteDeployWorkflow -Assert-TextContract ` - -Content $sitePayloadStep ` - -Pattern '(?ms)build-site-payload\.ps1.*?build-site-payload\.ps1.*?SequenceEqual\[byte\].*?different sorted SHA-256 manifests' ` - -Description 'site payload is built twice and compared by its sorted SHA-256 manifest' ` - -Context "$siteDeployWorkflow :: payload" -Assert-TextContract ` - -Content $sitePayloadStep ` - -Pattern '(?ms)noctty-wrangler-.*?New-Item -ItemType Directory -Path \$wrangler.*?wrangler_directory=' ` - -Description 'Wrangler installs in a runner-temporary directory outside the checkout' ` - -Context "$siteDeployWorkflow :: payload" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $siteDeployWorkflowText -Name 'Verify canary provenance and bytes' -Source $siteDeployWorkflow) ` - -Pattern '(?ms)DEPLOY_SHA: \$\{\{ steps\.source\.outputs\.sha \}\}.*?-ExpectedEnvironment preview.*?-ExpectedBranch \$env:CANARY_BRANCH.*?-ExpectedCommit \$env:DEPLOY_SHA.*?-ManifestPath \$env:PAYLOAD_MANIFEST' ` - -Description 'canary verification binds API provenance and payload bytes to the exact commit' ` - -Context "$siteDeployWorkflow :: canary verification" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $siteDeployWorkflowText -Name 'Require the zone-owned www redirect before production' -Source $siteDeployWorkflow) ` - -Pattern '(?ms)DEPLOY_SHA: \$\{\{ steps\.source\.outputs\.sha \}\}.*?-Mode Redirect.*?-DeploymentId \$env:DEPLOYMENT_ID.*?-ExpectedCommit \$env:DEPLOY_SHA' ` - -Description 'the zone-owned www redirect is verified before the production write' ` - -Context "$siteDeployWorkflow :: redirect preflight" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $siteDeployWorkflowText -Name 'Verify production provenance, domain, and bytes' -Source $siteDeployWorkflow) ` - -Pattern '(?ms)DEPLOY_SHA: \$\{\{ steps\.source\.outputs\.sha \}\}.*?-ExpectedEnvironment production.*?-ExpectedBranch main.*?-ExpectedCommit \$env:DEPLOY_SHA.*?-CanonicalBaseUrl ''https://noctty\.com/''.*?-RequireCanonical.*?-VerifyWwwRedirect' ` - -Description 'production verification binds the canonical domain and zone redirect' ` - -Context "$siteDeployWorkflow :: production verification" -Assert-TextContract ` - -Content (Get-YamlStepBlock -Content $siteDeployWorkflowText -Name 'Resolve canary branch' -Source $siteDeployWorkflow) ` - -Pattern '(?ms)DEPLOY_SHA: \$\{\{ steps\.source\.outputs\.sha \}\}.*?\$sha = \$env:DEPLOY_SHA' ` - -Description 'canary metadata receives the validated SHA through the environment' ` - -Context "$siteDeployWorkflow :: canary metadata" -Assert-WorkflowContractAbsent ` - -Path $siteDeployWorkflow ` - -Pattern '(?m)^\s+(?:\$sha\s*=\s*''\$\{\{ steps\.source\.outputs\.sha \}\}''|-ExpectedCommit\s+''\$\{\{ steps\.source\.outputs\.sha \}\}'')' ` - -Description 'validated action outputs are never interpolated directly into PowerShell source' -Assert-WorkflowContractAbsent ` - -Path $siteDeployWorkflow ` - -Pattern '(?i)rollback|previous\.outputs\.deployment_id' ` - -Description 'production verification cannot race an automatic Pages rollback' -Assert-WorkflowContractAbsent ` - -Path $cloudflarePagesVerifier ` - -Pattern '(?i)/rollback|Mode Rollback|RollbackDeploymentId' ` - -Description 'the verifier exposes no non-atomic automatic rollback path' -Assert-TextContract ` - -Content $site404Text ` - -Pattern '(?ms)href="/assets/favicon\.svg".*?href="/styles\.css\?v=[0-9a-f]{64}".*?src="/app\.js\?v=[0-9a-f]{64}"' ` - -Description 'the nested 404 fallback resolves all local assets from the site root' ` - -Context $site404 -Assert-TextContract ` - -Content $cloudflarePagesVerifierText ` - -Pattern '"/__noctty_missing_\$\(\$Commit\.Substring\(0, 12\)\)/nested/page"' ` - -Description 'deployment verification exercises the 404 fallback below a multi-segment path' ` - -Context $cloudflarePagesVerifier -Assert-TextContract ` - -Content $siteIndexText ` - -Pattern '(?ms)property="og:image" content="https://noctty\.com/assets/noctty-social\.jpg".*?property="og:image:type" content="image/jpeg".*?property="og:image:width" content="1200".*?property="og:image:height" content="630".*?name="twitter:card" content="summary_large_image".*?name="twitter:image" content="https://noctty\.com/assets/noctty-social\.jpg"' ` - -Description 'social previews use a current raster large-image card with explicit dimensions' ` - -Context $siteIndex -Assert-TextContract ` - -Content $sitePayloadBuilderText ` - -Pattern "'assets/noctty-social\.jpg'" ` - -Description 'the deterministic Cloudflare payload includes the social preview image' ` - -Context $sitePayloadBuilder -Assert-TextContract ` - -Content $siteGitattributesText ` - -Pattern '(?ms)^site/\*\.html text eol=lf\r?$.*?^site/\*\.css text eol=lf\r?$.*?^site/\*\.js text eol=lf\r?$.*?^site/_headers text eol=lf\r?$.*?^site/assets/\*\.svg text eol=lf\r?$.*?^site/tests/\*\.mjs text eol=lf\r?$.*?^scripts/build-site-assets\.mjs text eol=lf\r?$' ` - -Description 'every text file in the site payload and its source graph has a deterministic LF checkout rule' ` - -Context $siteGitattributes -Assert-TextContract ` - -Content $siteAssetBuilderText ` - -Pattern '(?ms)const checkOnly = process\.argv\.includes\("--check"\).*?if \(/\\r/\.test\(text\)\) \{.*?must be LF-normalized' ` - -Description 'the asset builder has a --check mode and rejects CR bytes so hashes bind to clean LF checkouts' ` - -Context $siteAssetBuilder -Assert-TextContract ` - -Content $siteAssetBuilderText ` - -Pattern '(?ms)const inlineScriptPattern = /