Desktop/Public/Get-TreeItem.ps1

# src/Public/Get-TreeItem.ps1

<#
.SYNOPSIS
    Streams tree traversal records for a path.
 
.DESCRIPTION
    The Get-TreeItem cmdlet resolves a path and performs a depth-first traversal of its children.
    It emits ShowTree.TreeRecord objects that contain both the item information and layout metadata
    required for hierarchical rendering.
 
.PARAMETER Path
    The path to traverse. Default is '.'.
     
.PARAMETER Mode
    The formatting mode ('Normal', 'Tree', 'List'). This affects how traversal is prioritized and filtered.
 
.PARAMETER Depth
    The maximum depth to traverse. -1 for unlimited, 0 for the root item only.
 
.PARAMETER ProviderMode
    The provider to use for enumerating items ('PowerShell' or 'Win32').
    'Win32' is significantly faster on Windows for large directories but may have different behavior for certain virtual or networked file types.
    'PowerShell' is the default and provides cross-platform compatibility.
     
.PARAMETER GapPolicy
    Policy followed when rendering the gaps ('None', 'Tree', 'Show').
    'None' suppresses all gaps, 'Show' shows all gaps, and 'Tree' is used for a special tree.com compatible mode where gaps only appear between files and folders.
 
.PARAMETER FollowLinks
    If set, follows symbolic links and junctions during traversal. Use with caution to avoid infinite loops.
 
.PARAMETER Include
    Filters items to include based on glob patterns.
 
.PARAMETER Exclude
    Filters items to exclude based on glob patterns.
 
.PARAMETER HideHidden
    If set, hides hidden files and directories.
 
.PARAMETER HideSystem
    If set, hides system files and directories.
 
.PARAMETER DirectoryOnly
    If set, only directories are included in the traversal.
 
.EXAMPLE
    Get-TreeItem -Path C:\Source -Depth 2 | Format-Tree
    Retrieves items from C:\Source up to 2 levels deep and formats them using the default style.
 
.EXAMPLE
    Get-TreeItem -Path . -ProviderMode Win32 -DirectoryOnly
    Efficiently retrieves only directories from the current path using Win32 APIs on Windows.
 
.LINK
    Invoke-TreeTraversal
    Format-Tree
    New-TreeItem
#>

function Get-TreeItem {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string] $Path = '.',

        [ValidateSet('Normal', 'Tree', 'List')]
        [string] $Mode = 'Normal',
        
        [int] $Depth = -1,

        [ValidateSet('PowerShell', 'Win32')]
        [string] $ProviderMode = 'PowerShell',

        [ValidateSet('None', 'Tree', 'Show')]
        [string] $GapPolicy = 'Show',

        [switch] $FollowLinks,

        [string[]] $Include,
        [string[]] $Exclude,

        [switch] $HideHidden,
        [switch] $HideSystem,
        [switch] $DirectoryOnly
    )

    if (-not $PSBoundParameters.ContainsKey('Debug') -and $PSCmdlet)
    {
        $DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference')
    }
    if (-not $PSBoundParameters.ContainsKey('Verbose') -and $PSCmdlet)
    {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    $resolvedPathInfo = Resolve-Path -LiteralPath $Path -ErrorAction SilentlyContinue

    $resolvedPath = (&{if($resolvedPathInfo){(&{if(($resolvedPathInfo.PSObject.Properties.Match('ProviderPath'))){$resolvedPathInfo.ProviderPath}else{$resolvedPathInfo.Path}})}else{$Path}})

    # Preprocessing Step: Resolve relative filters to explicit path candidates.
    # We generate candidates by joining every exclusion with every inclusion.
    # This enables "structural rescue" for relative inclusions under excluded directories.
    $processedInclude = (New-Object -TypeName System.Collections.Generic.List[string])
    if ($Include) {
        foreach ($pattern in $Include) {
            [void]$processedInclude.Add($pattern)

            if ($Exclude) {
                foreach ($exPattern in $Exclude) {
                    $exFilter = ConvertTo-TreeFilterPattern -Pattern $exPattern -RootPath $resolvedPath

                    # If it's a path pattern, or a name that happens to be a directory, we should attempt rescue joins
                    $isExDir = $exFilter.DirectoryOnly -or (Test-Path -LiteralPath (Join-Path $resolvedPath $exFilter.Pattern) -PathType Container)
                    $hasSep = $exPattern.Contains([System.IO.Path]::DirectorySeparatorChar)
                    
                    if ($hasSep -or $isExDir) {
                        $candidate = [System.IO.Path]::Combine($exFilter.Pattern, $pattern)

                        # Verify if the candidate exists before adding it as an explicit rescue
                        $absCandidate = if ([System.IO.Path]::IsPathRooted($candidate)) {
                            $candidate
                        } else {
                            [System.IO.Path]::Combine($resolvedPath, $candidate)
                        }

                        if (Test-Path -LiteralPath $absCandidate) {
                            [void]$processedInclude.Add($candidate)
                        }
                    }
                }
            }
        }
    }
    
    Write-Verbose "processedInclude: $processedInclude"
    
    $provider = New-TreeChildProvider -ProviderMode $ProviderMode

    $traversalDepth = (&{if(($Depth -eq -1)){-1}else{(&{if(($Depth -le 0)){0}else{($Depth - 1)}})}})

    $invokeTreeTraversalParams = @{
        Path          = $resolvedPath
        Mode          = $Mode
        RootPath      = $resolvedPath
        MaxDepth      = $traversalDepth
        CurrentDepth  = 0
        Provider      = $provider
        GapPolicy     = $GapPolicy
        Include       = $processedInclude.ToArray()
        Exclude       = $Exclude
        HideHidden    = $HideHidden
        HideSystem    = $HideSystem
        DirectoryOnly = $DirectoryOnly
        FollowLinks   = $FollowLinks
    }

    Invoke-TreeTraversal @invokeTreeTraversalParams
}