Private/Terminal.ps1

# Dialogue with the terminal emulator: OSC sequences and window title, plus
# reading the Windows Terminal settings to find the reference background colour.

$script:TcEsc = [string][char]27
$script:TcBel = [string][char]7

# Backgrounds of the colour schemes shipped with Windows Terminal.
$script:TcBuiltInSchemes = @{
    'campbell'            = '#0C0C0C'
    'campbell powershell' = '#012456'
    'vintage'             = '#000000'
    'one half dark'       = '#282C34'
    'one half light'      = '#FAFAFA'
    'solarized dark'      = '#002B36'
    'solarized light'     = '#FDF6E3'
    'tango dark'          = '#000000'
    'tango light'         = '#FFFFFF'
}

$script:TcDefaultBaseBackground = '#0C0C0C'
$script:TcBaseBackgroundCache = $null

# Titles this session has set on its tab, most recent first (see Set-TcWindowTitle).
$script:TcAppliedTitles = @()

function Test-TcWindowsTerminal {
    return -not [string]::IsNullOrEmpty($env:WT_SESSION)
}

function Test-TcVtSupported {
    <#
        .SYNOPSIS
        Determines whether OSC sequences can be emitted without risking stray
        characters on screen.
    #>

    if (Test-TcWindowsTerminal) { return $true }
    if ($env:TERM_PROGRAM -in @('vscode', 'WezTerm', 'Hyper', 'Tabby')) { return $true }
    if (-not [string]::IsNullOrEmpty($env:WEZTERM_PANE)) { return $true }
    return $false
}

function Get-TcWtSettingsPath {
    <#
        .SYNOPSIS
        Path of the Windows Terminal settings.json (Store, Preview, unpackaged or
        portable).
    #>

    [CmdletBinding()]
    param()

    $candidates = @()
    if ($env:WT_SETTINGS_DIR) { $candidates += (Join-Path $env:WT_SETTINGS_DIR 'settings.json') }
    $local = $env:LOCALAPPDATA
    if ($local) {
        $candidates += Join-Path $local 'Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json'
        $candidates += Join-Path $local 'Packages\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\LocalState\settings.json'
        $candidates += Join-Path $local 'Microsoft\Windows Terminal\settings.json'
    }

    foreach ($c in $candidates) {
        if ($c -and [System.IO.File]::Exists($c)) { return $c }
    }
    return $null
}

function Get-TcWtProfile {
    <#
        .SYNOPSIS
        Returns the active Windows Terminal profile, merged with profiles.defaults.
    #>

    [CmdletBinding()]
    param($Settings, [string] $ProfileId)

    if ($null -eq $Settings) { return $null }

    $profiles = Get-TcJsonProperty -InputObject $Settings -Name 'profiles'
    if ($null -eq $profiles) { return $null }

    $defaults = Get-TcJsonProperty -InputObject $profiles -Name 'defaults'
    $list = Get-TcJsonProperty -InputObject $profiles -Name 'list'

    $match = $null
    if ($list -and $ProfileId) {
        $match = $list | Where-Object { $_.guid -eq $ProfileId } | Select-Object -First 1
    }
    if (-not $match -and $list) {
        $defaultProfile = Get-TcJsonProperty -InputObject $Settings -Name 'defaultProfile'
        if ($defaultProfile) {
            $match = $list | Where-Object { $_.guid -eq $defaultProfile } | Select-Object -First 1
        }
    }

    $result = @{ background = $null; colorScheme = $null; suppressApplicationTitle = $false; tabColor = $null; tabTitle = $null }
    foreach ($source in @($defaults, $match)) {
        if ($null -eq $source) { continue }
        foreach ($key in @('background', 'colorScheme', 'suppressApplicationTitle', 'tabColor', 'tabTitle')) {
            $value = Get-TcJsonProperty -InputObject $source -Name $key
            if ($null -ne $value) { $result[$key] = $value }
        }
    }
    return $result
}

function Get-TcSettingsBackground {
    <#
        .SYNOPSIS
        Background colour declared by an already-parsed settings.json: the
        profile's if it sets one, otherwise its colour scheme's. Returns $null when
        the document does not allow a conclusion.
    #>

    [CmdletBinding()]
    param($Settings, [string] $ProfileId)

    $wtProfile = Get-TcWtProfile -Settings $Settings -ProfileId $ProfileId
    if ($null -eq $wtProfile) { return $null }

    if ($wtProfile.background) {
        $rgb = ConvertFrom-TcColor -Value ([string]$wtProfile.background)
        if ($rgb) { return $rgb }
    }

    $schemeName = [string]$wtProfile.colorScheme
    if ([string]::IsNullOrWhiteSpace($schemeName)) { $schemeName = 'Campbell' }

    $schemes = Get-TcJsonProperty -InputObject $Settings -Name 'schemes'
    if ($schemes) {
        $scheme = $schemes | Where-Object { $_.name -eq $schemeName } | Select-Object -First 1
        if ($scheme -and $scheme.background) {
            $rgb = ConvertFrom-TcColor -Value ([string]$scheme.background)
            if ($rgb) { return $rgb }
        }
    }

    $key = $schemeName.ToLowerInvariant()
    if ($script:TcBuiltInSchemes.ContainsKey($key)) {
        return (ConvertFrom-TcColor -Value $script:TcBuiltInSchemes[$key])
    }

    return $null
}

