rust-switcher 1.0.13

Windows keyboard layout switcher and text conversion utility
Documentation
param(
    [Parameter(Mandatory = $true)]
    [string]$SharedRoot,

    [switch]$KeepOpen
)

$ErrorActionPreference = "Stop"

$logPath = Join-Path $SharedRoot "e2e.log"
$resultPath = Join-Path $SharedRoot "result.json"
$switcher = $null

function Write-Result {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Status,

        [string]$ErrorMessage,

        [hashtable]$Details = @{}
    )

    [ordered]@{
        status = $Status
        error = $ErrorMessage
        details = $Details
        timestampUtc = (Get-Date).ToUniversalTime().ToString("o")
    } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $resultPath -Encoding UTF8
}

function Add-NativeInputTypes {
    Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

public static class NativeInput {
    [StructLayout(LayoutKind.Sequential)]
    public struct INPUT {
        public uint type;
        public INPUTUNION u;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct INPUTUNION {
        [FieldOffset(0)]
        public KEYBDINPUT ki;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct KEYBDINPUT {
        public ushort wVk;
        public ushort wScan;
        public uint dwFlags;
        public uint time;
        public IntPtr dwExtraInfo;
    }

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

    [DllImport("user32.dll")]
    public static extern bool SetForegroundWindow(IntPtr hWnd);

    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    public const uint INPUT_KEYBOARD = 1;
    public const uint KEYEVENTF_KEYUP = 0x0002;
    public const ushort VK_LSHIFT = 0xA0;
    public const int SW_SHOWNORMAL = 1;

    public static void TapLeftShift() {
        INPUT[] inputs = new INPUT[2];
        inputs[0].type = INPUT_KEYBOARD;
        inputs[0].u.ki.wVk = VK_LSHIFT;
        inputs[1].type = INPUT_KEYBOARD;
        inputs[1].u.ki.wVk = VK_LSHIFT;
        inputs[1].u.ki.dwFlags = KEYEVENTF_KEYUP;
        uint sent = SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(INPUT)));
        if (sent != inputs.Length) {
            throw new InvalidOperationException("SendInput failed");
        }
    }
}
"@
}

function Wait-Until {
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]$Predicate,

        [int]$TimeoutMilliseconds = 5000,
        [int]$StepMilliseconds = 50
    )

    $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMilliseconds)
    while ([DateTime]::UtcNow -lt $deadline) {
        if (& $Predicate) {
            return $true
        }
        Start-Sleep -Milliseconds $StepMilliseconds
    }
    return $false
}

function Get-PlaygroundElement {
    Add-Type -AssemblyName UIAutomationClient
    Add-Type -AssemblyName UIAutomationTypes

    $root = [System.Windows.Automation.AutomationElement]::RootElement
    $windowCondition = New-Object System.Windows.Automation.PropertyCondition(
        [System.Windows.Automation.AutomationElement]::NameProperty,
        "RustSwitcher"
    )
    $window = $root.FindFirst(
        [System.Windows.Automation.TreeScope]::Children,
        $windowCondition
    )
    if ($null -eq $window) {
        return $null
    }

    $editCondition = New-Object System.Windows.Automation.PropertyCondition(
        [System.Windows.Automation.AutomationElement]::ControlTypeProperty,
        [System.Windows.Automation.ControlType]::Edit
    )
    $edits = $window.FindAll(
        [System.Windows.Automation.TreeScope]::Descendants,
        $editCondition
    )

    foreach ($edit in $edits) {
        $patternObj = $null
        if (-not $edit.TryGetCurrentPattern(
            [System.Windows.Automation.ValuePattern]::Pattern,
            [ref]$patternObj
        )) {
            continue
        }

        $pattern = [System.Windows.Automation.ValuePattern]$patternObj
        if (-not $pattern.Current.IsReadOnly -and $pattern.Current.Value -eq "") {
            return $edit
        }
    }

    return $null
}

function Get-ValuePattern {
    param([Parameter(Mandatory = $true)]$Element)

    $patternObj = $null
    if (-not $Element.TryGetCurrentPattern(
        [System.Windows.Automation.ValuePattern]::Pattern,
        [ref]$patternObj
    )) {
        throw "Focused playground does not support ValuePattern"
    }
    return [System.Windows.Automation.ValuePattern]$patternObj
}

