Private/SpinnerProgress.ps1

function Write-InTUISpinner {
    <#
    .SYNOPSIS
        Rotating spinner with carriage return and elapsed time.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,

        [Parameter()]
        [int]$Frame = 0,

        [Parameter()]
        [System.Diagnostics.Stopwatch]$Stopwatch
    )

    $palette = Get-InTUIColorPalette
    $reset = $palette.Reset
    $spinChars = @('|', '/', '-', '\')
    $spinChar = $spinChars[$Frame % $spinChars.Count]

    $elapsed = ''
    if ($Stopwatch) {
        $secs = [int]$Stopwatch.Elapsed.TotalSeconds
        $elapsed = " $($palette.Dim)(${secs}s)$reset"
    }

    $ansiMessage = ConvertFrom-InTUIMarkup -Text $Message
    Write-Host "`r$($palette.Blue)$spinChar$reset $ansiMessage$elapsed " -NoNewline
}

function Write-InTUISpinnerComplete {
    <#
    .SYNOPSIS
        Clears spinner line and shows completion message.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Message = 'Done'
    )

    $palette = Get-InTUIColorPalette
    $reset = $palette.Reset
    $clearWidth = [math]::Max(80, [Console]::WindowWidth - 1)

    Write-Host "`r$(' ' * $clearWidth)`r$($palette.Green)+$reset $Message"
}

function Invoke-InTUIWithSpinner {
    <#
    .SYNOPSIS
        Wraps a script block with spinner animation.
        Replaces Invoke-SpectreCommandWithStatus.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Title,

        [Parameter(Mandatory)]
        [scriptblock]$ScriptBlock
    )

    # Start background job approach won't work for script-scope vars.
    # Use a simple spinner with polling.
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $frame = 0

    # Show initial spinner
    Write-InTUISpinner -Message $Title -Frame $frame -Stopwatch $sw

    # We can't run truly async in single-threaded PS, so we just run
    # the script block and show the spinner before/after.
    # For operations that support it, the spinner shows during execution.
    $result = $null
    try {
        $result = & $ScriptBlock
    }
    catch {
        $sw.Stop()
        Write-Host ""
        throw
    }

    $sw.Stop()
    Write-InTUISpinnerComplete -Message (Strip-InTUIMarkup -Text $Title)

    return $result
}