src/Tui.ps1

# Tui.ps1 - the interactive console UI (Simple + Detail modes).
# Hand-rolled with [Console]::ReadKey + truecolor ANSI so there are zero deps.

$e = [char]27   # ESC

function Write-Fg { param([string]$Hex, [string]$Text)
    $r = [convert]::ToInt32($Hex.Substring(1,2),16)
    $g = [convert]::ToInt32($Hex.Substring(3,2),16)
    $b = [convert]::ToInt32($Hex.Substring(5,2),16)
    "$e[38;2;$r;$g;${b}m$Text$e[0m"
}

# --- shared UI primitives (refined-flat style) --------------------------------

# A dim horizontal rule under section titles.
function Write-PPRule { param([int] $Width = 54) Write-Host (' ' + ([string][char]0x2500 * $Width)) -ForegroundColor DarkGray }

# A dim footer of key hints, separated by middots.
function Write-PPFooter { param([string[]] $Hints) Write-Host ''; Write-Host (' ' + ($Hints -join ' · ')) -ForegroundColor DarkGray }

# Longest .Length among strings, for programmatic column sizing.
function Get-PPMaxLen { param([string[]] $Strings) (@($Strings | ForEach-Object { ([string]$_).Length }) + 0 | Measure-Object -Maximum).Maximum }

# One selectable row. The selected row gets an inverse-video pill; the leading
# marker area is a fixed 3 cells (" ❯ " when selected, " " when not) so the
# text starts at the same column in both states.
function Write-PPRow {
    param([bool] $Selected, [string] $Text, [int] $Width)
    $pad = ([string]$Text).PadRight($Width)
    if ($Selected) {
        Write-Host ' ' -NoNewline
        Write-Host (" ❯ $pad ") -ForegroundColor Black -BackgroundColor Gray
    } else {
        Write-Host (" $pad ") -ForegroundColor DarkGray
    }
}

# Merge a working composition (hashtable) with optional slot overrides and
# return it as a PSCustomObject ready for Resolve-PoshPaletteTheme.
function ConvertTo-PPComposition {
    param([hashtable] $Comp, [hashtable] $Override = @{})
    $h = @{}
    foreach ($k in $Comp.Keys)     { $h[$k] = $Comp[$k] }
    foreach ($k in $Override.Keys) { $h[$k] = $Override[$k] }
    [pscustomobject]$h
}

# A mini terminal drawn from the theme's own hex values, rendered on a filled
# block of the theme's BACKGROUND color so the whole thing recolors as you scroll
# (a foreground-only preview looked static on the host terminal's dark bg).
function Show-PoshPalettePreview {
    param($Theme, [int] $Left = 42, [int] $Top = 4)
    $sc = $Theme.terminal.scheme
    $pr = $Theme.psReadLine
    $ps = $Theme.psStyle
    $W  = 42
    $rgb = { param($h) '{0};{1};{2}' -f [convert]::ToInt32($h.Substring(1,2),16), [convert]::ToInt32($h.Substring(3,2),16), [convert]::ToInt32($h.Substring(5,2),16) }
    $bg  = & $rgb $sc.background

    # Build one line from a flat list of hex,text,hex,text... on the bg block.
    $row = {
        param([object[]] $parts)
        $s = "$e[48;2;${bg}m"; $len = 0
        for ($j = 0; $j -lt $parts.Count; $j += 2) {
            $hex = $parts[$j]; if (-not $hex) { $hex = $sc.foreground }
            $s += "$e[38;2;$(& $rgb $hex)m$($parts[$j + 1])"
            $len += ('' + $parts[$j + 1]).Length
        }
        if ($len -lt $W) { $s += (' ' * ($W - $len)) }
        $s + "$e[0m"
    }

    $lines = @(
        (& $row @($sc.purple, ' preview'))
        (& $row @($null, ''))
        (& $row @($sc.green, ' user', $pr.Comment, ' in ', $sc.blue, '~/projects', $sc.purple, ' ❯'))
        (& $row @($pr.Command, ' git', $null, ' ', $pr.Parameter, 'commit', $null, ' ', $pr.Parameter, '-m', $null, ' ', $pr.String, '"feat: theme"'))
        (& $row @($pr.Variable, ' $count', $pr.Operator, ' = ', $pr.Number, '42'))
        (& $row @($pr.Comment, ' # tidy up'))
        (& $row @($ps.Directory, ' Documents/', $null, ' README.md'))
        (& $row @($ps.Error, ' Error: build failed'))
        (& $row @($null, ''))
        (& $row @($sc.green, ' user', $pr.Comment, ' in ', $sc.blue, '~/projects', $sc.purple, ' ❯ '))
    )

    [Console]::CursorVisible = $false
    for ($i = 0; $i -lt $lines.Count; $i++) {
        try { [Console]::SetCursorPosition($Left, $Top + $i); [Console]::Write($lines[$i]) } catch { }
    }
    [Console]::Write("$e[0m")
}

