src/Services/AnsiService.ps1

# AnsiService — Helpers VT100/ANSI para el Renderer (ADR-005)
# Truecolor escape codes. Sin Write-Host -Foreground. Sin [Console]::ForegroundColor.

class AnsiService {
    static [string] $Esc = [char]27 + '['
    static [string] $Reset = [char]27 + '[0m'

    # Foreground truecolor: \x1b[38;2;r;g;bm
    static [string] Fg([int]$r, [int]$g, [int]$b) {
        return "$([AnsiService]::Esc)38;2;${r};${g};${b}m"
    }

    # Background truecolor: \x1b[48;2;r;g;bm
    static [string] Bg([int]$r, [int]$g, [int]$b) {
        return "$([AnsiService]::Esc)48;2;${r};${g};${b}m"
    }

    # Hex (#rrggbb o rrggbb) → foreground
    static [string] FgHex([string]$hex) {
        $rgb = [AnsiService]::HexToRgb($hex)
        return [AnsiService]::Fg($rgb[0], $rgb[1], $rgb[2])
    }

    static [string] BgHex([string]$hex) {
        $rgb = [AnsiService]::HexToRgb($hex)
        return [AnsiService]::Bg($rgb[0], $rgb[1], $rgb[2])
    }

    # Cursor positioning: \x1b[<row>;<col>H (1-indexed)
    static [string] MoveTo([int]$row, [int]$col) {
        return "$([AnsiService]::Esc)${row};${col}H"
    }

    # Clear screen + cursor home
    static [string] ClearScreen() {
        return "$([AnsiService]::Esc)2J$([AnsiService]::Esc)H"
    }

    # Hide / show cursor
    static [string] HideCursor() { return "$([AnsiService]::Esc)?25l" }
    static [string] ShowCursor() { return "$([AnsiService]::Esc)?25h" }

    # Alternate screen buffer (entrar/salir sin ensuciar el scrollback)
    static [string] EnterAltBuffer() { return "$([AnsiService]::Esc)?1049h" }
    static [string] LeaveAltBuffer() { return "$([AnsiService]::Esc)?1049l" }

    # Synchronized output (DEC SYNC, modo 2026). Le dice a la terminal: "procesá
    # todo lo que viene pero no lo pintes hasta que veas End". Evita render
    # progresivo línea-por-línea que el ojo percibe como parpadeo cuando el frame
    # nuevo se solapa visualmente con el anterior.
    # Soportado por Windows Terminal, iTerm2, Kitty, Alacritty, modernos pwsh.
    # Si la terminal no lo entiende, ignora silenciosamente — defensivo siempre.
    static [string] BeginSync() { return "$([AnsiService]::Esc)?2026h" }
    static [string] EndSync()   { return "$([AnsiService]::Esc)?2026l" }

    # Atributos
    static [string] Bold()      { return "$([AnsiService]::Esc)1m" }
    static [string] Dim()       { return "$([AnsiService]::Esc)2m" }
    static [string] Italic()    { return "$([AnsiService]::Esc)3m" }
    static [string] Underline() { return "$([AnsiService]::Esc)4m" }

    # "#6ee7ff" → @(110, 231, 255). Acepta con o sin '#'.
    static [int[]] HexToRgb([string]$hex) {
        $h = $hex.TrimStart('#')
        if ($h.Length -ne 6) {
            throw "Hex inválido: '$hex' (esperado #rrggbb)"
        }
        return @(
            [Convert]::ToInt32($h.Substring(0, 2), 16),
            [Convert]::ToInt32($h.Substring(2, 2), 16),
            [Convert]::ToInt32($h.Substring(4, 2), 16)
        )
    }

    # ¿Soporta truecolor el host actual? Heuristica simple: COLORTERM=truecolor o WT_SESSION presente.
    # Nota: dentro de class methods PS no expone $PSVersionTable / $env: directamente.
    static [bool] SupportsTrueColor() {
        $colorTerm = [Environment]::GetEnvironmentVariable('COLORTERM')
        if ($colorTerm -eq 'truecolor' -or $colorTerm -eq '24bit') { return $true }
        if ([Environment]::GetEnvironmentVariable('WT_SESSION')) { return $true }   # Windows Terminal
        $psv = $global:PSVersionTable
        if ($psv -and $psv.PSVersion.Major -ge 7) { return $true }                  # pwsh 7+ moderno
        return $false
    }
}