Private/Rendering.ps1

# ShowTree\Private\Rendering.ps1

#region TreeItem Rendering
<#
.SYNOPSIS
    Renders a single file or directory entry.
 
.DESCRIPTION
    Handles:
      • Connector selection
      • Style/color application
      • Reparse target display
      • Attribute debug output
      • Recursion into subdirectories
      • Gap-state reset
#>

function Write-TreeItem {
    param(
        [Parameter(Mandatory)]
        $Item,

        [Parameter(Mandatory)]
        [ValidateSet('File','Directory')]
        [string]$Type,

        [ValidateSet('Normal','Tree','List')]
        [string]$Mode = 'Normal',

        # Prefix inherited from parent
        [string]$Prefix = "",

        # Whether this item is the last sibling
        [bool]$IsLast,

        # Mode switches
        [switch]$Ascii,
        [switch]$Colorize,
        [switch]$ShowTargets,
        [switch]$DebugAttributes,
        [switch]$Recurse,

        # Whether to suppress file connector span
        [bool]$NoSpan = $false,

        # Recursion state
        [int]$MaxDepth,
        [int]$CurrentDepth,

        # Additional flags
        [switch]$IncludeFiles,
        [string[]]$Exclude,
        [string[]]$Include,        
        [switch]$Gap,
        [switch]$HideHidden,
        [switch]$HideSystem
    )

    # Compute connector for this item
    $connector = Get-Connector `
        -Type   $Type `
        -Mode   $Mode `
        -Ascii:$Ascii `
        -IsLast $IsLast `
        -NoSpan $NoSpan

    # Compute style
    $style = Get-ItemStyle -Item $Item -Colorize:$Colorize

    #
    # Reparse target resolution
    #
    $target = $null
    if ($ShowTargets -and ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
        $info = Get-Item -LiteralPath $Item.FullName -Force -ErrorAction SilentlyContinue
        if ($info -and $info.PSObject.Properties.Match('Target')) {
            $target = $info.Target
        }
    }

    #
    # Output formatting
    #
    $esc   = [char]27
    $reset = $Colorize ? "${esc}[0m"  : ""
    $dim   = $Colorize ? "${esc}[90m" : ""

    $targetText = $target ? " ${dim}->${reset} $target" : ""

    # Optional attribute debug
    $debug = ""
    if ($DebugAttributes) {
        $styleName = $style.Name
        $attrHex   = ('0x{0:X8}' -f [uint32]$Item.Attributes)
        $attrNames = $Item.Attributes.ToString()
        $debug     = " [$attrHex $attrNames | $styleName]"
    }

    Write-Output "${dim}${Prefix}${dim}${connector}$($style.Ansi)$($Item.Name)$reset$targetText$debug"

    #
    # Reset gap state unless tail gap was printed
    #
    if ($script:GapState.LastGapMode -ne [GapMode]::Tail) {
        $script:GapState.LastGapMode = [GapMode]::None
    }

    #
    # Recursion into subdirectories
    #
    if ($Recurse -and
        $Type -eq 'Directory' -and
        -not ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {

        # Build next-level prefix
        $newPrefix = $Prefix + (Get-Connector `
            -Type   Prefix `
            -Mode   $Mode `
            -Ascii:$Ascii `
            -IsLast $IsLast)

        # Recurse
        Show-TreeInternal `
            -Path          $Item.FullName `
            -Mode          $Mode `
            -MaxDepth      $MaxDepth `
            -Colorize:$Colorize `
            -IncludeFiles:$IncludeFiles `
            -HideHidden:$HideHidden `
            -HideSystem:$HideSystem `
            -ShowTargets:$ShowTargets `
            -Exclude      $Exclude `
            -Include      $Include `
            -Gap:$Gap `
            -Ascii:$Ascii `
            -DebugAttributes:$DebugAttributes `
            -CurrentDepth  ($CurrentDepth + 1) `
            -Prefix        $newPrefix `
            -IsLastParent  $IsLast
    }

    #
    # Reset gap state again after recursion
    #
    if ($script:GapState.LastGapMode -ne [GapMode]::Tail) {
        $script:GapState.LastGapMode = [GapMode]::None
    }
}
#endregion

#region Gap Rendering
<#
.SYNOPSIS
    Writes a gap line between blocks.
 
.DESCRIPTION
    Handles Internal, Tail, and Sibling gap modes.
    Updates the global gap-state machine.
#>

function Write-Gap {
    param(
        $colorGap,
        $Prefix,
        $GapConnector,
        $colorReset,
        [GapMode]$Mode
    )

    $connector = $GapConnector ? $GapConnector : ""
    Write-Output "${colorGap}${Prefix}${connector}${colorReset}"
    $script:GapState.LastGapMode = $Mode
}

#region Connector Rendering
<#
.SYNOPSIS
    Returns the connector string for a given item type.
 
.DESCRIPTION
    Handles:
      • Tree.com ASCII mode
      • Unicode graphical mode
      • Prefix vs File vs Directory vs Gap
      • Last-sibling logic
      • NoSpan suppression for Tree.com file connectors
#>

function Get-Connector {
    param(
        [Parameter(Mandatory)]
        [ValidateSet('File','Directory','Gap','Prefix')]
        [string]$Type,

        [ValidateSet('Normal','Tree','List')]
        [string]$Mode = 'Normal',

        [switch]$Ascii,

        [bool]$IsLast = $false,
        [bool]$NoSpan = $false
    )

    #
    # Listing mode: indentation only
    #
    if ($Mode -eq 'List') {
        return ' '
    }

    #
    # Tree.com compatibility mode
    #
    if ($Mode -eq 'Tree') {
        if ($Type -eq 'File' -and $NoSpan) {
            return ' '
        }

        switch ($Type) {
            'File'      { return $Ascii ? '| '  : '│ ' }
            'Directory' {
                if ($IsLast) { return $Ascii ? '\---' : '└───' }
                else         { return $Ascii ? '+---' : '├───' }
            }
            'Gap'       { return $Ascii ? '|'    : '│' }
            'Prefix'    {
                if ($IsLast) { return ' ' }
                else         { return $Ascii ? '| ' : '│ ' }
            }
        }
    }

    #
    # Graphical Unicode mode (Show-Tree default)
    #
    switch ($Type) {
        'File' {
            if ($IsLast) { return $Ascii ? '\-- ' : '╙── ' }
            else         { return $Ascii ? '+-- ' : '╟── ' }
        }

        'Directory' {
            if ($IsLast) { return $Ascii ? '\== ' : '╚══ ' }
            else         { return $Ascii ? '+== ' : '╠══ ' }
        }

        'Gap' {
            return $Ascii ? '|' : '║'
        }

        'Prefix' {
            if ($IsLast) { return ' ' }
            else         { return $Ascii ? '| ' : '║ ' }
        }
    }
}
#endregion

#region Style Rendering
<#
.SYNOPSIS
    Computes the ANSI style for a file or directory.
 
.DESCRIPTION
    Applies:
      • Base style (directory/file/symlink/junction)
      • Attribute overlays (hidden, system, temporary, etc.)
      • Foreground overrides
      • Combined ANSI escape sequence
 
    Returns an object:
      @{ Name = "..."; Ansi = "..."; }
#>

function Get-ItemStyle {
    param(
        $Item,
        $Colorize,
        $StyleProfile = $script:StyleProfile
    )

    $esc = [char]27

    $isDir     = $Item.PSIsContainer
    $attrs     = $Item.Attributes
    $isReparse = [bool]($attrs -band [IO.FileAttributes]::ReparsePoint)

    #
    # Determine base style
    #
    if ($isReparse -and $isDir) {
        $styleName = "Junction"
        $base      = $StyleProfile.Base.Junction
    }
    elseif ($isReparse -and -not $isDir) {
        $styleName = "Symlink"
        $base      = $StyleProfile.Base.File
    }
    elseif ($isDir) {
        $styleName = "Directory"
        $base      = $StyleProfile.Base.Directory
    }
    else {
        $styleName = "File"
        $base      = $StyleProfile.Base.File
    }

    #
    # No color mode
    #
    if (-not $Colorize) {
        return [PSCustomObject]@{
            Name = $styleName
            Ansi = ""
        }
    }

    #
    # Parse base style codes
    #
    $codes = @() + ($base -split ';' | Where-Object { $_ -ne '' })

    # Extract foreground codes (30–37, 90–97)
    $fg    = @() + ($codes | Where-Object { $_ -match '^(3[0-7]|9[0-7])$' })
    $codes = @() + ($codes | Where-Object { $_ -notmatch '^(3[0-7]|9[0-7])$' })

    #
    # Apply attribute overlays
    #
    foreach ($flag in Get-SetFileAttributes $attrs) {
        $flagName = $flag.ToString()

        if ($StyleProfile.Attributes.ContainsKey($flagName)) {
            $overlay = $StyleProfile.Attributes[$flagName]

            # Add overlay attributes
            if ($overlay.Attributes) {
                $codes += ($overlay.Attributes -split ';')
            }

            # Foreground override
            if ($overlay.OverrideForeground) {
                if ($overlay.OverrideForeground -is [string]) {
                    $fg = $overlay.OverrideForeground
                }
                elseif ($overlay.OverrideForeground.ContainsKey($styleName)) {
                    $fg = $overlay.OverrideForeground[$styleName]
                }
            }
        }
    }

    #
    # Build final ANSI sequence
    #
    $final = @()
    if ($fg) { $final += $fg }
    $final += $codes

    $ansi = "${esc}[$($final -join ';')m"

    [PSCustomObject]@{
        Name = $styleName
        Ansi = $ansi
    }
}
#endregion