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 } } } |