Private/VisualWidth.ps1

# Test Visual Width by using ANSI cursor position report (ESC[6n)
function script:Test-VisualWidth {
    param(
        [string]$Text,
        [int]$TimeoutMs = 300
    )
    $ESC = [char]27
    # Use ANSI cursor position report to determine the visual width of the text
    Write-Host -NoNewline "$ESC[8m$Text$ESC[0m$ESC[6n"

    $pos = ""
    $sw = [Diagnostics.Stopwatch]::StartNew()
    try {
        while ($sw.ElapsedMilliseconds -lt $TimeoutMs) {
            # Read cursor report response non-blockingly
            if ([System.Console]::KeyAvailable) {
                $key = [System.Console]::ReadKey($true)
                $char = $key.KeyChar
                $pos += $char
                if ($char -eq 'R') { break }
            }
            else {
                # If no input available, wait a bit before retrying to avoid busy loop
                Start-Sleep -Milliseconds 1
            }
        }
    }
    catch {}
    finally {
        # Ensure cursor is reset and traces are cleared on success, timeout, or exception
        Write-Host -NoNewline "$ESC[G$ESC[K"
        $sw.Stop()
    }

    # Parse the cursor report (should be in format ESC[row;colR)
    if ($pos -match ';(\d+)R') {
        # The column number in the cursor report indicates the visual width + 1 (because cursor is after the text)
        return [int]$matches[1] - 1
    }
    
    # If no coordinate matched, detection failed (timeout or exception)
    # In this case, we return $null to indicate failure,
    # and the caller can decide how to fallback (e.g. config or default value)
    return $null
}

function script:Test-AmbiguousAsWide {
    # Simple logic:
    # print an ambiguous width character (e.g. 'α')
    # and check how many cells it occupies
    $char = [char]0x03B1 # Greek letter Alpha
    $width = Test-VisualWidth -Text $char
    if ($null -ne $width) {
        return ($width -eq 2)
    }
    return $null
}

function script:Get-WTAmbiguousAsWide {
    # 1. Define possible config file paths (stable and preview)
    $paths = @(
        $localAppData = $env:LOCALAPPDATA
        "$localAppData\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json"
        "$localAppData\Packages\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\LocalState\settings.json"
        "$env:USERPROFILE\AppData\Local\Microsoft\Windows Terminal\settings.json"
    )
 
    foreach ($path in $paths) {
        if ([System.IO.File]::Exists($path)) {
            try {
                # 2. Read and parse JSON
                $settings = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8) | ConvertFrom-Json --ErrorAction Stop
                if ($null -eq $settings) { continue }
                # 3. Priority: Specific Profile -> Global Defaults -> Default False
                # Note: In WT, the property is "ambiguousUnicodeWidth", value is "wide" or "narrow"
                $widthSetting = $null
                if ($null -ne $settings.profiles.defaults.ambiguousUnicodeWidth) {
                    $widthSetting = $settings.profiles.defaults.ambiguousUnicodeWidth
                }
                # If found, return boolean value
                if ($widthSetting -eq "wide") { return $true }
                if ($widthSetting -eq "narrow") { return $false }
            }
            catch {
                continue # On parse failure, try next path
            }
        }
    }
    return $false # Default to False (Narrow)
}

function script:Get-InitialAmbiguousAsWide {
    # In VSCode integrated terminal, always return False,
    # as it does not support real-time cursor probing.
    if ($env:TERM_PROGRAM -eq "vscode" -or $Host.Name -match "Visual Studio Code Host") {
        return $false
    }

    # Read software config (Windows Terminal settings.json)
    # This step is the last fallback
    if ($null -ne $env:WT_SESSION) {
        $wtSetting = Get-WTAmbiguousAsWide
        if ($null -ne $wtSetting) { return $wtSetting }
    }

    # Real-time cursor probing (most accurate, now robust)
    $detected = Test-AmbiguousAsWide  # This is the improved function
    if ($null -ne $detected) {
        return $detected
    }

    # Environment variable (user explicit declaration has high priority)
    if ($null -ne $env:AMBIGUOUS_AS_WIDE) {
        return $env:AMBIGUOUS_AS_WIDE -in @('1', 'true', '$true')
    }

    # Final default value
    return $false 
}

