TerBg.psm1

# WtBg — per-session terminal background.
# Windows Terminal / VS Code / iTerm etc.: truecolor via OSC 11.
# Legacy Windows console (conhost) ignores OSC 11, so we fall back to a
# 16-color ConsoleColor background there.
# Nothing persisted, nothing written to settings.json. Session-only.

$script:ESC = [char]27
$script:BEL = [char]7

# Remember the starting console colors so Reset can restore them on conhost.
$script:OrigBg = try { $Host.UI.RawUI.BackgroundColor } catch { $null }
$script:OrigFg = try { $Host.UI.RawUI.ForegroundColor } catch { $null }
$script:OrigPriv = $null   # original $Host.PrivateData background colors

# Sync the host's Error/Warning/etc. backgrounds to the chosen bg, so error and
# warning text doesn't keep its own (black) background. Captures originals once.
function Set-HostPrivateBg {
    param([System.ConsoleColor]$ConsoleColor)
    try {
        $pd = $Host.PrivateData
        if (-not $pd) { return }
        $keys = 'ErrorBackgroundColor','WarningBackgroundColor','VerboseBackgroundColor','DebugBackgroundColor'
        if ($null -eq $script:OrigPriv) {
            $script:OrigPriv = @{}
            foreach ($k in $keys) { try { $script:OrigPriv[$k] = $pd.$k } catch {} }
        }
        foreach ($k in $keys) { try { $pd.$k = $ConsoleColor } catch {} }
    } catch {}
}

function Restore-HostPrivateBg {
    if ($null -eq $script:OrigPriv) { return }
    try {
        $pd = $Host.PrivateData
        if (-not $pd) { return }
        foreach ($k in $script:OrigPriv.Keys) { try { $pd.$k = $script:OrigPriv[$k] } catch {} }
    } catch {}
}

# The line you TYPE is drawn by PSReadLine with its own per-token colors. We keep
# each token's foreground (syntax highlighting) and just prepend our background as
# a 24-bit VT sequence — so the input line gets the chosen bg AND stays colorful.
$script:OrigPSRL = $null
# Map PSReadLine -Colors key -> Get-PSReadLineOption property. Selection/Emphasis
# are left alone so their own highlight backgrounds keep working.
$script:PSRLMap = [ordered]@{
    Command='CommandColor'; Comment='CommentColor'; ContinuationPrompt='ContinuationPromptColor'
    Default='DefaultTokenColor'; Error='ErrorColor'; Keyword='KeywordColor'; Member='MemberColor'
    Number='NumberColor'; Operator='OperatorColor'; Parameter='ParameterColor'; String='StringColor'
    Type='TypeColor'; Variable='VariableColor'
}

# Console index -> ANSI background SGR code (4-bit). conhost ignores 24-bit
# truecolor SGR, but renders this and maps it to the palette slot we remapped.
$script:AnsiBg = @{
    0=40; 1=44; 2=42; 3=46; 4=41; 5=45; 6=43; 7=47
    8=100; 9=104; 10=102; 11=106; 12=101; 13=105; 14=103; 15=107
}

function Set-PSReadLineBg {
    param([System.ConsoleColor]$ConsoleColor)
    if (-not (Get-Command Set-PSReadLineOption -ErrorAction SilentlyContinue)) { return }
    $opt = try { Get-PSReadLineOption } catch { $null }
    if (-not $opt) { return }
    # Capture originals once (so re-applying never stacks backgrounds).
    if ($null -eq $script:OrigPSRL) {
        $script:OrigPSRL = @{}
        foreach ($k in $script:PSRLMap.Keys) {
            $prop = $script:PSRLMap[$k]
            try { $script:OrigPSRL[$k] = [string]$opt.$prop } catch {}
        }
    }
    $code = $script:AnsiBg[[int]$ConsoleColor]
    $bg = "$($script:ESC)[${code}m"
    foreach ($k in $script:PSRLMap.Keys) {
        $orig = $script:OrigPSRL[$k]
        if ($null -eq $orig) { continue }
        try { Set-PSReadLineOption -Colors @{ $k = ($bg + $orig) } } catch {}
    }
}

