functions/tree-demo.ps1
|
# https://gui-cs.github.io/Terminal.GuiV1Docs/docs/treeview.html using namespace Terminal.Gui #define your TUI function function Invoke-TuiTreeDemo { [cmdletbinding()] [Alias('tuiTree')] param( [Parameter(position = 0, HelpMessage = 'Specify a path to display.')] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path -Path $_ }, ErrorMessage = 'Failed to find or validate {0}.')] [ValidateScript({ (Get-Item $_).PSProvider.Name -eq 'FileSystem' }, ErrorMessage = 'The path {0} does not appear to be a filesystem location.')] [String]$Path = $Home, [Parameter(HelpMessage = 'Specify the number of levels to display. The higher the value the longer it will take to display the tree.')] [ValidateRange(1, 7)] [int]$Depth = 4 ) #region initialize #convert to a file system path $Path = (Convert-Path $Path) #You MUST invoke Init() [Application]::Init() #I recommend setting a QuitKey [Application]::QuitKey = 'Esc' #endregion #region helper functions function toggle { param() if ($btnToggle.Text.ToString() -match 'Expand') { $tree.ExpandAll() $btnToggle.Text = 'Collapse All' $btnQuit.X = $btnQuit.X + 2 } else { $tree.CollapseAll() $btnToggle.Text = 'Expand All' $btnQuit.X = $btnQuit.X - 2 } [Application]::Refresh() } # Helper function to create tree nodes from directory structure function New-DirectoryTree { param( [string]$Path, [int]$MaxDepth = $Depth, [int]$CurrentDepth = 0, [switch]$IsRoot ) if ($CurrentDepth -ge $MaxDepth) { return $null } try { $item = Get-Item -LiteralPath $Path -ErrorAction Stop # Create node for this item $node = [Trees.TreeNode]::new($item.Name) $node.Tag = $item.FullName $StatusBar.items[0].Title = "Loading $($item.Fullname)" [Application]::Refresh() if ($isRoot) { $node.Text = $item.FullName.ToUpper() } if ($item.PSIsContainer) { # If it's a directory, add children $children = Get-ChildItem -LiteralPath $Path -ErrorAction SilentlyContinue foreach ($child in $children) { $childNode = New-DirectoryTree -Path $child.FullName -MaxDepth $Depth -CurrentDepth ($CurrentDepth + 1) if ($childNode) { [void]($node.Children.Add($childNode)) } } } #the function output is the tree $node } catch { Write-Warning "Cannot access: $Path" } } function updateDetail { param( [string]$Path = $txtPath.Text.ToString() ) #a helper for the helper function _node { param([string]$name) [Trees.TreeNode]::new($name) } $treeDetail.ClearObjects() $item = Get-Item $Path $node = _node $item.FullName #add property nodes $node.Children.Add((_node "Created $($item.CreationTime)")) $node.Children.Add((_node "Modified $($item.LastWriteTime)")) $age = '{0:dd\.hh\:mm\:ss}' -f (New-TimeSpan -Start $item.LastWriteTime -End (Get-Date)) $node.Children.Add((_node "Age $age")) if ($item.PSIsContainer) { $node.Children.Add((_node "Root Files $($item.GetFiles().count)")) $node.Children.Add((_node "Root Folders $($item.GetDirectories().count)")) #get total size $sz = $item.EnumerateFiles('*', 'AllDirectories') | measure length -Sum $node.Children.Add((_node "Total File $($sz.Count)")) $node.Children.Add((_node "Total Size $($sz.Sum)")) if ($sz.sum -ge 1MB) { $node.Children.Add((_node "Total Size (MB) $($sz.Sum/1MB -as [int])")) } } else { $node.Children.Add((_node "Extension $($item.Extension)")) $node.Children.Add((_node "Size $($item.Length)")) $node.Children.Add((_node "SizeKB $([int]($item.Length/1KB))")) #add file version if detected if ($item.VersionInfo.ProductVersion -or $item.VersionInfo.FileInfo) { $ver = _node VersionInfo #pad names $pad = 18 $item.VersionInfo.PSObject.properties | ForEach-Object { $n = '{0} {1}' -f $_.name.PadRight($pad), $_.Value $ver.Children.Add((_node $n)) } $node.Children.Add($ver) Remove-Variable ver } else { switch -regex ($item.Extension) { '(txt)|(md)|(ps(d|m)?1)' { #add text measurements $stat = _node FileStatics $measure = Get-Content $item.FullName -Raw | Measure-Object -Word -Line $stat.children.Add(( _node "Words $($measure.Words)" )) $stat.children.Add(( _node "Lines $($measure.Lines)" )) $node.children.add($stat) Remove-Variable stat } 'zip' { $zip = _node ZipStatistics #open the zip in read only mode $zipArchive = [System.IO.Compression.ZipFile]::Open($item.FullName, 'Read') #measure the contents $measure = $zipArchive.Entries | Measure-Object -Property Length, CompressedLength -Sum $zip.Children.Add(( _node "Total Entries $($measure[0].count)" )) $zip.Children.Add(( _node "Total Compressed Size $($measure[1].Sum)" )) $zip.Children.Add(( _node "Total Size $($measure[0].Sum)" )) $node.Children.Add($zip) # $zipArchive.Dispose() Remove-Variable zip } '(mp3)|(m4a)' { $pad = 15 $media = _node MediaStatistics $tagFile = [Taglib.File]::Create($item.Fullname) $length = '{0:hh\:mm\:ss}' -f $tagFile.Properties.Duration $media.Children.Add((_node "Duration $length") ) ($tagFile.tag | Select-Object Album, title, joinedArtists, year, joinedGenres).PSObject.Properties.ForEach({ $media.Children.Add((_node "$($_.Name.PadRight($pad)) $($_.value)")) }) $tagFile.Dispose() $node.Children.Add($media) Remove-Variable media } '(png)|(bmp)|(gif)|(jp(e)?g)' { $imgNode = _node "ImageDetails" $img = [System.Drawing.Image]::FromFile($item.FullName) $imgNode.Children.Add((_node "Width $($img.Width)")) $imgNode.Children.Add((_node "Height $($img.Height)")) $imgNode.Children.Add((_node "HorizontalResolution $($img.HorizontalResolution -as [int])")) $imgNode.Children.Add((_node "VerticalResolution $($img.VerticalResolution -as [int])")) $imgNode.Children.Add((_node "RawFormat $($img.RawFormat)")) $img.Dispose() $node.Children.Add($imgNode) Remove-Variable imgNode } } #close Switch } } $node.Children.Add((_node "Target $($item.ResolvedTarget)")) $node.Children.Add((_node "LinkType $($item.LinkType)")) $treeDetail.AddObject($node) $treeDetail.ExpandAll() } #endregion #region create the main window and status bar $StatusBar = [StatusBar]::New( @([StatusItem]::New('Unknown', 'Ready', {}))) #Add the control to the application [Application]::Top.Add($StatusBar) $window = [Window]@{Title = 'Directory TUI Tree Demo' } $window.Add_Initialized({ $lblNote.Text = "Building tree for $Path. Please wait. This may take a minute or two." $frameDetail.Visible = $False $btnUpdate.Enabled = $false $btnToggle.Enabled = $false [Application]::Refresh() }) $window.Add_Ready({ $StatusBar.items[0].Title = "Loading $($txtPath.Text.ToString())" [Application]::Refresh() $rootNode = New-DirectoryTree -Path $txtPath.Text.ToString() -MaxDepth $Depth -isRoot if ($rootNode) { # Expand each of the root node's direct children foreach ($child in $rootNode.Children) { $tree.Expand($child) } $tree.AddObject($rootNode) } updateDetail $treeDetail.ExpandAll() $lblNote.Text = "The tree view is limited to a depth of $Depth." $frameDetail.Visible = $True $btnUpdate.Enabled = $True $btnToggle.Enabled = $True $StatusBar.items[0].Title = 'Ready' [Application]::Refresh() $Tree }) #endregion #region add controls $txtPath = [TextField]@{ X = 1 Y = 1 Text = (Convert-Path $Path) Width = 50 } $window.Add($txtPath) $btnUpdate = [button]@{ X = 1 Y = 3 Text = '_Update' } $btnUpdate.Add_Clicked({ $lblSelected.Text = '' toggle #remove the existing tree $tree.ClearObjects() $treeDetail.ClearObjects() if (Test-Path $txtPath.Text.ToString()) { #rebuild $rootNode = New-DirectoryTree -Path $txtPath.Text.ToString() -MaxDepth $Depth -isRoot if ($rootNode) { # Expand each of the root node's direct children foreach ($child in $rootNode.Children) { $tree.Expand($child) } } $tree.AddObject($rootNode) updateDetail $txtPath.Text.ToString() } else { $StatusBar.Items[0].Title = "Failed to find $($txtPath.Text.ToString())" } [Application]::Refresh() }) $Window.Add($btnUpdate) $btnToggle = [Button]@{ X = $btnUpdate.Frame.Right + 1 Y = $btnUpdate.Frame.Y Text = 'Expand All' } $btnToggle.Add_Clicked({ #call a helper function toggle }) $window.Add($btnToggle) $btnQuit = [Button]@{ X = $btnToggle.Frame.Right + 1 Y = $btnToggle.Frame.Y Text = '_Quit' } $btnQuit.Add_Clicked({ #stop the TUI application [Application]::RequestStop() }) $window.Add($btnQuit) $lblSelected = [Label]@{ X = $txtPath.Frame.Right + 1 Y = $txtPath.Frame.Y Text = 'Selected:' Width = [Dim]::Percent(99) } $window.add($lblSelected) $treeDetail = [TreeView]@{ X = 2 Y = 1 Width = [Dim]::Percent(90) Height = [Dim]::Percent(90) Text = 'Detail TBA' Style = [Trees.TreeStyle]@{ ShowBranchLines = $True ColorExpandSymbol = $True HighlightModelTextOnly = $True InvertExpandSymbolColors = $False } } $frameDetail = [FrameView]@{ X = $txtPath.Frame.Right + 6 Y = $btnQuit.Frame.Y Title = 'Selected Details' Width = [Dim]::Percent(55) Height = [Dim]::Percent(90) } $frameDetail.Add($treeDetail) $window.Add($frameDetail) $lblNote = [Label]@{ X = 1 Y = $btnUpdate.Frame.Bottom + 1 Text = "The tree view is limited to a depth of $Depth" } $window.Add($lblNote) # Create TreeView $tree = [TreeView]@{ X = 1 Y = $lblNote.Frame.Bottom + 2 Width = [Dim]::Percent(35) Height = [Dim]::Percent(80) Text = Split-Path $Path -Parent Style = [Trees.TreeStyle]@{ ShowBranchLines = $True ColorExpandSymbol = $True HighlightModelTextOnly = $True InvertExpandSymbolColors = $False } } # Add selection event handler $tree.Add_SelectionChanged({ $selected = $tree.SelectedObject if ($selected) { $lblSelected.Text = "Selected: $($selected.Tag)" updateDetail $selected.Tag [Application]::Refresh() } }) $tree.Add_MouseClick({ param($e) # Get the node at the clicked position using the mouse event row $clickedNode = $tree.GetObjectOnRow($e.MouseEvent.Y) if ($clickedNode) { # Move focus to the clicked node $tree.SelectedObject = $clickedNode [Application]::Refresh() } #button1 is left click #button3 is right click #display content from selected file types in a dialog box with a right mouse click if ($e.MouseEvent.Flags -eq 'Button3clicked') { if ($clickedNode.Text -match '(txt)|(ps(d|m)?1)|(md)|(yml)|(json)|(csv)|(xml)$') { $content = [Dialog]@{ Title = "Preview: $($clickedNode.Tag)" Width = [Dim]::Percent(90) Height = [Dim]::Percent(80) } # TextView fills the dialog and is read-only $cv = [TextView]@{ X = 0 Y = 0 Width = [Dim]::Fill() Height = [Dim]::Fill(2) # leave room for the button ReadOnly = $true Text = (Get-Content $clickedNode.Tag -Raw) } $ok = [Button]@{ Text = 'OK' IsDefault = $true } $ok.Add_Clicked({ $content.RequestStop() }) $content.Add($cv) $content.AddButton($ok) [Application]::Run($content) } } }) $window.Add($tree) #endregion #region display #Add the Window and its nested controls to the TUI application [Application]::Top.Add($window) #Invoke the TUI [Application]::Run() #When the TUI ends it will shutdown [Application]::ShutDown() #endregion } |