function script:Test-ZWJSupport {
    # 👨‍👩‍👧‍👦 Family sequence (4 Emojis + 3 ZWJ)
    $familyEmoji = [char]::ConvertFromUtf32(0x1F468) + 
    [char]0x200D + 
    [char]::ConvertFromUtf32(0x1F469) + 
    [char]0x200D + 
    [char]::ConvertFromUtf32(0x1F467) + 
    [char]0x200D + 
    [char]::ConvertFromUtf32(0x1F466)

    $width = Test-VisualWidth -Text $familyEmoji
    if ($null -ne $width) {
        return @{
            Support = ($width -eq 2) # Proper ZWJ support if treated as single emoji (width 2)
            Width   = switch ($width) {
                2 { 0 } # Proper ZWJ support, treat as 0-width
                8 { 0 } # No ZWJ support, treat entire sequence as 2 separate emojis (4 chars * 2 width each)
                11 { 1 } # No ZWJ support, but counts each char as width 1 (e.g. some terminals), treat as 1-width
                Default { 0 } # Unexpected width, fallback to 0-width to avoid breaking layouts (best effort)
            }
        }
    }
    # If detection fails, assume no ZWJ support and treat as 0-width (safe fallback)
    return @{ Support = $false; Width = 0 }
}

function script:Get-InitialZWJSupport {
    # In VSCode integrated terminal, xterm.js has no ZWJ support,
    # but it treats ZWJ as zero-width (no visual effect, just concatenation).
    if ($env:TERM_PROGRAM -eq "vscode" -or $Host.Name -match "Visual Studio Code Host") {
        return @{ Support = $false; Width = 0; }
    }

    # Windows Terminal has good emoji support including ZWJ.
    if ($null -ne $env:WT_SESSION) {
        return @{ Support = $true; Width = 0; }
    }

    # Real-time probing on other terminals (most accurate)
    return Test-ZWJSupport
}

$script:AmbiguousAsWide = Get-InitialAmbiguousAsWide
$script:ZWJ = [PSCustomObject](Get-InitialZWJSupport)

Add-Type -AssemblyName System.Runtime.Caching
$script:WidthCache = [System.Runtime.Caching.MemoryCache]::Default