function Restore-PSReadLineBg {
    if ($null -eq $script:OrigPSRL) { return }
    if (-not (Get-Command Set-PSReadLineOption -ErrorAction SilentlyContinue)) { return }
    foreach ($k in $script:OrigPSRL.Keys) {
        try { Set-PSReadLineOption -Colors @{ $k = $script:OrigPSRL[$k] } } catch {}
    }
}

# True when the host honors OSC 11 (skip the 16-color fallback there).
function Test-OscCapable {
    [bool]($env:WT_SESSION -or $env:TERM_PROGRAM -or $env:ConEmuPID)
}

# Curated dark palette the arrow-key picker cycles through.
# Hex = truecolor (Windows Terminal); CC = nearest ConsoleColor (legacy conhost).
# All sit at low luminance: contrast vs the #d0d0d0 text is >= 8.3 (WCAG AAA),
# muted (not saturated) to stay easy on the eyes.
$script:WtBgPalette = @(
    @{ Name='Navy';     Hex='1a2333'; R=26; G=35; B=51; CC='DarkBlue'    },
    @{ Name='Indigo';   Hex='221a33'; R=34; G=26; B=51; CC='DarkBlue'    },
    @{ Name='Teal';     Hex='11302e'; R=17; G=48; B=46; CC='DarkCyan'    },
    @{ Name='Forest';   Hex='16301b'; R=22; G=48; B=27; CC='DarkGreen'   },
    @{ Name='Olive';    Hex='2c2e16'; R=44; G=46; B=22; CC='DarkYellow'  },
    @{ Name='Umber';    Hex='2f2417'; R=47; G=36; B=23; CC='DarkRed'     },
    @{ Name='Maroon';   Hex='331a1a'; R=51; G=26; B=26; CC='DarkRed'     },
    @{ Name='Plum';     Hex='2e1a2a'; R=46; G=26; B=42; CC='DarkMagenta' },
    @{ Name='Violet';   Hex='241a33'; R=36; G=26; B=51; CC='DarkMagenta' },
    @{ Name='Slate';    Hex='1f262e'; R=31; G=38; B=46; CC='DarkGray'    },
    @{ Name='Aqua';     Hex='14302f'; R=20; G=48; B=47; CC='DarkCyan'    },
    @{ Name='Graphite'; Hex='232323'; R=35; G=35; B=35; CC='DarkGray'    }
)

# Relative luminance (WCAG) of an RGB triple — used to pick a readable text color.
function Get-RelLum {
    param([int]$R, [int]$G, [int]$B)
    $lin = { param($c) $s = $c / 255.0; if ($s -le 0.03928) { $s / 12.92 } else { [math]::Pow(($s + 0.055) / 1.055, 2.4) } }
    0.2126 * (& $lin $R) + 0.7152 * (& $lin $G) + 0.0722 * (& $lin $B)
}

# Choose a comfortable, readable foreground for a given background:
# soft light gray on dark backgrounds, near-black on light ones. Never pure white.
function Get-ReadableFg {
    param([int]$R, [int]$G, [int]$B)
    if ((Get-RelLum -R $R -G $G -B $B) -gt 0.35) {
        @{ R=26;  G=26;  B=26;  CC=[System.ConsoleColor]::Black }   # #1a1a1a on light bg
    } else {
        @{ R=208; G=208; B=208; CC=[System.ConsoleColor]::Gray }    # #d0d0d0 on dark bg
    }
}

# --- Legacy console palette remap -------------------------------------------
# conhost has only 16 fixed color slots and ignores truecolor. But we CAN
# reprogram the RGB of those 16 slots at runtime via SetConsoleScreenBufferInfoEx
# (no admin, session-only). So we set the slot we're about to use to the exact
# truecolor we want, then select it. Compiled on first use; degrades gracefully
# (e.g. redirected handle) to the plain 16-color behavior.
$script:ConhostReady   = $null   # $true / $false after first probe
$script:OrigColorTable = $null   # captured once, for reset

