Private/Utilities.psm1
|
using module .\Enums.psm1 using module .\Abstracts.psm1 using module .\Console\Colors.psm1 using module .\Console.psm1 using module .\Console\Internal.psm1 using module .\Result.psm1 class FileTools { static [PSObject] GetItemSize([string]$Path) { $size = 0 if (Test-Path $Path -PathType Container -ErrorAction SilentlyContinue) { $size = Get-ChildItem -Path $Path -File -Recurse -Force | Measure-Object -Property Length -Sum | Select-Object -ExpandProperty sum } else { $size = Get-Item -Path $Path | Select-Object -ExpandProperty Length } return [PSCustomObject] @{ bytes = $size; Item = Get-Item $Path } } static [string] GetShortPath([string]$Path, [int]$KeepBefore, [int]$KeepAfter, [string]$Separator, [string]$TruncateChar) { $splitPath = $Path.Split($Separator, [System.StringSplitOptions]::RemoveEmptyEntries) if ($splitPath.Count -gt ($KeepBefore + $KeepAfter)) { $outPath = [string]::Empty for ($i = 0; $i -lt $KeepBefore; $i++) { $outPath += $splitPath[$i] + $Separator } $outPath += "$($TruncateChar)$($Separator)" for ($i = ($splitPath.Count - $KeepAfter); $i -lt $splitPath.Count; $i++) { if ($i -eq ($splitPath.Count - 1)) { $outPath += $splitPath[$i] } else { $outPath += $splitPath[$i] + $Separator } } } else { $outPath = $splitPath -join $Separator if ($splitPath.Count -eq 1) { $outPath += $Separator } } return $outPath } static [string] GetShortPath([string]$Path) { return [FileTools]::GetShortPath($Path, 2, 1, [System.IO.Path]::DirectorySeparatorChar, [char]8230) } static [string] NewRandomFileName([string]$Extension, [bool]$UseTempFolder, [bool]$UseHomeFolder) { if ($UseTempFolder) { $filename = [system.io.path]::GetTempFileName() } elseif ($UseHomeFolder) { $homedocs = [Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments) $filename = Join-Path -Path $homedocs -ChildPath ([system.io.path]::GetRandomFileName()) } else { $filename = [system.io.path]::GetRandomFileName() } if (![string]::IsNullOrEmpty($Extension)) { $original = [system.io.path]::GetExtension($filename).Substring(1) return $filename -replace "$original$", $Extension } return $filename } static [string] GetTempDirectory() { return [System.IO.Path]::GetTempPath() } static [string] GetHomeDirectory() { return [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile) } } class HashTools { static [hashtable] JoinHashtable([hashtable]$First, [hashtable]$Second, [bool]$Force) { $Primary = $First.Clone() $Secondary = $Second.Clone() $duplicates = $Primary.keys | Where-Object { $Secondary.ContainsKey($_) } if ($duplicates) { foreach ($item in $duplicates) { if ($Force) { $Secondary.Remove($item) } else { $r = Read-Host "Which key do you want to KEEP [AB]?" if ($r -eq "A") { $Secondary.Remove($item) } elseif ($r -eq "B") { $Primary.Remove($item) } else { Write-Warning "Aborting"; return $null } } } } return $Primary + $Secondary } } class HostTools { static [int] GetHostHeight() { return (Get-Host).UI.RawUI.BufferSize.Height } static [int] GetHostWidth() { return ([type]'ConsoleWriter')::get_ConsoleWidth() } static [string] GetHostOs() { return [cryptobase]::GetHostOs() } static [object] InvokeInputBox([string]$Title, [string]$Prompt, [bool]$AsSecureString, [string]$BackgroundColor) { if ((Test-IsPSWindows)) { Add-Type -AssemblyName 'PresentationFramework' Add-Type -AssemblyName 'PresentationCore' Remove-Variable -Name myInput -Scope script -ErrorAction SilentlyContinue $form = New-Object System.Windows.Window $stack = New-Object System.Windows.Controls.StackPanel $form.Title = $Title $form.Height = 150 $form.Width = 350 $form.Background = $BackgroundColor $label = New-Object System.Windows.Controls.Label $label.Content = " $Prompt" $label.HorizontalAlignment = "left" $stack.AddChild($label) if ($AsSecureString) { $inputbox = New-Object System.Windows.Controls.PasswordBox } else { $inputbox = New-Object System.Windows.Controls.TextBox } $inputbox.Width = 300 $inputbox.HorizontalAlignment = "center" $stack.AddChild($inputbox) $space = New-Object System.Windows.Controls.Label $space.Height = 10 $stack.AddChild($space) $btn = New-Object System.Windows.Controls.Button $btn.Content = "_OK" $btn.Width = 65 $btn.HorizontalAlignment = "center" $btn.VerticalAlignment = "bottom" $btn.Add_click({ if ($AsSecureString) { $script:myInput = $inputbox.SecurePassword } else { $script:myInput = $inputbox.text } $form.Close() }) $stack.AddChild($btn) $space2 = New-Object System.Windows.Controls.Label $space2.Height = 10 $stack.AddChild($space2) $btn2 = New-Object System.Windows.Controls.Button $btn2.Content = "_Cancel" $btn2.Width = 65 $btn2.HorizontalAlignment = "center" $btn2.VerticalAlignment = "bottom" $btn2.Add_click({ $form.Close() }) $stack.AddChild($btn2) $form.AddChild($stack) [void]$inputbox.Focus() $form.WindowStartupLocation = 1 [void]$form.ShowDialog() return $script:myInput } else { Write-Warning "Sorry. This command requires a Windows platform." return $null } } static [object] InvokeInputBox() { return [HostTools]::InvokeInputBox("User Input", "Please enter a value:", $false, "White") } } class ModuleTools { static [hashtable] GetModuleData() { $d = @{} Get-ChildItem -Path "$((Get-Module cliHelper.core).ModuleBase)/en-US" -File data*.csv | ForEach-Object { $d[$_.Name.Replace('data.', '').Replace('.csv', '')] = [IO.File]::ReadAllText($_.FullName) | ConvertFrom-Csv } return $d } } class ProgressUtil { static [PsRecord] $data static ProgressUtil() { # Static constructor: build $data explicitly to avoid PsRecord's broken implicit # hashtable-cast (which fails on non-hashtable values like string arrays & scriptblocks). $d = [PsRecord]::new() $d.Add('ShowProgress', [scriptblock] { return (Get-Variable 'ProgressPreference' -ValueOnly) -eq 'Continue' }) $d.Add('DefaultProgressMsg', 'Running background task') $d.Add('ProgressBarColor', 'LightSeaGreen') $d.Add('ProgressMsgColor', 'LightGoldenrodYellow') $d.Add('ProgressBlock', '■') $d.Add('TwirlFrames', '') $d.Add('TwirlEmojis', [string[]]@( '◰◳◲◱', '◇◈◆', '◐◓◑◒', '←↖↑↗→↘↓↙', '┤┘┴└├┌┬┐', '⣾⣽⣻⢿⡿⣟⣯⣷', '|/-\', '-\|/', '|/-\' ) ) [ProgressUtil]::data = $d } static [void] WriteProgressBar([int]$percent) { [ProgressUtil]::WriteProgressBar($percent, $true, "") } static [void] WriteProgressBar([int]$percent, [string]$message) { [ProgressUtil]::WriteProgressBar($percent, $true, $message) } static [void] WriteProgressBar([int]$percent, [bool]$update, [string]$message) { [ProgressUtil]::WriteProgressBar($percent, $update, [int]([ConsoleWriter]::get_ConsoleWidth() * 0.7), $message) } static [void] WriteProgressBar([int]$percent, [bool]$update, [string]$message, [bool]$Completed) { [ProgressUtil]::WriteProgressBar($percent, $update, [int]([ConsoleWriter]::get_ConsoleWidth() * 0.7), $message, $Completed) } static [void] WriteProgressBar([int]$percent, [bool]$update, [int]$PBLength, [string]$message) { [ProgressUtil]::WriteProgressBar($percent, $update, $PBLength, $message, $false) } static [void] WriteProgressBar([int]$percent, [bool]$update, [int]$PBLength, [string]$message, [bool]$Completed) { [ProgressUtil]::WriteProgressBar($percent, $update, $PBLength, $message, $Completed, [ProgressUtil]::data.ProgressBarcolor) } static [void] WriteProgressBar([int]$percent, [bool]$update, [int]$PBLength, [string]$message, [bool]$Completed, [string]$PBcolor) { <# .SYNOPSIS Renders a progress bar using the Progress engine (markup + ANSI colors). Colors are resolved via [Color]::FromName() → markup string so no [RGB] cast is required. .EXAMPLE for ($i = 0; $i -le 100; $i++) { [ProgressUtil]::WriteProgressBar($i, $true, 40, "doing stuff") } #> if (![ProgressUtil]::data.ShowProgress) { Write-Debug '[ProgressUtil]::data.ShowProgress is set to false. Progress bar will not be displayed. Please enable it by running [ProgressUtil]::ToggleShowProgress()' return } # --- Runtime type resolution (avoids parse-time forward-reference failures) --- $consoleType = [type]'AnsiConsole' $progressType = [type]'Progress' $settingsType = [type]'ProgressTaskSettings' $descColType = [type]'TaskDescriptionColumn' $progressBarColType = [type]'ProgressBarColumn' $pctColType = [type]'PercentageColumn' $colorType = [type]'Color' # Resolve markup-safe color strings from legacy color names $resolveMarkup = { param([string]$name, [string]$fallback) try { $c = $colorType::FromName($name) if ($null -eq $c -or $c.IsDefault) { return $fallback } return $c.ToMarkup() } catch { return $fallback } } $barColor = & $resolveMarkup $PBcolor 'seagreen1' $msgColor = & $resolveMarkup ([ProgressUtil]::data.ProgressMsgcolor) 'lightyellow3' $console = $consoleType::Console $progress = $progressType::new($console) $progress.RefreshRateMs = 80 $progress.Columns.Clear() $progress.Columns.Add($descColType::new($progress)) $progress.Columns.Add($progressBarColType::new($progress)) $progress.Columns.Add($pctColType::new($progress)) # Capture locals for the action closure $capturedPct = [Math]::Max(0, [Math]::Min(100, $percent)) $capturedMsg = $message $capturedBarColor = $barColor $capturedMsgColor = $msgColor $capturedCompleted = $Completed $capturedSettings = $settingsType::new() $capturedSettings.MaxValue = 100 $capturedSettings.IsIndeterminate = $false $progress.Start([System.Action[object]] { param([object]$ctx) $label = "[$capturedMsgColor]$capturedMsg[/]" $task = $ctx.AddTask($label, $capturedSettings) # Jump straight to the target percentage $task.SetValue($capturedPct) if ($capturedCompleted) { $task.Complete() } } ) } static [Results] WaitJob([string]$progressMsg, [scriptblock]$sb) { return [ProgressUtil]::WaitJob($progressMsg, $sb, $null) } static [Results] WaitJob([string]$progressMsg, [System.Management.Automation.Job]$Job) { return [ProgressUtil]::WaitJob($progressMsg, $Job, [ProgressUtil]::data.ProgressMsgcolor) } static [Results] WaitJob([string]$progressMsg, [System.Management.Automation.Job]$Job, [string]$PmsgColor) { <# .DESCRIPTION Visual progress bar that spins while waiting for a job to complete. Uses cliHelper.core robust Progress framework preventing silent failures. Types are resolved at runtime to avoid PowerShell parse-time type resolution failures for transitively-imported module classes. #> [System.Diagnostics.Stopwatch]$stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # --- Runtime type resolution (avoids parse-time [TypeName] failures for transitive using module deps) --- $consoleType = [type]'AnsiConsole' $progressType = [type]'Progress' $settingsType = [type]'ProgressTaskSettings' $descColType = [type]'TaskDescriptionColumn' $spinColType = [type]'SpinnerColumn' $colorType = [type]'Color' $console = $consoleType::Console $progress = $progressType::new($console) $progress.Columns.Clear() $progress.Columns.Add($descColType::new($progress)) $progress.Columns.Add($spinColType::new($progress)) # Translate legacy RGB color name to Spectre markup color string try { $colorObj = $colorType::FromName($PmsgColor) $PmsgColorStyle = if ($null -eq $colorObj -or $colorObj.IsDefault) { 'yellow' } else { $colorObj.ToMarkup() } } catch { $PmsgColorStyle = 'yellow' } # Capture outer variables for use inside the Action scriptblock. # NOTE: Progress.Start() is synchronous — the action runs on the MAIN thread. # To animate the spinner we must call a task-state method (SetValue/SetDescription) # each loop iteration so it fires TriggerUpdate() → OnUpdate → liveSession.Tick() # → SpinnerColumn advances a frame → console render. Without this, the main thread # just sleeps and the spinner never draws. $capturedJob = $Job $capturedMsg = $PmsgColorStyle $capturedMsgText = $progressMsg $capturedSettings = $settingsType::new() $capturedSettings.IsIndeterminate = $true # keep task alive during SetValue(0) loop $progress.Start([System.Action[object]] { param([object]$ctx) $task = $ctx.AddTask("[$capturedMsg]$capturedMsgText[/]", $capturedSettings) while ($capturedJob.JobStateInfo.State -notin @('Completed', 'Failed', 'Stopped')) { [System.Threading.Thread]::Sleep(80) # Fire OnUpdate → Tick() → spinner frame advance + console render $task.SetValue(0) } $task.Complete() # sets IsCompleted=$true, shows ✓ in SpinnerColumn } ) $stopwatch.Stop() $elapsed = $stopwatch.Elapsed.TotalSeconds $res = [Results]::new() # Collect errors from child jobs [object[]]$Errors = @($Job.ChildJobs | Where-Object { $null -ne $_.Error } | ForEach-Object { $_.Error }) # Pre-declare $errVars so it's always in scope regardless of -ErrorVariable behavior $errVars = [System.Collections.ArrayList]::new() $errVarsTmp = @() $jobOutputs = Receive-Job -Job $Job -ErrorAction SilentlyContinue -ErrorVariable errVarsTmp if ($errVarsTmp.Count -gt 0) { foreach ($e in $errVarsTmp) { [void]$errVars.Add($e) } } if ($Job.JobStateInfo.State -in @('Failed', 'Stopped') -or $Errors.Count -gt 0 -or $errVars.Count -gt 0) { if ($Errors.Count -gt 0) { $res.Add([Result]::Err($Errors[0]), $elapsed) } elseif ($errVars.Count -gt 0) { $res.Add([Result]::Err($errVars[0]), $elapsed) } else { $res.Add([Result]::Err([System.Exception]::new('Job failed without a specific error.')), $elapsed) } } else { $res.Add([Result]::Ok($jobOutputs), $elapsed) } return $res } static [Results] WaitJob([string]$progressMsg, [scriptblock]$sb, [Object[]]$ArgumentList) { if ($null -ne $ArgumentList) { $Job = Start-ThreadJob -ScriptBlock $sb -ArgumentList $ArgumentList } else { $Job = Start-ThreadJob -ScriptBlock $sb } return [ProgressUtil]::WaitJob($progressMsg, $Job) } static [void] ToggleShowProgress() { # .DESCRIPTION # The ShowProgress option respects $ProgressPreference, this method enables you to take control of that and set/toggle it manualy. [ProgressUtil]::data.Set('ShowProgress', [scriptblock]::Create(" return [bool]$([int]![ProgressUtil]::data.ShowProgress)")) } } class StringTools { static [string[]] SplitLine([string]$String) { if ($String -notmatch "`n") { return , ([array]$String) } $ReturnValue = $String -split "`r`n" if ($ReturnValue.Count -eq 1) { $ReturnValue = $String -split "`n" } return $ReturnValue } static [Object[]] SplitStringOnLiteralString([string]$objToSplit, [string]$objSplitter) { if ([string]::IsNullOrEmpty($objToSplit)) { return @() } if ([string]::IsNullOrEmpty($objSplitter)) { return @($objToSplit) } $objSplitterInRegEx = [regex]::Escape($objSplitter) $result = @([regex]::Split($objToSplit, $objSplitterInRegEx)) return $result } static [string] ReverseString([string]$Inputstr) { if ([string]::IsNullOrEmpty($Inputstr)) { return $Inputstr } $charArray = $Inputstr.ToCharArray() [Array]::Reverse($charArray) return [string]::new($charArray) } static [string] ToTitleCase([string]$Inputstr) { if ([string]::IsNullOrEmpty($Inputstr)) { return $Inputstr } $TextInfo = (Get-Culture).TextInfo return $TextInfo.ToTitleCase($Inputstr.ToLower()) } } |