# # Define SGR (color) regex (ends with m)
$script:sgrRegex = [Regex]::new("\x1B\[[0-9;]*m", [System.Text.RegularExpressions.RegexOptions]::Compiled)
# Define ANSI regex to match all ANSI escape codes, including SGR and others (like cursor movement), for proper splitting
$script:ansiRegex = [Regex]::new("([\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~])))", [System.Text.RegularExpressions.RegexOptions]::Compiled)
# Define regex to detect full-width characters (CJK, Emoji, etc.)
$script:fullWidthRegex = [Regex]::new('[\u1100-\u11ff\u2e80-\ua4cf\uac00-\ud7af\uf900-\ufaff\ufe30-\ufe4f\uff00-\uffee]', [System.Text.RegularExpressions.RegexOptions]::Compiled)
# Define regex to detect Unicode 17 Emojis (for better emoji width handling)
$script:emojiRegex = [Regex]::new('[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E-\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED8\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])))?))?|\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3C-\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE8A\uDE8E-\uDEC2\uDEC6\uDEC8\uDECD-\uDEDC\uDEDF-\uDEEA\uDEEF]|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)', [System.Text.RegularExpressions.RegexOptions]::Compiled)
# Define AmbiguousWidth regex (includes Greek, Cyrillic, some symbols, etc.)
$script:ambigWidthRegex = [Regex]::new('[\u00a1\u00a4\u00a7\u00a8\u00aa\u00ad\u00ae\u00b0-\u00b4\u00b6\u00b7\u00b8\u00ba\u00bc-\u00be\u00bf\u00c6\u00d0\u00d7\u00d8\u00de-\u00e1\u00e6\u00e8-\u00ea\u00ec-\u00ed\u00f0\u00f2-\u00f3\u00f7-\u00fa\u00fc\u00fe\u0101\u0111\u0113\u011b\u0126\u012b\u0131-\u0133\u0138\u013f\u0141\u0142\u0144\u0148\u0149\u014a\u014d\u0152\u0153\u0166\u0167\u016b\u01ce\u01d0\u01d2\u01d4\u01d6\u01d8\u01da\u01dc\u0251\u0261\u02c4\u02c7\u02c9-\u02cb\u02cd\u02d0\u02d8\u02d9\u02da\u02db\u02dd\u02df\u0300-\u036f\u0391-\u03a9\u03b1-\u03c9\u0401\u0410-\u044f\u0451\u2010\u2013-\u2016\u2018\u2019\u201c\u201d\u2020-\u2022\u2024-\u2027\u2030\u2032\u2033\u2035\u203b\u203e\u2074\u207f\u2081-\u2084\u20ac\u2103\u2105\u2109\u2113\u2116\u2121\u2122\u2126\u212b\u2153-\u2154\u215b-\u215e\u2160-\u216b\u2170-\u217b\u2189\u2190-\u2199\u21b8\u21b9\u21d2\u21d4\u21e7\u2200\u2202\u2203\u2207\u2208\u220b\u220f\u2211\u2215\u221a\u221d-\u221f\u2220\u2223\u2225\u2227-\u222c\u222e\u2234-\u2237\u223d\u2248\u224c\u2252\u2260\u2261\u2264\u2265\u2266\u2267\u226a\u226b\u226e\u226f\u2282\u2283\u2286\u2287\u2295\u2299\u22a5\u22bf\u2312\u2460-\u24e9\u24eb-\u254b\u2550-\u2573\u2580-\u258f\u2592-\u2595\u25a0\u25a1\u25a3-\u25a9\u25b2\u25b3\u25b6\u25b7\u25bc\u25bd\u25c0\u25c1\u25c6-\u25c8\u25cb\u25ce-\u25d1\u25e2-\u25e5\u25ef\u2605\u2606\u2609\u260e\u260f\u2614\u2615\u261c\u261e\u2640\u2642\u2660\u2661\u2663-\u2665\u2667-\u266a\u266c-\u266d\u266f\u273d\u2776-\u277f\ue000-\uf8ff\ufe00-\ufe0f\ufffd]', [System.Text.RegularExpressions.RegexOptions]::Compiled)
function script:Get-VisualElementsAndWidths {
    param([string]$Text)

    $elements = [System.Collections.Generic.List[string]]::new()
    $widths = [System.Collections.Generic.List[int]]::new()

    if ([string]::IsNullOrEmpty($Text)) { return $elements, $widths }

    $cacheKey = "E_$($script:ZWJ.Support)_$($script:ZWJ.Width)_$($script:AmbiguousAsWide)_$Text"
    $cached = $script:WidthCache.Get($cacheKey)

    if ($null -ne $cached) {
        return $cached.elements, $cached.widths
    }

    # Use regex Split to iterate, only keep ANSI codes with color features
    # Split text by all ANSI codes
    $parts = $script:ansiRegex.Split($Text)

    foreach ($part in $parts) {
        if ([string]::IsNullOrEmpty($part)) { continue }

        # If it's an ANSI code
        if ($script:ansiRegex.IsMatch($part)) {
            # Only keep SGR (color/bold) codes as 0-width elements, discard others
            if ($script:sgrRegex.IsMatch($part)) {
                $elements.Add($part)
                $widths.Add(0)
            }
            continue
        }

        # --- Normal text processing ---
        $it = [System.Globalization.StringInfo]::GetTextElementEnumerator($part)
        while ($it.MoveNext()) {
            $char = $it.GetTextElement()
            # ZWJ logic
            if ($script:ZWJ.Support -and $elements.Count -gt 0 -and (([char[]]$elements[-1])[-1] -eq 0x200D -or ([char[]]$char)[0] -eq 0x200D)) {
                $elements[$elements.Count - 1] += $char
            }
            elseif (-not $script:ZWJ.Support -and $char.Contains([char]0x200D)) {
                $emojiParts = $char -split [char]0x200D
                for ($i = 0; $i -lt $emojiParts.Length; $i++) {
                    if ($emojiParts[$i][0] -eq 0x200D) {
                        $elements[$elements.Count - 1] += $emojiParts[$i] # Append ZWJ to previous element
                        # If ZWJ is not supported, treat the ZWJ character itself according to the configured width (0 or 1)
                        $widths[$elements.Count - 1] += $script:ZWJ.Width
                    }
                    else {
                        $elements.Add($emojiParts[$i]) # Add the emoji part as a new element
                        $widths.Add(2) # Emoji parts are treated as width 2 regardless of ZWJ support, to avoid breaking layouts (best effort)
                    }
                }
            }
            else {
                $elements.Add($char)
                if ($script:fullWidthRegex.IsMatch($char)) {
                    # Full-width characters (CJK, Emoji, etc.) treated as width 2
                    $widths.Add(2)
                }
                elseif ($char.Contains([char]0xFE0F)) {
                    # Emoji variation selector (ZWJ sequence) treated as width 2
                    $widths.Add(2)
                }
                elseif ($char.Contains([char]0xFE0E)) {
                    # Text variation selector treated as width 1
                    $widths.Add(1)
                }
                elseif ($script:AmbiguousAsWide -and $script:ambigWidthRegex.IsMatch($char)) {
                    # Ambiguous width characters (e.g., Greek, Cyrillic) treated as width 2 if global flag is set
                    $widths.Add(2)
                }
                elseif ($script:emojiRegex.IsMatch($char)) {
                    # Emoji characters treated as width 2
                    $widths.Add(2)
                }
                else {
                    # Normal ASCII and combining marks
                    # Combining marks may have .Length > 1, but visually follow the previous character, width is 1
                    $widths.Add(1)
                }
            }
        }
    }

    # Cache the result with a sliding expiration of 5 minutes
    $policy = New-Object System.Runtime.Caching.CacheItemPolicy
    $policy.SlidingExpiration = [TimeSpan]::FromMinutes(5)

    $cacheEntry = @{
        elements = $elements.ToArray()
        widths   = $widths.ToArray()
    }

    $script:WidthCache.Set($cacheKey, $cacheEntry, $policy)

    return $cacheEntry.elements, $cacheEntry.widths
}

