Spinner.ps1

# 加载动画:后台线程旋转字符指示器(winget 风格 |/-\ 青色)
# 权限解释:spinner 用 System.Threading.Timer 后台线程回调写屏,
# PowerShell scriptblock 跨线程调用不安全,回调必须是纯 .NET 代码;Console.Write 内部自带锁,跨线程安全。
# 重定向判断放在 PS 层(Invoke-WithSpinner),C# 只负责真实终端下的写屏,不判断重定向。

# 内联 C# 后台 spinner(进程内只编译一次)
if (-not ('CctSpinner' -as [type])) {
    Add-Type -TypeDefinition @'
using System;
using System.Threading;
 
public static class CctSpinner {
    private static readonly char[] Frames = new char[] { '|', '/', '-', '\\' };
    private static volatile int _idx;
    private static volatile bool _stopped;
    private static string _currentText;
 
    // 启动后台 spinner:每 intervalMs 毫秒重写一行(不换行)。
    // autoStopMs 后自动停止,防止长时间交互式程序期间后台线程持续写屏。
    // 返回 Timer 供 Stop 停止。
    public static Timer Start(string text, int intervalMs, int autoStopMs) {
        _idx = 0;
        _stopped = false;
        _currentText = text;
        var t = new Timer(Tick, null, 0, intervalMs);
        if (autoStopMs > 0) {
            // 独立停止定时器;线程池保持引用直到触发,不必显式存
            new Timer(AutoStop, t, autoStopMs, Timeout.Infinite);
        }
        return t;
    }
 
    private static void Tick(object state) {
        if (_stopped) return;
        try {
            char c = Frames[_idx & 3];
            _idx++;
            // 每帧先 ESC[2K 擦整行再写:中文显示宽 2 列,按 Length 算宽度清不干净(实测残留「史 /」)
            Console.Write("\r\x1b[2K\x1b[37m" + _currentText + " \x1b[36m" + c + "\x1b[0m");
        } catch { }
    }
 
    private static void AutoStop(object state) {
        Stop((Timer)state);
    }
 
    // 停止并清行(\r + ESC[2K 擦整行 + \r 归位);幂等安全
    public static void Stop(Timer t) {
        if (_stopped) return;
        _stopped = true;
        try {
            if (t != null) t.Dispose();
            Console.Write("\r\x1b[2K");
            _currentText = null;
        } catch { }
    }
}
'@

}

# PS 封装:控制台未重定向时启动 spinner,否则直接跑脚本(零输出)
# ScriptBlock 的返回值(含管道输出)原样透传
function Invoke-WithSpinner {
    param(
        [string]$Text,
        [scriptblock]$ScriptBlock,
        [int]$AutoStopMs = 8000
    )
    if ([Console]::IsOutputRedirected) {
        return & $ScriptBlock
    }
    $timer = [CctSpinner]::Start($Text, 100, $AutoStopMs)
    try {
        return & $ScriptBlock
    } finally {
        [CctSpinner]::Stop($timer)
    }
}