function Tap-LeftShift {
    param([int]$Count)

    for ($i = 0; $i -lt $Count; $i++) {
        [NativeInput]::TapLeftShift()
        Start-Sleep -Milliseconds 120
    }
}

function Wait-For-PlaygroundValue {
    param(
        [Parameter(Mandatory = $true)]
        $Pattern,

        [Parameter(Mandatory = $true)]
        [string]$Expected,

        [int]$TimeoutMilliseconds = 5000
    )

    Wait-Until -TimeoutMilliseconds $TimeoutMilliseconds -Predicate {
        $Pattern.Current.Value -eq $Expected
    }
}

try {
    Start-Transcript -LiteralPath $logPath -Force | Out-Null

    Add-Type -AssemblyName System.Windows.Forms
    Add-NativeInputTypes

    $exe = Join-Path $SharedRoot "rust-switcher-debug.exe"
    if (-not (Test-Path $exe)) {
        throw "rust-switcher-debug.exe is missing from $SharedRoot"
    }

    $appData = "C:\rust-switcher-e2e-appdata"
    Remove-Item -LiteralPath $appData -Recurse -Force -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path (Join-Path $appData "RustSwitcherDebug") -Force | Out-Null

@"
start_minimized = false
theme_dark = false
smarter_hotkeys_enabled = true
"@ | Set-Content -LiteralPath (Join-Path $appData "RustSwitcherDebug\config.json") -Encoding UTF8

    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.FileName = $exe
    $psi.WorkingDirectory = $SharedRoot
    $psi.UseShellExecute = $false
    $psi.Environment["APPDATA"] = $appData
    $psi.Environment["RUST_LOG"] = "trace"
    $psi.Environment["RUST_SWITCHER_E2E_PLAYGROUND"] = "1"
    $psi.Environment["RUST_SWITCHER_E2E_ALLOW_INJECTED"] = "1"
    $switcher = [System.Diagnostics.Process]::Start($psi)

    if (-not (Wait-Until -TimeoutMilliseconds 10000 -Predicate {
        $switcher.Refresh()
        $switcher.MainWindowHandle -ne [IntPtr]::Zero
    })) {
        throw "RustSwitcher window did not appear"
    }

    [NativeInput]::ShowWindow($switcher.MainWindowHandle, [NativeInput]::SW_SHOWNORMAL) | Out-Null
    [NativeInput]::SetForegroundWindow($switcher.MainWindowHandle) | Out-Null
    Start-Sleep -Milliseconds 800

    $playground = Get-PlaygroundElement
    if ($null -eq $playground) {
        throw "Playground edit was not found"
    }
    $playground.SetFocus()
    Start-Sleep -Milliseconds 300
    $value = Get-ValuePattern -Element $playground
    $value.SetValue("")

    [System.Windows.Forms.SendKeys]::SendWait("ghbdtn")
    Start-Sleep -Milliseconds 500
    if (-not (Wait-For-PlaygroundValue -Pattern $value -Expected "ghbdtn" -TimeoutMilliseconds 3000)) {
        throw "Initial playground typing failed. Value='$($value.Current.Value)'"
    }

    Tap-LeftShift -Count 3
    if (-not (Wait-For-PlaygroundValue -Pattern $value -Expected "привет" -TimeoutMilliseconds 6000)) {
        throw "Triple Shift smart conversion failed. Value='$($value.Current.Value)'"
    }

    Tap-LeftShift -Count 2
    if (-not (Wait-For-PlaygroundValue -Pattern $value -Expected "ghbdtn" -TimeoutMilliseconds 6000)) {
        throw "Deferred double Shift conversion failed. Value='$($value.Current.Value)'"
    }

    Write-Result -Status "passed" -Details @{
        finalValue = $value.Current.Value
        appData = $appData
        scenario = "ghbdtn -> 3 Shift -> привет -> 2 Shift -> ghbdtn"
    }
} catch {
    Write-Result -Status "failed" -ErrorMessage $_.Exception.Message -Details @{
        stack = $_.ScriptStackTrace
    }
    throw
} finally {
    if ($null -ne $switcher -and -not $switcher.HasExited) {
        Stop-Process -Id $switcher.Id -Force -ErrorAction SilentlyContinue
    }
    try {
        Stop-Transcript | Out-Null
    } catch {
    }
    if (-not $KeepOpen) {
        shutdown.exe /s /t 0 /f | Out-Null
    }
}