function global:Get-VisualWidth {
    <#
    .SYNOPSIS
        Calculates the visual width of a string.
    .PARAMETER Text
        The string for which to calculate the visual width.
    .OUTPUT
        An integer representing the visual width of the string.
    #>

    param([string]$Text)
    $elements, $widths = Get-VisualElementsAndWidths -Text $Text
    $totalWidth = 0
    foreach ($w in $widths) { $totalWidth += $w }
    return $totalWidth
}

function script:VisualWidthPad {
    <#
    .SYNOPSIS
        Pads a string to a specific visual width.
    .PARAMETER Alignment
        -1: Left (Pad right)
         0: Center
         1: Right (Pad left)
    #>

    param(
        [string]$Text,
        [int]$Width,
        [int]$Alignment
    )

    $currentWidth = Get-VisualWidth -Text $Text
    
    $padTotal = $Width - $currentWidth
    if ($padTotal -le 0) { return $Text }

    switch ($Alignment) {
        -1 { 
            # Left: Text + Spaces
            return $Text + (" " * $padTotal) 
        }
        0 { 
            # Center: HalfSpaces + Text + HalfSpaces
            $leftPad = [Math]::Floor($padTotal / 2)
            $rightPad = $padTotal - $leftPad
            return (" " * $leftPad) + $Text + (" " * $rightPad)
        }
        1 { 
            # Right: Spaces + Text
            return (" " * $padTotal) + $Text 
        }
    }
}

