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 |