Public/Show-Tree.ps1

# Show-Tree\Public\Show-Tree.ps1

#region Style Profile
<#
.SYNOPSIS
    Defines the ANSI color style profile used by Show-Tree.
 
.DESCRIPTION
    This profile controls:
      • Base colors for files, directories, symlinks, junctions
      • Attribute overlays (Hidden, System, Temporary, etc.)
      • Foreground overrides for specific attribute/type combinations
 
    The profile is consumed by Get-ItemStyle in Show-TreeInternal.ps1.
#>

$script:StyleProfile = @{
    Base = @{
        File      = "37"
        Directory = "36"
        Symlink   = "37"
        Junction  = "36"
    }
    Attributes = @{
        None              = @{ Attributes = "90" }
        ReadOnly          = @{ Attributes = "3" }
        Hidden            = @{ Attributes = "2" }
        System            = @{
            OverrideForeground = @{
                File      = "31"
                Directory = "35"
            }
        }
        Directory         = @{ Attributes = "" }
        Archive           = @{ Attributes = "" }
        Device            = @{ Attributes = "" }
        Normal            = @{ Attributes = "" }
        Temporary         = @{ Attributes = "7" }
        SparseFile        = @{ Attributes = "7" }
        ReparsePoint      = @{ Attributes = "4" }
        Compressed        = @{ Attributes = "" }
        Offline           = @{ Attributes = "7" }
        NotContentIndexed = @{ Attributes = "" }
        Encrypted         = @{ Attributes = "" }
        IntegrityStream   = @{ Attributes = "" }
        NoScrubData       = @{ Attributes = "" }
    }
}
#endregion

#region Public Entry Point
<#
.SYNOPSIS
    Displays a directory tree in Normal, Tree.com-compatible, or Listing mode.
 
.DESCRIPTION
    Show-Tree renders directory structures using one of three modes:
 
      • Normal - Modern, colorized output with Unicode connectors (default)
      • Tree - DOS tree.com compatibility mode
      • List - Indentation-only listing mode
 
    The active mode is determined by:
      • -Mode Normal|Tree|List
      • -Tree or -List (backward-compatible aliases)
      • Defaulting to Normal when no mode is specified
 
    After resolving the mode, Show-Tree computes effective settings for:
      • Depth (MaxDepth, Recurse)
      • Colorization (Color, Mono)
      • File visibility (Files, NoFiles)
      • Hidden/system filtering (ShowHidden/HideHidden, ShowSystem/HideSystem)
      • Reparse point targets (ShowTargets/NoTargets)
      • Gap lines (NoGap)
      • ASCII vs Unicode connectors (Ascii)
 
    Paired switches (e.g., Color/Mono, Files/NoFiles) are mutually exclusive.
    If both halves of a pair are supplied, an error is raised.
 
    Once effective settings are computed, rendering is delegated to
    Show-TreeInternal, which performs recursion, gap logic, connector selection,
    and formatting.
 
.PARAMETER Path
    The root path to display. Defaults to the current directory.
 
.PARAMETER Mode
    Explicitly selects the output mode: Normal, Tree, or Listing.
    Defaults to Normal.
 
.PARAMETER Tree
    Backward-compatible alias for: -Mode Tree
 
.PARAMETER List
    Backward-compatible alias for: -Mode Listing
 
.PARAMETER MaxDepth
    Maximum recursion depth. -1 means unlimited.
 
.PARAMETER Recurse
    Shortcut for unlimited depth (equivalent to -MaxDepth -1).
 
.PARAMETER Mono
    Disable color output (Normal and Listing modes).
 
.PARAMETER Color
    Enable color output. In Tree mode, this matches tree.com behavior.
    In Normal mode, this simply forces color on.
 
.PARAMETER NoFiles / Files
    Control whether files are included. Files is Tree-only; NoFiles applies to
    Normal and Listing modes.
 
.PARAMETER HideHidden / ShowHidden
    Control visibility of hidden items.
 
.PARAMETER HideSystem / ShowSystem
    Control visibility of system items.
 
.PARAMETER Include
    Glob patterns that explicitly include matching items.
    Exact matches override all other filtering rules.
    Glob matches resurrect items removed by Hidden, System, or Exclude (glob).
 
.PARAMETER Exclude
    Glob patterns that remove matching items.
    Exact matches override Include (glob).
    Glob matches are overridden by Include (exact or glob).
 
.PARAMETER NoGap
    Disable gap lines between blocks (Normal and Tree modes).
 
.PARAMETER NoTargets / ShowTargets
    Control whether reparse point targets are shown.
 
.PARAMETER Ascii
    Use ASCII connectors instead of Unicode.
 
.PARAMETER DebugAttributes
    Show attribute debug info for each item.
 