# Generic scrollable picker. Items need a .Name; returns the chosen item or $null.
# A navigable "Back" row sits below the items (Esc is still the shortcut).
function Show-PoshPaletteList {
    param([string] $Title, [array] $Items, [scriptblock] $PreviewFor)

    $idx   = 0
    $total = $Items.Count + 1   # +1 for the Back row
    $width = [Math]::Max((Get-PPMaxLen (@($Items | ForEach-Object { $_.Name }) + '← Back')), 12)
    [Console]::CursorVisible = $false
    try {
        while ($true) {
            Clear-Host
            Write-Host ""
            Write-Host " $Title" -ForegroundColor White
            Write-PPRule
            Write-Host ""
            for ($i = 0; $i -lt $Items.Count; $i++) {
                Write-PPRow ($i -eq $idx) $Items[$i].Name $width
            }
            Write-PPRow ($idx -eq $Items.Count) '← Back' $width
            Write-PPFooter @('↑/↓ move', 'Enter select', 'Esc back')
            if ($PreviewFor -and $idx -lt $Items.Count) { & $PreviewFor $Items[$idx] }

            $key = [Console]::ReadKey($true)
            switch ($key.Key) {
                'UpArrow'   { $idx = ($idx - 1 + $total) % $total }
                'DownArrow' { $idx = ($idx + 1) % $total }
                'Enter'     { if ($idx -eq $Items.Count) { return $null } else { return $Items[$idx] } }
                'Escape'    { return $null }
            }
        }
    } finally { [Console]::CursorVisible = $true }
}

# --- Simple mode: pick a full preset ------------------------------------------

# Shown after a theme is applied: confirms what happened and what to do next.
# Returns 'quit' if the user chose to quit, otherwise $null (back to menu).
function Show-PoshPaletteApplied {
    param($Theme)
    Write-Host ""
    Write-Host " ✓ Applied '$($Theme.name)'" -ForegroundColor Green
    Write-Host ""
    Write-Host " Next steps" -ForegroundColor Cyan
    Write-Host " • Terminal colors update instantly in this window." -ForegroundColor Gray
    Write-Host " • Open a NEW tab or window to load the prompt + input colors." -ForegroundColor Gray
    Write-Host " • Tweak one layer later, e.g. Set-PoshPalettePrompt <name>" -ForegroundColor Gray
    Write-Host ""
    Write-Host " [Enter] back to menu [Q] quit" -ForegroundColor DarkGray
    while ($true) {
        $k = [Console]::ReadKey($true)
        if ($k.Key -eq 'Enter' -or $k.Key -eq 'Escape') { return $null }
        if ([string]$k.KeyChar -in 'q', 'Q')            { return 'quit' }
    }
}

function Invoke-PoshPaletteSimpleMode {
    $themes = Get-PoshPaletteThemes
    $chosen = Show-PoshPaletteList -Title 'Simple mode - pick a theme' -Items $themes -PreviewFor {
        param($t) Show-PoshPalettePreview -Theme (Resolve-PoshPaletteTheme $t.Data)
    }
    if ($chosen) {
        Clear-Host
        $t = Resolve-PoshPaletteTheme $chosen.Data
        Set-PoshPaletteTheme -Theme $t
        return (Show-PoshPaletteApplied $t)
    }
}

# Adjust a numeric value with the arrow keys. Returns the new value, or $null on Esc.
function Invoke-PoshPaletteAdjust {
    param([string] $Label, [int] $Value, [int] $Min, [int] $Max, [int] $Step, [string] $Suffix = '')
    [Console]::CursorVisible = $false
    try {
        while ($true) {
            Clear-Host
            Write-Host "`n $Label" -ForegroundColor Cyan
            Write-Host " </> adjust Enter confirm Esc cancel`n" -ForegroundColor DarkGray
            Write-Host (" " + $Value + $Suffix) -ForegroundColor White
            $key = [Console]::ReadKey($true)
            switch ($key.Key) {
                'LeftArrow'  { $Value = [Math]::Max($Min, $Value - $Step) }
                'RightArrow' { $Value = [Math]::Min($Max, $Value + $Step) }
                'Enter'      { return $Value }
                'Escape'     { return $null }
            }
        }
    } finally { [Console]::CursorVisible = $true }
}