$script:ConhostCs = @'
using System;
using System.Runtime.InteropServices;
namespace WtBgNative {
  public static class Con {
    [StructLayout(LayoutKind.Sequential)] struct COORD { public short X, Y; }
    [StructLayout(LayoutKind.Sequential)] struct SMALL_RECT { public short Left, Top, Right, Bottom; }
    [StructLayout(LayoutKind.Sequential)] struct CSBIEX {
      public uint cbSize;
      public COORD dwSize;
      public COORD dwCursorPosition;
      public ushort wAttributes;
      public SMALL_RECT srWindow;
      public COORD dwMaximumWindowSize;
      public ushort wPopupAttributes;
      public int bFullscreenSupported;
      [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public uint[] ColorTable;
    }
    [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle(int n);
    [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetConsoleScreenBufferInfoEx(IntPtr h, ref CSBIEX x);
    [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetConsoleScreenBufferInfoEx(IntPtr h, ref CSBIEX x);
    const int STD_OUTPUT_HANDLE = -11;

    // Pin the window/buffer to what it was on first read, so repeated sets don't
    // drift it (the +1 below would otherwise accumulate and scroll the screen).
    static bool haveOrig = false;
    static COORD origSize;
    static SMALL_RECT origWin;

    static bool Read(out CSBIEX x) {
      x = new CSBIEX();
      x.cbSize = (uint)Marshal.SizeOf(typeof(CSBIEX));
      bool ok = GetConsoleScreenBufferInfoEx(GetStdHandle(STD_OUTPUT_HANDLE), ref x);
      if (ok && !haveOrig) { origSize = x.dwSize; origWin = x.srWindow; haveOrig = true; }
      return ok;
    }
    static bool Write(ref CSBIEX x) {
      // Always base the geometry on the original, never the (possibly drifted)
      // current values. Documented quirk: Set shrinks the window by 1, so +1.
      if (haveOrig) { x.dwSize = origSize; x.srWindow = origWin; }
      x.srWindow.Right = (short)(x.srWindow.Right + 1);
      x.srWindow.Bottom = (short)(x.srWindow.Bottom + 1);
      return SetConsoleScreenBufferInfoEx(GetStdHandle(STD_OUTPUT_HANDLE), ref x);
    }
    public static uint[] GetTable() { CSBIEX x; if (!Read(out x)) return null; return x.ColorTable; }
    public static bool SetSlot(int i, uint cref) { CSBIEX x; if (!Read(out x)) return false; x.ColorTable[i] = cref; return Write(ref x); }
    public static bool SetTable(uint[] t) {
      CSBIEX x; if (!Read(out x)) return false;
      for (int i = 0; i < 16 && i < t.Length; i++) x.ColorTable[i] = t[i];
      return Write(ref x);
    }
  }
}
'@


function Initialize-Conhost {
    if ($null -ne $script:ConhostReady) { return $script:ConhostReady }
    $ok = $false
    try {
        if (-not ('WtBgNative.Con' -as [type])) { Add-Type -TypeDefinition $script:ConhostCs -ErrorAction Stop }
        $t = [WtBgNative.Con]::GetTable()
        if ($null -ne $t) { $script:OrigColorTable = $t; $ok = $true }
    } catch { $ok = $false }
    $script:ConhostReady = $ok
    $ok
}

# COLORREF is 0x00BBGGRR (note: BGR order).
function ConvertTo-ColorRef { param([int]$R, [int]$G, [int]$B) [uint32]($R -bor ($G -shl 8) -bor ($B -shl 16)) }

# Reprogram the two slots we're about to use to exact RGB. Returns $true if applied.
function Set-ConhostSlots {
    param([int]$BgIndex,[int]$BgR,[int]$BgG,[int]$BgB,[int]$FgIndex,[int]$FgR,[int]$FgG,[int]$FgB)
    if (-not (Initialize-Conhost)) { return $false }
    try {
        [void][WtBgNative.Con]::SetSlot($BgIndex, (ConvertTo-ColorRef -R $BgR -G $BgG -B $BgB))
        [void][WtBgNative.Con]::SetSlot($FgIndex, (ConvertTo-ColorRef -R $FgR -G $FgG -B $FgB))
        return $true
    } catch { return $false }
}

function Reset-Conhost {
    if ($script:ConhostReady -and $null -ne $script:OrigColorTable) {
        try { [void][WtBgNative.Con]::SetTable($script:OrigColorTable) } catch {}
    }
}
# ----------------------------------------------------------------------------

function Get-WtBgColor {
    <#
      Deterministic dark, readable background color from a seed string.
      Similar seeds -> well-separated hues (FNV-1a hash + golden-ratio scramble).
      Returns @{ R=; G=; B=; Hex= }.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Seed)

    # FNV-1a 32-bit hash -> good avalanche so "proj-a" and "proj-b" diverge.
    # int64 math + mask: PS 5.1 overflows a raw [uint32] multiply into a double.
    $h    = [int64]2166136261
    $mask = [int64]4294967295   # 0xFFFFFFFF as decimal; the hex literal sign-extends to -1 in PS 5.1
    foreach ($c in $Seed.ToLowerInvariant().ToCharArray()) {
        $h = ($h -bxor [int64][byte][char]$c)
        $h = ($h * 16777619) -band $mask
    }

    # Golden-ratio scramble of the unit hash -> hues spread maximally.
    $golden = 0.6180339887498949
    $unit   = ($h / [double]$mask)
    $hue    = (($unit + $golden) % 1.0) * 360.0   # 0..360
    $sat    = 0.42                                  # muted
    $lum    = 0.15                                  # dark, keeps fg text readable

    $rgb = ConvertFrom-Hsl -H $hue -S $sat -L $lum
    # Index into a 7-color ConsoleColor palette (for the legacy-console fallback),
    # keyed off the same hue so WT and conhost pick a related color.
    $i = [int][math]::Floor($hue / 360.0 * 7)
    if ($i -gt 6) { $i = 6 }
    $rgb.Index7 = $i
    # Same idea over 12 buckets, to seed the picker's starting position.
    $j = [int][math]::Floor($hue / 360.0 * 12)
    if ($j -gt 11) { $j = 11 }
    $rgb.Index12 = $j
    return $rgb
}

function ConvertFrom-Hsl {
    param([double]$H, [double]$S, [double]$L)
    $C = (1 - [math]::Abs(2 * $L - 1)) * $S
    $Hp = $H / 60.0
    $X = $C * (1 - [math]::Abs(($Hp % 2) - 1))
    $m = $L - $C / 2
    switch ([int][math]::Floor($Hp)) {
        0 { $r=$C; $g=$X; $b=0 }
        1 { $r=$X; $g=$C; $b=0 }
        2 { $r=0;  $g=$C; $b=$X }
        3 { $r=0;  $g=$X; $b=$C }
        4 { $r=$X; $g=0;  $b=$C }
        default { $r=$C; $g=0; $b=$X }
    }
    $R = [int][math]::Round(($r + $m) * 255)
    $G = [int][math]::Round(($g + $m) * 255)
    $B = [int][math]::Round(($b + $m) * 255)
    return @{ R=$R; G=$G; B=$B; Hex=("#{0:X2}{1:X2}{2:X2}" -f $R,$G,$B) }
}

# Apply one color to the current session: OSC 11 truecolor everywhere, plus a
# ConsoleColor repaint on legacy conhost (which ignores OSC 11).
function Invoke-TermBgApply {
    param(
        [int]$R, [int]$G, [int]$B,
        [System.ConsoleColor]$ConsoleColor,
        [switch]$Title, [string]$Label
    )
    # Pick a readable text color for this background (light on dark, dark on light).
    $fg = Get-ReadableFg -R $R -G $G -B $B

    # KEY: PowerShell output, the prompt, Clear-Host and PSReadLine all use the
    # session's DEFAULT bg/fg slots. So we don't switch to a different slot — we
    # RECOLOR the default slots themselves. Then every surface follows.
    $bgCC  = if ($null -ne $script:OrigBg) { $script:OrigBg } else { [System.ConsoleColor]::DarkBlue }
    $fgCC  = if ($null -ne $script:OrigFg) { $script:OrigFg } else { [System.ConsoleColor]::Gray }
    $bgIdx = [int]$bgCC
    $fgIdx = [int]$fgCC

    # OSC 11/10 set the terminal default bg/fg (truecolor terminals).
    $osc11 = "{0}]11;rgb:{1:x2}{1:x2}/{2:x2}{2:x2}/{3:x2}{3:x2}{4}" -f $script:ESC, $R, $G, $B, $script:BEL
    $osc10 = "{0}]10;rgb:{1:x2}{1:x2}/{2:x2}{2:x2}/{3:x2}{3:x2}{4}" -f $script:ESC, $fg.R, $fg.G, $fg.B, $script:BEL
    [Console]::Write($osc11)   # harmless no-op on legacy conhost
    [Console]::Write($osc10)

    if (Test-OscCapable) {
        # OSC 4 — recolor the default palette slots to exact truecolor.
        $o4bg = "{0}]4;{1};rgb:{2:x2}/{3:x2}/{4:x2}{5}" -f $script:ESC, $bgIdx, $R, $G, $B, $script:BEL
        $o4fg = "{0}]4;{1};rgb:{2:x2}/{3:x2}/{4:x2}{5}" -f $script:ESC, $fgIdx, $fg.R, $fg.G, $fg.B, $script:BEL
        [Console]::Write($o4bg)
        [Console]::Write($o4fg)
    }
    else {
        # conhost: recolor the same default slots via the Win32 console palette.
        [void](Set-ConhostSlots -BgIndex $bgIdx -BgR $R -BgG $G -BgB $B `
                                -FgIndex $fgIdx -FgR $fg.R -FgG $fg.G -FgB $fg.B)
    }
    if ($Title) {
        # OSC 0 — tab title. Build first: a comma inside [Console]::Write(...)
        # is a method-arg separator, not -f args.
        $osc0 = "{0}]0;{1}{2}" -f $script:ESC, $Label, $script:BEL
        [Console]::Write($osc0)
    }
    # Keep the default slots as the session default (we just recolored them) and
    # repaint. Output/prompt/errors all use these slots, so they now match.
    try {
        $Host.UI.RawUI.BackgroundColor = $bgCC
        $Host.UI.RawUI.ForegroundColor = $fgCC
        Set-HostPrivateBg -ConsoleColor $bgCC
        Clear-Host
    } catch {}
    # Give the typed line our background while keeping its syntax-highlight colors.
    Set-PSReadLineBg -ConsoleColor $bgCC
    $script:WtBgLast = @{ R=$R; G=$G; B=$B }
}

# Map an arbitrary RGB to the nearest of the 16 ConsoleColors (for explicit hex
# on legacy conhost). Very dark seed colors are handled separately via Index7.
function Get-NearestConsoleColor {
    param([int]$R, [int]$G, [int]$B)
    $bright = [int]( [math]::Max([math]::Max($R,$G),$B) -gt 180 )
    [System.ConsoleColor]( [int]($B -gt 96) + 2*[int]($G -gt 96) + 4*[int]($R -gt 96) + 8*$bright )
}

# --- Persistent per-folder store --------------------------------------------
# One central JSON: %APPDATA%\terbg\colors.json (or ~/.config/terbg on *nix).
# Maps a folder path -> a color (palette name like "Teal", or a #hex).
$script:WtBgStorePath = if ($env:APPDATA) {
    Join-Path $env:APPDATA 'terbg\colors.json'
} else {
    Join-Path $HOME '.config/terbg/colors.json'
}
$script:StoreCache      = $null   # in-memory hashtable, lazily loaded
$script:LastAppliedPath = $null   # guards the prompt hook against re-applying

function Get-WtBgKey { param([string]$Path) $Path.TrimEnd('\','/').ToLowerInvariant() }

function Get-WtBgStore {
    if ($null -ne $script:StoreCache) { return $script:StoreCache }
    $h = @{}
    try {
        if (Test-Path $script:WtBgStorePath) {
            $raw = Get-Content $script:WtBgStorePath -Raw -ErrorAction Stop
            if ($raw -and $raw.Trim()) {
                $obj = $raw | ConvertFrom-Json
                foreach ($p in $obj.PSObject.Properties) { $h[$p.Name.ToLowerInvariant()] = [string]$p.Value }
            }
        }
    } catch {}
    $script:StoreCache = $h
    $h
}

function Save-WtBgStore {
    $h = Get-WtBgStore
    try {
        $dir = Split-Path $script:WtBgStorePath
        if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
        ($h | ConvertTo-Json) | Set-Content -Path $script:WtBgStorePath -Encoding UTF8
    } catch {}
}

function Set-WtBgChoice {
    param([string]$Path, [string]$Color)
    $h = Get-WtBgStore
    $h[(Get-WtBgKey $Path)] = $Color
    $script:StoreCache = $h
    Save-WtBgStore
}

function Remove-WtBgChoice {
    param([string]$Path)
    $h = Get-WtBgStore
    $k = Get-WtBgKey $Path
    if ($h.ContainsKey($k)) { $h.Remove($k); $script:StoreCache = $h; Save-WtBgStore }
}

function Get-WtBgChoice {
    param([string]$Path)
    $h = Get-WtBgStore
    $k = Get-WtBgKey $Path
    if ($h.ContainsKey($k)) { $h[$k] } else { $null }
}

# Turn a stored value (palette name or #hex) into R/G/B + ConsoleColor.
function Resolve-StoredColor {
    param([string]$Value)
    if (-not $Value) { return $null }
    $e = $script:WtBgPalette | Where-Object { $_.Name -ieq $Value } | Select-Object -First 1
    if ($e) { return @{ R=$e.R; G=$e.G; B=$e.B; CC=[System.ConsoleColor]$e.CC } }
    $clean = $Value.TrimStart('#')
    if ($clean -match '^[0-9a-fA-F]{6}$') {
        $R = [Convert]::ToInt32($clean.Substring(0,2),16)
        $G = [Convert]::ToInt32($clean.Substring(2,2),16)
        $B = [Convert]::ToInt32($clean.Substring(4,2),16)
        return @{ R=$R; G=$G; B=$B; CC=(Get-NearestConsoleColor -R $R -G $G -B $B) }
    }
    return $null
}
# ----------------------------------------------------------------------------

function Set-TermBg {
    <#
      .SYNOPSIS
        Set terminal background for THIS session only.
      .EXAMPLE
        Set-TermBg # interactive arrow-key picker (saves your choice for this folder)
        Set-TermBg "#1e1e2e" # explicit color
        Set-TermBg -Seed prod # color from a label
        Set-TermBg -Auto # color from current folder (non-interactive, computed)
        Set-TermBg -Restore # re-apply this folder's saved color (for the prompt hook)
        Set-TermBg -Forget # forget this folder's saved color
        Set-TermBg -List # list all saved folders
    #>

    [CmdletBinding(DefaultParameterSetName='Pick')]
    param(
        [Parameter(Position=0, ParameterSetName='Explicit')][string]$Hex,
        [Parameter(ParameterSetName='Seed')][string]$Seed,
        [Parameter(ParameterSetName='Auto')][switch]$Auto,
        [Parameter(ParameterSetName='Restore')][switch]$Restore,
        [Parameter(ParameterSetName='Forget')][switch]$Forget,
        [Parameter(ParameterSetName='List')][switch]$List,
        [switch]$Title
    )

    $set = $PSCmdlet.ParameterSetName

    if ($set -eq 'Explicit') {
        $clean = $Hex.TrimStart('#')
        $R = [Convert]::ToInt32($clean.Substring(0,2),16)
        $G = [Convert]::ToInt32($clean.Substring(2,2),16)
        $B = [Convert]::ToInt32($clean.Substring(4,2),16)
        $cc = Get-NearestConsoleColor -R $R -G $G -B $B
        Invoke-TermBgApply -R $R -G $G -B $B -ConsoleColor $cc -Title:$Title -Label $Hex
    }
    elseif ($set -eq 'Pick') {
        # No argument -> interactive picker, starting on the folder's color.
        $seed  = Split-Path -Leaf (Get-Location).Path
        $start = (Get-WtBgColor -Seed $seed).Index12
        Show-TermBgPicker -StartIndex $start
    }
    elseif ($set -eq 'Restore') {
        # Re-apply the saved color for the current folder. Idempotent and quiet:
        # only acts when the folder changed, so it's safe to call every prompt.
        $here = (Get-Location).Path
        $key  = Get-WtBgKey $here
        if ($key -eq $script:LastAppliedPath) { return }
        $col = Resolve-StoredColor (Get-WtBgChoice -Path $here)
        if ($col) {
            Invoke-TermBgApply -R $col.R -G $col.G -B $col.B -ConsoleColor $col.CC
            $script:LastAppliedPath = $key
        }
        elseif ($null -ne $script:LastAppliedPath) {
            # Left a remembered folder for an unremembered one -> back to default.
            Reset-TermBg
            $script:LastAppliedPath = $null
        }
    }
    elseif ($set -eq 'Forget') {
        $here = (Get-Location).Path
        Remove-WtBgChoice -Path $here
        Reset-TermBg
        $script:LastAppliedPath = $null
        Write-Host ("Forgotten: {0}" -f $here) -ForegroundColor Yellow
    }
    elseif ($set -eq 'List') {
        $h = Get-WtBgStore
        if ($h.Count -eq 0) { Write-Host "No saved folders." -ForegroundColor DarkGray; return }
        Write-Host ("Store: {0}" -f $script:WtBgStorePath) -ForegroundColor DarkGray
        $h.GetEnumerator() | Sort-Object Name | ForEach-Object {
            Write-Host (" {0,-40} {1}" -f $_.Name, $_.Value)
        }
    }
    else {
        # Seed or Auto -> deterministic color, non-interactive.
        if ($set -eq 'Auto' -or -not $Seed) { $Seed = Split-Path -Leaf (Get-Location).Path }
        $c = Get-WtBgColor -Seed $Seed
        $palette7 = @('DarkBlue','DarkGreen','DarkCyan','DarkRed','DarkMagenta','DarkYellow','DarkGray')
        $cc = [System.ConsoleColor]$palette7[$c.Index7]
        Invoke-TermBgApply -R $c.R -G $c.G -B $c.B -ConsoleColor $cc -Title:$Title -Label $Seed
    }
}

function Show-TermBgPicker {
    <#
      .SYNOPSIS
        Interactive background picker: arrow keys cycle colors live,
        Enter confirms, Esc restores the original background.
    #>

    param([int]$StartIndex = 0)

    $pal = $script:WtBgPalette
    $n   = $pal.Count
    $i   = ((($StartIndex % $n) + $n) % $n)

    # Non-interactive (piped/redirected input): apply start color, never block.
    $interactive = $true
    try { if ([Console]::IsInputRedirected) { $interactive = $false } } catch { $interactive = $false }
    if (-not $interactive) {
        $e = $pal[$i]
        Invoke-TermBgApply -R $e.R -G $e.G -B $e.B -ConsoleColor ([System.ConsoleColor]$e.CC)
        return
    }

    $osc    = [bool](Test-OscCapable)
    $origBg = try { $Host.UI.RawUI.BackgroundColor } catch { $null }
    $origFg = try { $Host.UI.RawUI.ForegroundColor } catch { $null }

    # A terminal can't change font size per line — that's the app's font setting.
    # So we fake "bigger": a letter-spaced title + blank lines for vertical room.
    $up = [char]0x2191; $dn = [char]0x2193
    $title = 'S E L E C T T E R M I N A L C O L O R'
    $ctrl  = "$up / $dn move Enter pick Esc cancel"
    # Plain Write-Host (no -ForegroundColor): inherit the readable fg set by
    # Invoke-TermBgApply, so the header never matches a remapped bg slot.
    $header = {
        Write-Host ''
        Write-Host (" " + $title)
        Write-Host ''
        Write-Host (" " + $ctrl)
        Write-Host ''
    }

    # Render the arrow glyphs even on legacy code pages (IT default is CP850).
    $prevEnc = try { [Console]::OutputEncoding } catch { $null }
    try { [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false } catch {}

    try {
        while ($true) {
            $e = $pal[$i]
            # Apply repaints the whole screen (Clear-Host), so redraw the header +
            # status from the top every frame — this keeps the header always visible.
            Invoke-TermBgApply -R $e.R -G $e.G -B $e.B -ConsoleColor ([System.ConsoleColor]$e.CC)
            $status = (' [{0}/{1}] {2,-10} #{3}' -f ($i+1), $n, $e.Name, $e.Hex)
            & $header
            Write-Host $status

            $key = [Console]::ReadKey($true)
            switch ($key.Key) {
                'UpArrow'   { $i = ((($i - 1) + $n) % $n) }
                'DownArrow' { $i = (($i + 1) % $n) }
                'Enter' {
                    # Persist the choice for this folder so it returns next time.
                    $here = (Get-Location).Path
                    Set-WtBgChoice -Path $here -Color $e.Name
                    $script:LastAppliedPath = Get-WtBgKey $here
                    Write-Host ('Background set: {0} (#{1}) - saved for {2}' -f $e.Name, $e.Hex, $here) -ForegroundColor Green
                    return
                }
                'Escape' {
                    # Reset terminal defaults + the remapped palette.
                    [Console]::Write(("{0}]111{1}" -f $script:ESC, $script:BEL))
                    [Console]::Write(("{0}]110{1}" -f $script:ESC, $script:BEL))
                    if ($osc) {
                        [Console]::Write(("{0}]104{1}" -f $script:ESC, $script:BEL))
                    } else {
                        Reset-Conhost
                    }
                    try {
                        if ($null -ne $origBg) { $Host.UI.RawUI.BackgroundColor = $origBg }
                        if ($null -ne $origFg) { $Host.UI.RawUI.ForegroundColor = $origFg }
                        Restore-HostPrivateBg
                        Clear-Host
                    } catch {}
                    Restore-PSReadLineBg
                    Write-Host 'Cancelled.' -ForegroundColor Yellow
                    return
                }
                default { }
            }
        }
    }
    catch {
        # ReadKey not supported by this host -> apply current color and exit.
        $e = $pal[$i]
        Invoke-TermBgApply -R $e.R -G $e.G -B $e.B -ConsoleColor ([System.ConsoleColor]$e.CC)
    }
    finally {
        if ($null -ne $prevEnc) { try { [Console]::OutputEncoding = $prevEnc } catch {} }
    }
}

function Reset-TermBg {
    <#
      .SYNOPSIS
        Restore default background.
      .DESCRIPTION
        Tries OSC 111 (reset bg to default). If your build ignores it,
        pass -To with an explicit color, e.g. Reset-TermBg -To "#0d1117".
    #>

    param([string]$To)
    if ($To) { Set-TermBg $To; return }
    # Reset terminal default fg/bg (OSC 111/110) AND the remapped palette entries
    # (OSC 104 on capable terminals, Win32 table restore on conhost).
    $osc111 = "{0}]111{1}" -f $script:ESC, $script:BEL
    $osc110 = "{0}]110{1}" -f $script:ESC, $script:BEL
    [Console]::Write($osc111)
    [Console]::Write($osc110)
    if (Test-OscCapable) {
        $osc104 = "{0}]104{1}" -f $script:ESC, $script:BEL
        [Console]::Write($osc104)
    } else {
        Reset-Conhost
    }
    # Restore the default attributes captured at module load, and repaint.
    try {
        if ($null -ne $script:OrigBg) { $Host.UI.RawUI.BackgroundColor = $script:OrigBg }
        if ($null -ne $script:OrigFg) { $Host.UI.RawUI.ForegroundColor = $script:OrigFg }
        Restore-HostPrivateBg
        Clear-Host
    } catch {}
    Restore-PSReadLineBg
}

Set-Alias -Name terbg   -Value Set-TermBg
Set-Alias -Name terbgx  -Value Reset-TermBg

Export-ModuleMember -Function Set-TermBg, Reset-TermBg, Get-WtBgColor -Alias terbg, terbgx