.PARAMETER Legend
    Display a color legend instead of rendering a tree.
 
.EXAMPLE
    Show-Tree C:\
 
.EXAMPLE
    Show-Tree -Tree C:\Windows
 
.EXAMPLE
    Show-Tree -List -Recurse
 
.NOTES
    Author: Ryan Beesley
    Version: 1.2.0
    Last Updated: April 2026
#>

function Show-Tree {
    [CmdletBinding()]
    param(
        #
        # MODE SELECTION
        #
        [ValidateSet('Normal','Tree','List')]
        [string]$Mode = 'Normal',

        # Backward-compatible aliases
        [Alias('Tree')]
        [switch]$AsTree,

        [Alias('List','Listing')]
        [switch]$AsListing,


        #
        # PATH
        #
        [Parameter(Position = 0)]
        [string]$Path = ".",


        #
        # MODE-SPECIFIC SWITCHES
        #

        # Colorization
        [switch]$Color,      # Tree
        [Alias('NoColor')]
        [switch]$Mono,       # Normal/Listing

        # Files
        [switch]$Files,      # Tree
        [switch]$NoFiles,    # Normal/Listing

        # Hidden
        [switch]$ShowHidden, # Tree
        [switch]$HideHidden, # Normal/Listing

        # System
        [switch]$ShowSystem, # Tree
        [switch]$HideSystem, # Normal/Listing

        # Reparse targets
        [switch]$ShowTargets, # Listing
        [switch]$NoTargets,   # Normal/Tree

        # Gap lines
        [switch]$NoGap,       # Normal/Tree

        # Depth
        [Alias('Depth')]
        [int]$MaxDepth,
        [switch]$Recurse,

        # ASCII connectors
        [switch]$Ascii,

        # Debugging
        [switch]$DebugAttributes,

        # Show the color legend
        [switch]$Legend
    )

    #
    # Legend mode: no tree rendering
    #
    if ($Legend) {
        Show-TreeLegend
        return
    }

    #
    # Resolve Mode (explicit or implied)
    #
    if ($AsTree)    { $Mode = 'Tree' }
    if ($AsListing) { $Mode = 'List' }

    #
    # Validate paired switches
    #
    if ($Color -and $Mono) {
        throw "Cannot specify both -Color and -Mono."
    }

    if ($Files -and $NoFiles) {
        throw "Cannot specify both -Files and -NoFiles."
    }

    if ($ShowHidden -and $HideHidden) {
        throw "Cannot specify both -ShowHidden and -HideHidden."
    }

    if ($ShowSystem -and $HideSystem) {
        throw "Cannot specify both -ShowSystem and -HideSystem."
    }

    if ($ShowTargets -and $NoTargets) {
        throw "Cannot specify both -ShowTargets and -NoTargets."
    }

    #
    # Resolve the path (with proper error record)
    #
    $Path = Resolve-TreePath -Path $Path -Mode $Mode
    if (-not $Path) { return }

    # Tree Mode: Output should follow `tree.com` output
    if ($Mode -eq 'Tree') {

        # Extract drive letter
        $drive = Split-Path $Path -Qualifier
        $driveName = $drive.TrimEnd(':')

        # 1. Invalid drive → tree.com behavior
        if (-not (Get-PSDrive -Name $driveName -ErrorAction SilentlyContinue)) {
            Write-Output "Invalid drive specification"
            return
        }

        # 2. Valid drive → print header
        $nearestExistingParent = Get-NearestExistingParent -Path $Path
        $fileSystemLabel       = Get-VolumeName -Path $nearestExistingParent
        $serialNumber          = Get-VolumeSerialNumber -Path $nearestExistingParent

        Write-Output "Folder PATH listing for volume $fileSystemLabel"
        Write-Output "Volume serial number is $serialNumber"

        Write-Output $Path

        # 3. Invalid path on valid drive → tree.com behavior
        if (-not (Test-Path $Path)) {
            $sub = $Path.Substring(2)  # remove drive letter
            Write-Output "Invalid path - $sub"
            Write-Output "No subfolders exist"
            return
        }
    }

    #
    # Compute effective settings
    #
    switch ($Mode) {

        'Tree' {
            $EffectiveMaxDepth    = $PSBoundParameters.ContainsKey('MaxDepth') ? $MaxDepth : -1
            $EffectiveColorize    = $Color.IsPresent
            $EffectiveFiles       = $Files.IsPresent
            $EffectiveHideHidden  = -not $ShowHidden.IsPresent
            $EffectiveHideSystem  = -not $ShowSystem.IsPresent
            $EffectiveShowTargets = -not $NoTargets.IsPresent
            $EffectiveGap         = -not $NoGap.IsPresent
        }

        'List' {
            $EffectiveMaxDepth    = $PSBoundParameters.ContainsKey('MaxDepth') ? $MaxDepth : -1
            $EffectiveColorize    = -not $Mono.IsPresent
            $EffectiveFiles       = -not $NoFiles.IsPresent
            $EffectiveHideHidden  = $HideHidden.IsPresent
            $EffectiveHideSystem  = $HideSystem.IsPresent
            $EffectiveShowTargets = $ShowTargets.IsPresent
            $EffectiveGap         = $false
        }

        'Normal' {
            $EffectiveMaxDepth    = $Recurse.IsPresent ? -1 : ($PSBoundParameters.ContainsKey('MaxDepth') ? $MaxDepth : 6)
            $EffectiveColorize    = -not $Mono.IsPresent
            $EffectiveFiles       = -not $NoFiles.IsPresent
            $EffectiveHideHidden  = $HideHidden.IsPresent
            $EffectiveHideSystem  = $HideSystem.IsPresent
            $EffectiveShowTargets = -not $NoTargets.IsPresent
            $EffectiveGap         = -not $NoGap.IsPresent
        }
    }

    if ($Mode -ne 'Tree') {
        # Render root directory name (Normal + Listing modes only)
        $root = Get-Item $Path
        $dir  = [PSCustomObject]@{
            FullName      = $root.FullName
            Name          = $root.Name
            Attributes    = $root.Attributes
            PSIsContainer = $true
        }
        $dir.PSObject.TypeNames.Insert(0, 'System.IO.DirectoryInfo')

        $style = Get-ItemStyle -Item $dir -Colorize:$EffectiveColorize

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

        $esc = [char]27
        $colorReset = $EffectiveColorize ? "${esc}[0m" : ""

        Write-Output "$($style.Ansi)$Path${colorReset}${debug}"
    }

    # Initialize gap state machine
    $script:GapState = [PSCustomObject]@{
        LastGapMode = [GapMode]::None
    }

    #
    # Delegate to internal engine
    #
    Show-TreeInternal `
        -Path          $Path `
        -Mode          $Mode `
        -MaxDepth      $EffectiveMaxDepth `
        -Colorize:$EffectiveColorize `
        -IncludeFiles:$EffectiveFiles `
        -HideHidden:$EffectiveHideHidden `
        -HideSystem:$EffectiveHideSystem `
        -ShowTargets:$EffectiveShowTargets `
        -Exclude $Exclude `
        -Include $Include `
        -Gap:$EffectiveGap `
        -Ascii:$Ascii `
        -DebugAttributes:$DebugAttributes

    #
    # Final newline for normal mode root
    #
    if ($Mode -ne 'Tree') {
        Write-Output ""
    }    
}
#endregion

#region Legend
<#
.SYNOPSIS
    Displays a color legend for all base types and attribute overlays.
 
.DESCRIPTION
    Useful for understanding how Show-Tree applies color to files,
    directories, symlinks, junctions, and attribute combinations.
#>

function Show-TreeLegend {
    param(
        $StyleProfile = $script:StyleProfile
    )

    $esc   = [char]27
    $reset = "${esc}[0m"

    Write-Output ""
    Write-Output "Legend"
    Write-Output "------"
    Write-Output ""

    #
    # Helper to render a sample line
    #
    function Show-Sample {
        param(
            [string]$Name,
            $Item
        )

        $style = Get-ItemStyle -Item $Item -Colorize:$true
        $ansi  = $style.Ansi
        Write-Output ("{0,-18} {1}{2}{3}" -f $Name, $ansi, $Name, $reset)
    }

    #
    # Base types
    #
    Write-Output "Types:"
    Show-Sample "Directory" ([pscustomobject]@{ PSIsContainer = $true;  Attributes = [IO.FileAttributes]::Directory })
    Show-Sample "File"      ([pscustomobject]@{ PSIsContainer = $false; Attributes = [IO.FileAttributes]::Archive })
    Show-Sample "Symlink"   ([pscustomobject]@{ PSIsContainer = $false; Attributes = [IO.FileAttributes]::ReparsePoint })
    Show-Sample "Junction"  ([pscustomobject]@{ PSIsContainer = $true;  Attributes = [IO.FileAttributes]::Directory -bor [IO.FileAttributes]::ReparsePoint })
    Write-Output ""

    #
    # Attribute overlays
    #
    Write-Output "Attributes:"
    foreach ($attr in $StyleProfile.Attributes.Keys) {
        $flag = [IO.FileAttributes]::$attr
        $item = [pscustomobject]@{
            PSIsContainer = $false
            Attributes    = $flag
        }
        Show-Sample $attr $item
    }

    Write-Output ""
}
#endregion