Display.ps1

# 显示工具:中文宽度 / 对齐 / 截断(East Asian Width 手动判区间,.NET 无内置支持)

function Get-DisplayWidth {
    param([string]$Text)
    $width = 0
    foreach ($rune in $Text.EnumerateRunes()) {
        $cp = [int]$rune.Value
        # 零宽:组合标记、ZWJ/ZWNJ、变体选择符、emoji 肤色修饰符
        if (($cp -ge 0x0300 -and $cp -le 0x036F) -or ($cp -ge 0x200B -and $cp -le 0x200F) -or
            ($cp -ge 0x2028 -and $cp -le 0x202E) -or ($cp -ge 0x2060 -and $cp -le 0x206F) -or
            ($cp -ge 0x20D0 -and $cp -le 0x20FF) -or ($cp -ge 0xFE00 -and $cp -le 0xFE0F) -or
            ($cp -ge 0xFE20 -and $cp -le 0xFE2F) -or ($cp -ge 0x1F3FB -and $cp -le 0x1F3FF) -or
            ($cp -ge 0xE0100 -and $cp -le 0xE01EF)) { continue }
        # 宽字符(East Asian Width = W/F)
        $isWide = ($cp -ge 0x1100 -and $cp -le 0x115F) -or ($cp -ge 0x2E80 -and $cp -le 0xA4CF) -or
                  ($cp -ge 0xAC00 -and $cp -le 0xD7A3) -or ($cp -ge 0xF900 -and $cp -le 0xFAFF) -or
                  ($cp -ge 0xFE30 -and $cp -le 0xFE4F) -or ($cp -ge 0xFF00 -and $cp -le 0xFF60) -or
                  ($cp -ge 0xFFE0 -and $cp -le 0xFFE6) -or ($cp -ge 0x1F300 -and $cp -le 0x1F64F) -or
                  ($cp -ge 0x1F900 -and $cp -le 0x1F9FF) -or ($cp -ge 0x20000 -and $cp -le 0x3FFFD)
        if ($isWide) { $width += 2 } else { $width += 1 }
    }
    return $width
}

function Pad-DisplayRight {
    param([string]$Text, [int]$Width, [char]$PadChar = ' ')
    $pad = $Width - (Get-DisplayWidth $Text); if ($pad -lt 0) { $pad = 0 }
    return $Text + ($PadChar.ToString() * $pad)
}

function Pad-DisplayLeft {
    param([string]$Text, [int]$Width, [char]$PadChar = ' ')
    $pad = $Width - (Get-DisplayWidth $Text); if ($pad -lt 0) { $pad = 0 }
    return ($PadChar.ToString() * $pad) + $Text
}

function Truncate-Display {
    param([string]$Text, [int]$MaxWidth, [string]$Ellipsis = '…')
    if ((Get-DisplayWidth $Text) -le $MaxWidth) { return $Text }
    $budget = $MaxWidth - (Get-DisplayWidth $Ellipsis)
    $result = ''; $w = 0
    foreach ($rune in $Text.EnumerateRunes()) {
        $rw = Get-DisplayWidth $rune.ToString()
        if ($w + $rw -gt $budget) { break }
        $result += $rune.ToString(); $w += $rw
    }
    return $result + $Ellipsis
}

# 会话项显示名(决策 48 + 第八轮去重):「标题(目录名)」;标题与目录名
# 归一化后相同(忽略空白/_/-/. 和大小写)则省略括号——「CC汉化」目录 CC_汉化 → 只显示「CC汉化」
function Get-CctSessionLabel {
    param([string]$Name, [string]$Path)
    $dir = Split-Path -Leaf $Path
    if ($dir -eq $Path) { return $Name }   # 根路径无父目录(Split-Path -Leaf 'E:\' 返回自身)
    $norm = [regex]::Replace($Name, '[\s_\-\.]', '').ToLowerInvariant()
    $normDir = [regex]::Replace($dir, '[\s_\-\.]', '').ToLowerInvariant()
    if ($norm -eq $normDir -or [string]::IsNullOrEmpty($normDir)) { return $Name }
    return "$Name($dir)"
}

# 相对时间(决策 17:刚刚/N分钟前/N小时前/昨天/N天前/MM-dd)
function Get-RelativeTime {
    param([datetime]$Time, [datetime]$Now)
    $span = $Now - $Time
    if ($span.TotalMinutes -lt 5) { return '刚刚' }
    if ($span.TotalHours -lt 1)   { return "$([int][Math]::Ceiling($span.TotalMinutes))分钟前" }
    if ($span.TotalHours -lt 24)  { return "$([int][Math]::Floor($span.TotalHours))小时前" }
    if ($span.TotalDays -lt 2)    { return '昨天' }
    if ($span.TotalDays -le 7)    { return "$([int][Math]::Floor($span.TotalDays))天前" }
    return $Time.ToString('MM-dd')
}

# 路径尾部(决策 18/19 + 47 修订:默认 3 级,冲突时逐级加深直到区分,路径耗尽即停)
function Get-TailPaths {
    param([string[]]$Paths)
    $levels = @{}
    foreach ($p in ($Paths | Select-Object -Unique)) { $levels[$p] = 3 }
    while ($true) {
        $tails = @{}
        foreach ($p in $levels.Keys) { $tails[$p] = (Get-PathTail $p $levels[$p]) }
        # 找出有冲突的尾部值,冲突组内能加深级的加深;全部不能再加就退出
        $conflicted = @($tails.Values | Group-Object | Where-Object Count -gt 1 | ForEach-Object Name)
        if ($conflicted.Count -eq 0) { break }
        $extended = $false
        foreach ($p in @($tails.Keys | Where-Object { $conflicted -contains $tails[$_] })) {
            if ((Get-PathDepth $p) -gt $levels[$p]) { $levels[$p]++; $extended = $true }
        }
        if (-not $extended) { break }
    }
    $result = @{}
    foreach ($p in $levels.Keys) { $result[$p] = (Get-PathTail $p $levels[$p]) }
    return $result
}

function Get-PathTail {
    param([string]$Path, [int]$Levels)
    $parts = $Path -split '[\\/]' | Where-Object { $_ -ne '' }
    $n = [Math]::Min($Levels, $parts.Count)
    return (($parts | Select-Object -Last $n) -join '/')
}

function Get-PathDepth {
    param([string]$Path)
    return @(($Path -split '[\\/]' | Where-Object { $_ -ne '' })).Count
}