function script:vPadLeft {
    param([string]$Text, [int]$Width)
    return VisualWidthPad -Text $Text -Width $Width -Alignment 1
}

function script:vPadCenter {
    param([string]$Text, [int]$Width)
    return VisualWidthPad -Text $Text -Width $Width -Alignment 0
}

function script:vPadRight {
    param([string]$Text, [int]$Width)
    return VisualWidthPad -Text $Text -Width $Width -Alignment -1
}

$script:MergeSlashRegex = [Regex]::new('\/+((' + $script:sgrRegex + ')*\/+)+', [System.Text.RegularExpressions.RegexOptions]::Compiled)
$script:TrailingSlashRegex = [Regex]::new('\/(' + $script:sgrRegex + ')*$', [System.Text.RegularExpressions.RegexOptions]::Compiled)
$script:TrailingColorRegex = [Regex]::new('(.*)(' + $script:sgrRegex + ')$', [System.Text.RegularExpressions.RegexOptions]::Compiled -bor [System.Text.RegularExpressions.RegexOptions]::Singleline)

function script:Format-DirName {
    param ([string]$Text)
    if ([string]::IsNullOrEmpty($Text)) { return "/" }

    # 1. Path normalization: replace backslashes with slashes, merge consecutive slashes
    $Text = $Text.Replace('\', '/')
    # 2. Merge consecutive slashes, also merge if color codes are between slashes (e.g., "///", "/\e[31m/\e[0m/")
    $Text = $script:MergeSlashRegex.Replace($Text, '/')
    # 3. Ensure there is a trailing slash, or a slash before trailing color code
    if ($script:TrailingSlashRegex.IsMatch($Text)) {
        return $Text
    }

    # 4. If there is a trailing color code but no slash, add a slash before the color code
    $match = $script:TrailingColorRegex.Match($Text)
    if ($match.Success) {
        return $match.Groups[1].Value + "/" + $match.Groups[2].Value
    }
    # 5. Otherwise, just add a slash
    return $Text + "/"
}

function script:VisualWidthTruncate {
    <#
    .SYNOPSIS
        Intelligent semantic truncation for file names and paths.
        Supports CJK characters, Emojis (ZWJ), and extension preservation.
    .PARAMETER Text
        The original string to truncate.
    .PARAMETER MaxWidth
        The maximum visual width (ASCII = 1, CJK/Emoji = 2).
    .PARAMETER Mode
        0: File mode (Preserve extension).
        1: Directory mode (Add trailing slash).
        2: Raw mode (Internal use for recursive base name truncation).
        3: Force truncate (Ignore extension).
    #>

    param(
        [string]$Text,
        [int]$MaxWidth,
        [int]$Mode = 0
    )
    
    if ($Mode -eq 1) { $Text = Format-DirName -Text $Text }
    elseif ($Mode -eq 0) { $Text = $Text.Replace('\', '/') }

    if ($MaxWidth -lt 0) { return $Text }

    $elements, $widths = Get-VisualElementsAndWidths -Text $Text

    $totalWidth = 0
    foreach ($width in $widths) { $totalWidth += $width }

    # If within limit, return original (with optional slash)
    if ($totalWidth -le $MaxWidth) { return $Text }
 
    # --- Mode Handling ---
    
    # File Mode: Preserve extension
    if ($Mode -eq 0) {        
        $lastDotIndex = $Text.LastIndexOf('.')
        $lastSlashIndex = $Text.LastIndexOf('/')
        if ($lastDotIndex -gt $lastSlashIndex) {
            $base = $Text.Substring(0, $lastDotIndex)
            $ext = $Text.Substring($lastDotIndex)
        }
        else {
            $base = $Text
            $ext = ""
        }

        # If extension itself is too long, fallback to force truncate
        $limitForBase = $MaxWidth - (Get-VisualWidth -Text $ext)
        if ($limitForBase -lt 3) {
            return VisualWidthTruncate -Text $Text -MaxWidth $MaxWidth -Mode 3
        }
        
        # Recursively truncate the base name and append extension
        return (VisualWidthTruncate -Text $base -MaxWidth $limitForBase -Mode 2) + $ext
    }

    # --- Truncation Logic ---
    # Reserve space for dots: Mode 1 (dir) needs at least 2 dots + '/', Mode 2 (internal) needs 1 dot, others 2 dots
    $reserve = switch ($Mode) {
        1 { 3 } # "../"
        2 { 1 } # "."
        default { 2 } # ".."
    }
    
    $limit = $MaxWidth - $reserve
    $result = ""
    $currentWidth = 0
    $i = 0

    # Add beginning SGR (color/bold) codes
    for (; $i -lt $elements.Count; $i++) {
        if ($widths[$i] -gt 0) { break }
        $result += $elements[$i]
    }

    for (; $i -lt $elements.Count; $i++) {
        if ($currentWidth + $widths[$i] -gt $limit) { break }
        $result += $elements[$i]
        $currentWidth += $widths[$i]
    }
    
    # --- Precision Padding with Dots ---
    $dotCount = $MaxWidth - $currentWidth
    if ($Mode -eq 1) {
        $result += ("." * ($dotCount - 1)) + "/"
    }
    else {
        $result += ("." * $dotCount)
    }

    # Add remaining SGR (color/bold) codes
    for (; $i -lt $elements.Count; $i++) {
        if ($widths[$i] -gt 0) { continue }
        $result += $elements[$i]
    }
    return $result
}

function global:Format-VisualWidthString {
    <#
    .SYNOPSIS
        Unified entry point for visual width operations.
    .PARAMETER Text
        The string to format.
    .PARAMETER VisualWidth
        The target visual width for the output string.
    .PARAMETER Mode
        Available: PadLeft, PadCenter, PadRight, TruncateFile, TruncateDir
    .OUTPUT
        A string that has been padded or truncated to fit the specified visual width, according to the selected mode.
    .EXAMPLE
        Format-VisualWidthString -Text "👨‍👩‍👧‍👦中国家庭.txt" -VisualWidth 20 -Mode PadRight
        Output: "👨‍👩‍👧‍👦中国家庭.txt " on support ZWJ term (treating 👨‍👩‍👧‍👦 as width 2)
        Output: "👨‍👩‍👧‍👦中国家庭.txt" on non-support ZWJ term (treating 👨‍👩‍👧‍👦 as width 8 or 11)
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$Text,
        
        [Parameter(Mandatory = $true)]
        [int]$VisualWidth,
        
        [Parameter(Mandatory = $true)]
        [ValidateSet("PadLeft", "PadCenter", "PadRight", "TruncateFile", "TruncateDir")]
        [string]$Mode
    )
    switch ($Mode) {
        "PadLeft" { return VisualWidthPad -Text $Text -Width $VisualWidth -Alignment -1 }
        "PadCenter" { return VisualWidthPad -Text $Text -Width $VisualWidth -Alignment 0 }
        "PadRight" { return VisualWidthPad -Text $Text -Width $VisualWidth -Alignment 1 }
        "TruncateFile" { return VisualWidthTruncate -Text $Text -MaxWidth $VisualWidth -Mode 0 }
        "TruncateDir" { return VisualWidthTruncate -Text $Text -MaxWidth $VisualWidth -Mode 1 }
    }
}