Desktop/Public/Show-Tree.ps1
|
# src/Public/Show-Tree.ps1 <# .SYNOPSIS Displays a directory tree in Normal, Tree.com-compatible, or Listing mode. .DESCRIPTION The Show-Tree cmdlet provides a modern, colorized directory tree visualization for the console. It is designed as a feature-rich replacement for the classic tree.com utility, supporting three distinct display modes: - Normal: A clean, modern Unicode tree view with gap lines and rich styling. - Tree: A legacy-compatible view mimicking the classic tree.com output, including drive headers. - List: A flat listing of items that retains hierarchical context. Show-Tree supports deep customization via style profiles, allows filtering by glob patterns, and handles symbolic links, hidden files, and system files across Windows and Unix platforms. .PARAMETER Mode Specifies the output mode. Valid values are 'Normal', 'Tree', and 'List'. Default is 'Normal'. .PARAMETER Path The path to the directory to display. Defaults to the current directory ('.'). .PARAMETER Include Includes only items that match the specified glob patterns. Ancestor directories of matching files are automatically included to maintain tree structure. .PARAMETER Exclude Excludes items and their descendants that match the specified glob patterns. .PARAMETER Color Forces colorization. Used primarily in Tree mode to override the default monochromatic output. Note: Defaults to ON for Normal/List modes and OFF for Tree mode. .PARAMETER NoColor Disables ANSI color coding. (Alias: Mono, NoColor) .PARAMETER Files Shows files in the tree listing. (Alias: ShowFiles) Note: Defaults to ON for Normal/List modes and OFF for Tree mode. .PARAMETER NoFiles Forcefully hides files regardless of the active mode. .PARAMETER Hidden Shows hidden items (files or directories marked with the Hidden attribute or dot-prefixed on Unix). (Alias: ShowHidden) Note: Defaults to OFF for all modes. .PARAMETER NoHidden Forcefully hides hidden items regardless of the active mode. (Alias: HideHidden) .PARAMETER System Shows system items (items marked with the System attribute on Windows). (Alias: ShowSystem) Note: Defaults to OFF for all modes. .PARAMETER NoSystem Forcefully hides system items regardless of the active mode. (Alias: HideSystem) .PARAMETER Targets Displays the targets of symbolic links and junctions. (Alias: ShowTargets) Note: Defaults to ON for Normal/List modes and OFF for Tree mode. .PARAMETER NoTargets Suppresses symbolic link and junction targets. .PARAMETER Gap Adds gap lines between item groups to improve visual clarity. Note: Defaults to ON for Normal mode and OFF for List/Tree modes. In Tree mode, enabling Files will also enable a specific "Tree" gap policy. .PARAMETER NoGap Removes gap lines. .PARAMETER Compat Enables strict compatibility mode when using -Mode Tree. This mimics the monochromatic, file-less, and target-less output of the classic tree.com utility. .PARAMETER MaxDepth The maximum recursion depth. Defaults to 6. Use -1 or the -Recurse switch for unlimited depth. (Alias: Depth) .PARAMETER Recurse Recursively traverses all subdirectories. Equivalent to -MaxDepth -1. .PARAMETER Ascii Uses standard ASCII characters (|, +, -) for tree connectors instead of Unicode box-drawing characters. .PARAMETER Legend Displays the color legend for the active style profile instead of showing a directory tree. .PARAMETER LegendAll Displays the full color legend for all supported platforms (Windows and Unix). .PARAMETER Platform Specifies which platform's style to show in legend mode ('Current', 'Windows', 'Unix'). .PARAMETER Culture Overrides the current system culture for selecting localized strings and style profiles. .EXAMPLE Show-Tree -Recurse Displays the full tree structure starting from the current directory. .EXAMPLE Show-Tree -Mode Tree -Files -Color Displays a colorized, legacy-style tree including files. .EXAMPLE Show-Tree -Hidden -NoGap Shows hidden files in the current directory and suppresses gap lines. .EXAMPLE Show-Tree -Include "src\*", "*.md" -Exclude "node_modules\" Shows Markdown files and the contents of the 'src' directory while ignoring 'node_modules'. .LINK Get-TreeItem Format-Tree Set-ShowTreeStyleProfile #> function Show-Tree { [CmdletBinding()] param( # # MODE SELECTION # [ValidateSet('Normal', 'Tree', 'List')] [string]$Mode = 'Normal', # # PATH # [Parameter(Position = 0)] [string]$Path = '.', # # GLOB FILTERING # [string[]]$Include, [string[]]$Exclude, # # MODE-SPECIFIC SWITCHES # # Colorization [switch]$Color, # Tree [Alias('Mono')] # Normal/Listing [switch]$NoColor, # Files [Alias('ShowFiles')] # Tree [switch]$Files, [switch]$NoFiles, # Normal/Listing # Hidden [Alias('ShowHidden')] # Tree [switch]$Hidden, [Alias('HideHidden')] # Normal/Listing [switch]$NoHidden, # System [Alias('ShowSystem')] # Tree [switch]$System, [Alias('HideSystem')] # Normal/Listing [switch]$NoSystem, # Reparse targets [Alias('ShowTargets')] # Listing [switch]$Targets, [switch]$NoTargets, # Normal/Tree # Gap lines [switch]$Gap, # Tree [switch]$NoGap, # Normal/Tree # Strict compatibility [switch]$Compat, # Depth [Alias('Depth')] [int]$MaxDepth, [switch]$Recurse, # ASCII connectors [switch]$Ascii, # Show the color legend [switch]$Legend, # Shortcut for Show-TreeLegend -All [switch]$LegendAll, # Platform to show in legend mode [ValidateSet('Current', 'Windows', 'Unix')] [string]$Platform = 'Current', # BCP-47 culture override [string]$Culture ) if (-not $PSBoundParameters.ContainsKey('Debug') -and $PSCmdlet) { $DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference') } if (-not $PSBoundParameters.ContainsKey('Verbose') -and $PSCmdlet) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } # # Resolve Style Profile # $resolvedStyleProfile = Get-ActiveShowTreeStyleProfile if ($Culture) { $resolvedStyleProfile = Get-ShowTreeStyleProfile -Culture $Culture } elseif ($resolvedStyleProfile) { $resolvedStyleProfile = Get-ShowTreeStyleProfile } $uiErrors = $resolvedStyleProfile.UIStrings.Errors # # Legend mode validation/rendering # $isLegendMode = $Legend -or $LegendAll if ($PSBoundParameters.ContainsKey('Platform') -and -not $isLegendMode) { throw $uiErrors.PlatformRequiresLegend } if ($Compat -and $Mode -ne 'Tree') { throw $uiErrors.CompatRequiresTree } if ($isLegendMode) { Show-TreeLegend ` -StyleProfile $resolvedStyleProfile ` -Platform $Platform ` -All:$LegendAll return } $localIsWindows = (&{if($IsWindows){$IsWindows}else{$true}}) if ($Mode -eq 'Tree' -and -not $localIsWindows) { throw $uiErrors.WindowsOnly } # # Validate paired switches # if ($Color -and $NoColor) { throw $uiErrors.ColorMonoConflict } if ($Files -and $NoFiles) { throw $uiErrors.FilesConflict } if ($Hidden -and $NoHidden) { throw $uiErrors.HiddenConflict } if ($System -and $NoSystem) { throw $uiErrors.SystemConflict } if ($Targets -and $NoTargets) { throw $uiErrors.TargetsConflict } if ($Gap -and $NoGap) { throw $uiErrors.GapConflict } # # Resolve the path # $resolvedPath = Resolve-TreePath -Path $Path -Mode $Mode if (-not $resolvedPath) { return } # # Resolve Filters against CWD # function Resolve-FilterPaths { param([string[]]$Patterns) if (-not $Patterns) { return $null } $results = New-Object System.Collections.Generic.List[string] $cwd = $PWD.ProviderPath foreach ($p in $Patterns) { # Only resolve if it's an explicit relative path or a drive-rooted path. # Names with trailing slashes (e.g. folder/) should remain as name patterns. $isRelative = $p.StartsWith('.' + [System.IO.Path]::DirectorySeparatorChar) -or $p.StartsWith('..' + [System.IO.Path]::DirectorySeparatorChar) -or $p.StartsWith('./') -or $p.StartsWith('../') if ($isRelative -or [System.IO.Path]::IsPathRooted($p)) { $resolved = $p if ($p.StartsWith('.\') -or $p.StartsWith('./')) { $resolved = $p.Substring(2) } if (-not [System.IO.Path]::IsPathRooted($resolved)) { $resolved = [System.IO.Path]::Combine($cwd, $resolved) } # Preserve wildcards $results.Add($resolved) } else { $results.Add($p) } } return $results.ToArray() } $effectiveInclude = Resolve-FilterPaths -Patterns $Include $effectiveExclude = Resolve-FilterPaths -Patterns $Exclude # # Compute effective settings # switch ($Mode) { 'Tree' { $EffectiveMaxDepth = (&{if($PSBoundParameters.ContainsKey('MaxDepth')){$MaxDepth}else{((&{if($Recurse.IsPresent){-1}else{6}}))}}) # Defaults for Tree mode depend on Compat switch $defaultColor = (&{if($Compat.IsPresent){$false}else{$true}}) $defaultFiles = (&{if($Compat.IsPresent){$false}else{$true}}) $defaultTargets = (&{if($Compat.IsPresent){$false}else{$true}}) $defaultGap = (&{if($Compat.IsPresent){((&{if($Files.IsPresent){'Tree'}else{'None'}}))}else{'Show'}}) # Resolution $EffectiveColorize = (&{if($Color.IsPresent){$true}else{((&{if($NoColor.IsPresent){$false}else{$defaultColor}}))}}) $EffectiveFiles = (&{if($Files.IsPresent){$true}else{((&{if($NoFiles.IsPresent){$false}else{$defaultFiles}}))}}) $EffectiveShowHidden = $Hidden.IsPresent $EffectiveShowSystem = $System.IsPresent $EffectiveShowTargets = (&{if($Targets.IsPresent){$true}else{((&{if($NoTargets.IsPresent){$false}else{$defaultTargets}}))}}) $GapPolicy = (&{if($Gap.IsPresent){'Show'}else{((&{if($NoGap.IsPresent){'None'}else{$defaultGap}}))}}) } default { # Normal and List modes $EffectiveMaxDepth = (&{if($PSBoundParameters.ContainsKey('MaxDepth')){$MaxDepth}else{((&{if($Recurse.IsPresent){-1}else{6}}))}}) # Defaults for Modern modes $defaultColor = $true $defaultFiles = $true $defaultTargets = ($Mode -eq 'Normal') # Normal shows targets by default, List does not $defaultGap = (&{if(($Mode -eq 'Normal')){'Show'}else{'None'}}) # Resolution $EffectiveColorize = (&{if($NoColor.IsPresent){$false}else{((&{if($Color.IsPresent){$true}else{$defaultColor}}))}}) $EffectiveFiles = (&{if($NoFiles.IsPresent){$false}else{((&{if($Files.IsPresent){$true}else{$defaultFiles}}))}}) $EffectiveShowHidden = $Hidden.IsPresent $EffectiveShowSystem = $System.IsPresent $EffectiveShowTargets = (&{if(($Mode -eq 'List')){((&{if($Targets.IsPresent){$true}else{((&{if($NoTargets.IsPresent){$false}else{$defaultTargets}}))}}))}else{((&{if($NoTargets.IsPresent){$false}else{((&{if($Targets.IsPresent){$true}else{$defaultTargets}}))}}))}}) $GapPolicy = (&{if($Gap.IsPresent){'Show'}else{((&{if($NoGap.IsPresent){'None'}else{$defaultGap}}))}}) } } # # Header Rendering # if ($Mode -eq 'Tree') { $header = Get-TreeModeHeader -Path $resolvedPath -Colorize:$EffectiveColorize -StyleProfile $resolvedStyleProfile $header | Where-Object { $_ -is [string] } if ($header -contains $false) { return } } else { # Normal + Listing modes: Print resolved path with style $rootItem = Get-Item $resolvedPath $native = [PSCustomObject]@{ Platform = (&{if($localIsWindows){'Windows'}else{'Unix'}}) FileAttributes = $rootItem.Attributes } $kind = (&{if($rootItem.PSIsContainer){'Directory'}else{'File'}}) $treeItem = New-TreeItem ` -FullPath $rootItem.FullName ` -IsContainer $rootItem.PSIsContainer ` -Kind $kind ` -Name $rootItem.Name ` -Native $native ` -Depth 0 $style = Get-ItemStyle -Item $treeItem -Colorize:$EffectiveColorize -StyleProfile $resolvedStyleProfile $colorReset = (&{if($EffectiveColorize){$resolvedStyleProfile.Reset}else{""}}) Write-Output "$($style.Ansi)$resolvedPath${colorReset}" } # # Enumerate -> Select -> Render # $providerMode = (&{if(($Mode -eq 'Tree' -and $Compat.IsPresent)){'Win32'}else{'PowerShell'}}) if ($providerMode -eq 'Win32' -and -not $localIsWindows) { throw $uiErrors.Win32WindowsOnly } $getTreeItemParams = @{ Path = $resolvedPath Mode = $Mode Depth = $EffectiveMaxDepth ProviderMode = $providerMode GapPolicy = $GapPolicy Include = $effectiveInclude Exclude = $effectiveExclude HideHidden = -not $EffectiveShowHidden HideSystem = -not $EffectiveShowSystem DirectoryOnly = (-not $EffectiveFiles) } $formatTreeParams = @{ Mode = $Mode Colorize = $EffectiveColorize ShowTargets = $EffectiveShowTargets Ascii = $Ascii GapPolicy = $GapPolicy StyleProfile = $resolvedStyleProfile } Get-TreeItem @getTreeItemParams | Format-Tree @formatTreeParams # # Footer / Last Line logic # if ($Mode -ne 'Tree') { Write-Output "" } } |