# --- Detail mode: compose each layer independently ----------------------------

# Build a live preview callback for a list picker that swaps one slot of the
# working composition. We use a plain (non-closure) scriptblock bound to the
# module session state plus $script: capture vars, because .GetNewClosure()
# rebinds the block to a fresh dynamic module that can't see module functions
# like ConvertTo-PPComposition / Resolve-PoshPaletteTheme.
$script:PPPreviewComp = $null
$script:PPPreviewSlot = $null
function New-PoshPalettePreviewFor {
    param([hashtable] $Comp, [string] $Slot)
    $script:PPPreviewComp = $Comp
    $script:PPPreviewSlot = $Slot
    { param($it)
        Show-PoshPalettePreview -Theme (Resolve-PoshPaletteTheme (ConvertTo-PPComposition $script:PPPreviewComp @{ $script:PPPreviewSlot = $it.Id }))
    }
}

function Invoke-PoshPaletteDetailMode {
    $presets = Get-PoshPaletteThemes
    if (-not $presets) { return }
    $base = $presets[0].Data

    # Working composition as a mutable hashtable, seeded from the first preset.
    $comp = @{
        name     = 'Custom'
        scheme   = $base.scheme
        palette  = $base.palette
        prompt   = $base.prompt
        font     = $base.font
        fontSize = [int]($base.fontSize ?? 11)
        opacity  = [int]($base.opacity ?? 100)
        acrylic  = [bool]($base.acrylic ?? $false)
    }

    # Menu rows: 7 editable fields + Apply + Back. Arrows OR number/letter keys.
    $rows = @('scheme', 'palette', 'prompt', 'font', 'opacity', 'acrylic', 'fontsize', 'apply', 'back')
    $idx  = 0

    $invoke = {
        param([string] $id)
        switch ($id) {
            'scheme' {
                $p = Show-PoshPaletteList -Title 'Color scheme' -Items (Get-PoshPaletteCatalog schemes) -PreviewFor (New-PoshPalettePreviewFor $comp 'scheme')
                if ($p) { $comp.scheme = $p.Id }
            }
            'palette' {
                $p = Show-PoshPaletteList -Title 'Shell colors (PSReadLine + output)' -Items (Get-PoshPaletteCatalog palettes) -PreviewFor (New-PoshPalettePreviewFor $comp 'palette')
                if ($p) { $comp.palette = $p.Id }
            }
            'prompt' {
                $p = Show-PoshPaletteList -Title 'Prompt (oh-my-posh)' -Items (Get-PoshPaletteCatalog prompts) -PreviewFor (New-PoshPalettePreviewFor $comp 'prompt')
                if ($p) { $comp.prompt = $p.Id }
            }
            'font' {
                # Font can't be shown in the ANSI preview; pick from the list.
                $p = Show-PoshPaletteList -Title 'Font (must be installed)' -Items (Get-PoshPaletteFonts)
                if ($p) { $comp.font = $p.id }
            }
            'opacity' {
                $v = Invoke-PoshPaletteAdjust 'Opacity' ([int]$comp.opacity) 30 100 5 '%'
                if ($null -ne $v) { $comp.opacity = $v }
            }
            'acrylic'  { $comp.acrylic = -not [bool]$comp.acrylic }
            'fontsize' {
                $v = Invoke-PoshPaletteAdjust 'Font size' ([int]$comp.fontSize) 8 24 1
                if ($null -ne $v) { $comp.fontSize = $v }
            }
            'apply' {
                Clear-Host
                $t = Resolve-PoshPaletteTheme (ConvertTo-PPComposition $comp)
                Set-PoshPaletteTheme -Theme $t
                return (Show-PoshPaletteApplied $t)   # 'quit' or $null
            }
        }
        return ''
    }

    [Console]::CursorVisible = $false
    try {
        while ($true) {
            Clear-Host
            Write-Host ""
            Write-Host " Detail mode - compose your look" -ForegroundColor White
            Write-PPRule
            Write-Host ""
            $tr = { param($s) $v = [string]$s; if ($v.Length -gt 16) { $v.Substring(0, 15) + '…' } else { $v } }
            $labels = @(
                "[1] Scheme : $(& $tr $comp.scheme)"
                "[2] Shell colors : $(& $tr $comp.palette)"
                "[3] Prompt : $(& $tr $comp.prompt)"
                "[4] Font : $(& $tr $comp.font)"
                "[5] Opacity : $($comp.opacity)%"
                "[6] Acrylic : $(if ($comp.acrylic) { 'on' } else { 'off' })"
                "[7] Font size : $($comp.fontSize)"
                "[A] Apply"
                "[Esc] ← Back"
            )
            $lw = Get-PPMaxLen $labels
            for ($i = 0; $i -lt $labels.Count; $i++) {
                Write-PPRow ($i -eq $idx) $labels[$i] $lw
            }
            Write-PPFooter @('↑/↓ move', 'Enter edit', 'A apply', 'Esc back')
            # live preview as the right column, top-aligned with the field rows
            Show-PoshPalettePreview -Theme (Resolve-PoshPaletteTheme (ConvertTo-PPComposition $comp)) -Left ($lw + 9) -Top 4

            $key = [Console]::ReadKey($true)
            if ($key.Key -eq 'Escape') { return }
            $target = $null
            switch ($key.Key) {
                'UpArrow'   { $idx = ($idx - 1 + $rows.Count) % $rows.Count }
                'DownArrow' { $idx = ($idx + 1) % $rows.Count }
                'Enter'     { $target = $rows[$idx] }
            }
            switch ($key.KeyChar) {
                '1' { $target = 'scheme'; $idx = 0 }
                '2' { $target = 'palette'; $idx = 1 }
                '3' { $target = 'prompt'; $idx = 2 }
                '4' { $target = 'font'; $idx = 3 }
                '5' { $target = 'opacity'; $idx = 4 }
                '6' { $target = 'acrylic'; $idx = 5 }
                '7' { $target = 'fontsize'; $idx = 6 }
                { $_ -in 'a', 'A' } { $target = 'apply'; $idx = 7 }
            }
            if ($target -eq 'back') { return }
            if ($target) {
                $r = & $invoke $target
                if ($target -eq 'apply') { return $r }   # 'quit' bubbles to Start
            }
        }
    } finally { [Console]::CursorVisible = $true }
}

