private/Get-WtwColorCircleEmoji.ps1

function Get-WtwColorCircleCodepoints {
    <#
    .SYNOPSIS
        Color-circle codepoints used as Peacock swatches (not worktree identity).
    #>

    @(
        0x1F534, # 🔴
        0x1F7E0, # 🟠
        0x1F7E1, # 🟡
        0x1F7E2, # 🟢
        0x1F535, # 🔵
        0x1F7E3, # 🟣
        0x1F7E4, # 🟤
        0x26AB,  # ⚫
        0x26AA   # ⚪
    )
}

function Get-WtwUnicodeScalarLength {
    param([int] $Codepoint)
    if ($Codepoint -gt 0xFFFF) { return 2 }
    return 1
}

function Get-WtwNameWithoutColorCircle {
    <#
    .SYNOPSIS
        Strip leading color-circle swatches (and following spaces) only.
    #>

    [CmdletBinding()]
    param([AllowNull()] [AllowEmptyString()] [string] $Name)

    if ([string]::IsNullOrWhiteSpace($Name)) { return $Name }
    $circles = @(Get-WtwColorCircleCodepoints)
    $i = 0
    $len = $Name.Length
    while ($i -lt $len) {
        $cp = [char]::ConvertToUtf32($Name, $i)
        if ($circles -notcontains $cp) { break }
        $i += Get-WtwUnicodeScalarLength $cp
        while ($i -lt $len -and [char]::IsWhiteSpace($Name[$i])) { $i++ }
    }
    if ($i -ge $len) { return '' }
    return $Name.Substring($i)
}

function Get-WtwColorCircleEmoji {
    <#
    .SYNOPSIS
        Pick the closest Unicode color-circle emoji for a #rrggbb color.
    .DESCRIPTION
        Returns a single-codepoint emoji from the standard color-circle set so
        SourceGit/Superset display reliably without ZWJ/font-composition issues.
        Distance is Euclidean RGB; saturation acts as a tiebreaker between
        gray-leaning candidates (black/white) and chromatic ones.
    #>

    param([string] $Hex)

    if ([string]::IsNullOrWhiteSpace($Hex)) { return [char]::ConvertFromUtf32(0x26AA) }  # ⚪ default
    $clean = $Hex.TrimStart('#')
    if ($clean -notmatch '^[0-9a-fA-F]{6}$') { return [char]::ConvertFromUtf32(0x26AA) }

    $r = [Convert]::ToInt32($clean.Substring(0, 2), 16)
    $g = [Convert]::ToInt32($clean.Substring(2, 2), 16)
    $b = [Convert]::ToInt32($clean.Substring(4, 2), 16)

    # Representative hex for each circle. Codepoints from Unicode Symbols/Geometric Shapes:
    # 🔴 1F534, 🟠 1F7E0, 🟡 1F7E1, 🟢 1F7E2, 🔵 1F535, 🟣 1F7E3, 🟤 1F7E4, ⚫ 26AB, ⚪ 26AA
    $palette = @(
        @{ CP = 0x1F534; R = 0xE0; G = 0x21; B = 0x21 }   # red
        @{ CP = 0x1F7E0; R = 0xF1; G = 0x8C; B = 0x29 }   # orange
        @{ CP = 0x1F7E1; R = 0xF4; G = 0xD0; B = 0x36 }   # yellow
        @{ CP = 0x1F7E2; R = 0x4B; G = 0xA5; B = 0x32 }   # green
        @{ CP = 0x1F535; R = 0x29; G = 0x6E; B = 0xC8 }   # blue
        @{ CP = 0x1F7E3; R = 0x80; G = 0x40; B = 0xB0 }   # purple
        @{ CP = 0x1F7E4; R = 0x8B; G = 0x4F; B = 0x2D }   # brown
        @{ CP = 0x26AB;  R = 0x1F; G = 0x1F; B = 0x1F }   # black
        @{ CP = 0x26AA;  R = 0xEE; G = 0xEE; B = 0xEE }   # white
    )

    # HSV-style saturation = (max - min) / max. Low-sat inputs (near-gray) should snap
    # to black/white instead of the closest chromatic circle, which usually looks wrong.
    $mx = [Math]::Max($r, [Math]::Max($g, $b))
    $mn = [Math]::Min($r, [Math]::Min($g, $b))
    $sat = if ($mx -eq 0) { 0.0 } else { 1.0 - ($mn / $mx) }

    if ($sat -lt 0.15) {
        # Effectively gray — choose black vs white by brightness.
        return $(if ($mx -lt 128) { [char]::ConvertFromUtf32(0x26AB) } else { [char]::ConvertFromUtf32(0x26AA) })
    }

    $bestCp = 0x26AA
    $bestDist = [double]::MaxValue
    foreach ($p in $palette) {
        if ($p.CP -eq 0x26AB -or $p.CP -eq 0x26AA) { continue }  # skip grays for chromatic inputs
        $dr = $r - $p.R; $dg = $g - $p.G; $db = $b - $p.B
        $d = [double]($dr * $dr + $dg * $dg + $db * $db)
        if ($d -lt $bestDist) {
            $bestDist = $d
            $bestCp = $p.CP
        }
    }
    return [char]::ConvertFromUtf32($bestCp)
}

function Format-WtwPrettyNameWithCircle {
    <#
    .SYNOPSIS
        Prepend the color-circle emoji to a pretty name, dedup-safe.
    .DESCRIPTION
        If the name already starts with one of our circle emojis we replace
        it instead of stacking, so repeated renames don't accumulate prefixes.
    #>

    param(
        [Parameter(Mandatory)][string] $Hex,
        [string] $Name
    )

    $circle = Get-WtwColorCircleEmoji -Hex $Hex
    if ([string]::IsNullOrWhiteSpace($Name)) { return $circle }

    $stripped = Get-WtwNameWithoutColorCircle -Name $Name
    if ([string]::IsNullOrWhiteSpace($stripped)) { return $circle }
    return "$circle $stripped"
}