function Get-TcBaseBackground {
    <#
        .SYNOPSIS
        The terminal's [normal] background colour, used as the blending base.
        Derived from the Windows Terminal settings, and cached.
 
        .PARAMETER SettingsPath
        Reads this settings.json instead of the automatically detected one. The
        cache is then bypassed (used by the installers and the tests).
    #>

    [CmdletBinding()]
    param(
        [switch] $Refresh,
        [string] $SettingsPath
    )

    if (-not $SettingsPath) {
        if ($script:TcOptions -and $script:TcOptions.BaseBackground) {
            $explicit = ConvertFrom-TcColor -Value $script:TcOptions.BaseBackground
            if ($explicit) { return $explicit }
        }
        if (-not $Refresh -and $script:TcBaseBackgroundCache) { return $script:TcBaseBackgroundCache }
        $SettingsPath = Get-TcWtSettingsPath
        $cacheable = $true
    } else {
        $cacheable = $false
    }

    $rgb = $null
    if ($SettingsPath) {
        $rgb = Get-TcSettingsBackground -Settings (ConvertFrom-TcJsonFile -Path $SettingsPath) -ProfileId $env:WT_PROFILE_ID
    }
    if ($null -eq $rgb) { $rgb = ConvertFrom-TcColor -Value $script:TcDefaultBaseBackground }

    if ($cacheable) { $script:TcBaseBackgroundCache = $rgb }
    return $rgb
}

function Write-TcRaw {
    <#
        .SYNOPSIS
        Writes a control sequence directly to the console, bypassing the PowerShell
        pipeline.
    #>

    [CmdletBinding()]
    param([string] $Text)

    try { [Console]::Write($Text) } catch {
        Write-Verbose "TerminalColors: could not write to the console ($($_.Exception.Message))"
    }
}

function Get-TcBackgroundSequence {
    <#
        .SYNOPSIS
        Builds the OSC 11 sequence requesting the background colour change.
        Canonical xterm format (rgb:rr/gg/bb), understood by Windows Terminal.
    #>

    [CmdletBinding()]
    param([hashtable] $Rgb)

    return ('{0}]11;rgb:{1:x2}/{2:x2}/{3:x2}{4}' -f $script:TcEsc, [int]$Rgb.R, [int]$Rgb.G, [int]$Rgb.B, $script:TcBel)
}

function Get-TcBackgroundResetSequence {
    <#
        .SYNOPSIS
        OSC 111 sequence: back to the profile's default background colour.
    #>

    [CmdletBinding()]
    param()

    return ('{0}]111{1}' -f $script:TcEsc, $script:TcBel)
}

function Set-TcTerminalBackground {
    <#
        .SYNOPSIS
        Changes the active pane's background colour (OSC 11). With the supplied
        Windows Terminal theme, the tab and the title bar follow.
    #>

    [CmdletBinding()]
    param([hashtable] $Rgb)

    Write-TcRaw -Text (Get-TcBackgroundSequence -Rgb $Rgb)
}

function Reset-TcTerminalBackground {
    <#
        .SYNOPSIS
        Restores the default background. OSC 111 is the sequence meant for that; in
        explicit mode the base colour derived from the settings is rewritten instead.
    #>

    [CmdletBinding()]
    param([switch] $Explicit)

    if ($Explicit) {
        Set-TcTerminalBackground -Rgb (Get-TcBaseBackground)
        return
    }
    Write-TcRaw -Text (Get-TcBackgroundResetSequence)
}

function Set-TcWindowTitle {
    <#
        .SYNOPSIS
        Sets the window title (and therefore the Windows Terminal tab title) through
        the Unicode console API: unlike OSC 0, emoji get through whatever the active
        code page is.
    #>

    [CmdletBinding()]
    param([string] $Title)

    try {
        $Host.UI.RawUI.WindowTitle = $Title
        # Remember the last two titles we set. Windows Terminal mirrors the active
        # tab's title onto the window title with a small delay, so right after a
        # change the window can still be showing the previous one - which
        # Test-TcActiveTab must not read as [this tab is not the active one].
        $script:TcAppliedTitles = @(@($Title) + @($script:TcAppliedTitles | Select-Object -First 1))
    } catch {
        Write-Verbose "TerminalColors: title could not be changed ($($_.Exception.Message))"
    }
}

function Get-TcWindowTitle {
    try { return $Host.UI.RawUI.WindowTitle } catch { return $null }
}

function Format-TcTitle {
    <#
        .SYNOPSIS
        Applies the title template. Tokens: {icon} {name} {color} {folder} {path}
    #>

    [CmdletBinding()]
    param(
        [string] $Format,
        [pscustomobject] $Info,
        [string] $Path
    )

    $folder = ''
    if ($Path) { $folder = Split-Path $Path -Leaf }

    $title = $Format
    $title = $title.Replace('{icon}', [string]$Info.Icon)
    $title = $title.Replace('{name}', [string]$Info.Name)
    $title = $title.Replace('{color}', [string]$Info.Color)
    $title = $title.Replace('{folder}', $folder)
    $title = $title.Replace('{path}', [string]$Path)

    return $title.Trim()
}