# --- Entry point --------------------------------------------------------------

function Start-PoshPalette {
    [CmdletBinding()]
    param()
    $items = @(
        @{ Key = '1'; Title = 'Simple mode'; Desc = 'Pick a full theme from a scrollable list';   Run = { Invoke-PoshPaletteSimpleMode } }
        @{ Key = '2'; Title = 'Detail mode'; Desc = 'Compose scheme, colors, prompt, font';        Run = { Invoke-PoshPaletteDetailMode } }
        @{ Key = '3'; Title = 'Doctor';      Desc = 'Check fonts, oh-my-posh, terminal';           Run = { Clear-Host; Test-PoshPaletteSetup | Out-Null; Write-Host "`n [Enter] back to menu" -ForegroundColor DarkGray; [Console]::ReadKey($true) | Out-Null } }
        @{ Key = 'Q'; Title = 'Quit';        Desc = 'Exit Posh Palette';                           Run = { 'quit' } }
    )
    $titleW = Get-PPMaxLen ($items | ForEach-Object { $_.Title })
    $texts  = $items | ForEach-Object { "[$($_.Key)] " + $_.Title.PadRight($titleW) + ' ' + $_.Desc }
    $rowW   = Get-PPMaxLen $texts
    $idx = 0
    [Console]::CursorVisible = $false
    try {
        while ($true) {
            Clear-Host
            Write-Host ""
            Write-Host " >_ " -ForegroundColor White -NoNewline
            Write-Host "Posh Palette" -ForegroundColor White
            Write-PPRule
            Write-Host " Style all 4 layers: scheme · PSReadLine · `$PSStyle · prompt" -ForegroundColor DarkGray
            Write-Host ""
            for ($i = 0; $i -lt $items.Count; $i++) {
                Write-PPRow ($i -eq $idx) $texts[$i] $rowW
            }
            Write-PPFooter @('↑/↓ move', 'Enter select', 'Q quit')

            $key = [Console]::ReadKey($true)
            # arrow navigation
            switch ($key.Key) {
                'UpArrow'   { $idx = ($idx - 1 + $items.Count) % $items.Count }
                'DownArrow' { $idx = ($idx + 1) % $items.Count }
                'Enter'     { if ((& $items[$idx].Run) -eq 'quit') { return } }
                'Escape'    { return }
            }
            # number/letter shortcuts (arrow keys have KeyChar 0, so no double-fire)
            $ch  = ([string]$key.KeyChar).ToUpper()
            $hit = $items | Where-Object { $_.Key -eq $ch } | Select-Object -First 1
            if ($hit) { if ((& $hit.Run) -eq 'quit') { return } }
        }
    } finally { [Console]::CursorVisible = $true }
}