$splat = @{
Title = 'Monitor-Workflow';
PropertyKey = 'Value';
Property = 'Value', 'Iteration';
InputPattern = "(\d+)";
RefreshInterval = 3;
ProcessBlock = {
param(
[object] $InputObject
)
$InputObject.Iteration++
}
}
1..10 | Monitor-Object @splat
Last active
May 28, 2025 05:53
-
-
Save romero126/be03f7d1f4c9c9ee93da78738f13dcd7 to your computer and use it in GitHub Desktop.
Monitor-Object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function Monitor-Object { | |
| <# | |
| .SYNOPSIS | |
| Monitor-Object is a cmdlet that allows you to monitor a collection of objects | |
| .DESCRIPTION | |
| Monitor-Object is a cmdlet that allows you to monitor a collection of objects. | |
| The cmdlet will display the entire collection to the screen | |
| and allow you to process each object in the collection | |
| with a scriptblock that you provide. The cmdlet will also allow you to | |
| add and remove objects from the collection using hotkeys. | |
| .PARAMETER ProcessBlock | |
| The processblock will be used to process/refresh the objects in the collection. | |
| Example: { param($InputObject) $InputObject } | |
| .PARAMETER InputObject | |
| Specify either a String or a PSObject that will be used to identify the | |
| Example: "Hello World", [PSCustomObject]@{ Name = "Hello World" } | |
| .PARAMETER PropertyKey | |
| The PropertyKey is the primary key identifier that will be used to identify the each unique object in the pipeline | |
| .PARAMETER Property | |
| The property or properties that be defined as each object in the collection[] | |
| .PARAMETER FormatTable | |
| If FormatTable is specified, the collection will be formatted with Format-Table using the properties specified. | |
| .PARAMETER Title | |
| The title of the monitor object | |
| .PARAMETER InputPattern | |
| The InputPattern is a regex pattern that will be used to identify if the input is a valid object to be added to the collection. | |
| .PARAMETER RefreshInterval | |
| The RefreshInterval is the interval in seconds that the monitor will refresh the screen. | |
| .PARAMETER Passthru | |
| The Passthru switch will return the collection at the end of the cmdlet's execution. | |
| .EXAMPLE | |
| 1..10 | Monitor-Object -Title 'Monitor-Item' -PropertyKey 'Value' -Property 'Name', 'Value', 'Iteration' -FormatTable 'Name', 'Value', 'Iteration' -InputPattern "(\d+)" -ProcessBlock { | |
| param( | |
| [object] $InputObject | |
| ) | |
| if ($null = $InputObject.Name) { | |
| $InputObject.Name = "Object $($InputObject.Value)" | |
| } | |
| $InputObject.Iteration += 1 | |
| } | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| # There is a hard requirement for the script to handle an InputObject with ValueFromPipeline | |
| [Parameter(Mandatory)] | |
| [Alias('ScriptBlock', 'SB')] | |
| [ScriptBlock]$ProcessBlock, | |
| [Parameter(ValueFromPipeline)] | |
| [Alias('Object')] | |
| [object[]] $InputObject, | |
| [Parameter(Mandatory)] | |
| [alias('key')] | |
| [System.String] $PropertyKey, | |
| [Parameter()] | |
| [System.String[]] $Property, | |
| [Parameter()] | |
| [PSCustomObject[]] $FormatTable, | |
| [Parameter()] | |
| [ValidateScript({ $_ -is [ScriptBlock] -or $_ -is [String] })] | |
| [System.Object] $Title = "Monitor-Object", | |
| [Parameter(Mandatory)] | |
| [System.String] $InputPattern, | |
| [Parameter()] | |
| [Alias('ri')] | |
| [int] $RefreshInterval = 30, | |
| [Parameter()] | |
| [switch] $Passthru | |
| ) | |
| begin { | |
| # this is not a complete list of ansi escape sequences | |
| $ansi = @{ | |
| esc = "$([char]27)" | |
| cursorHome = "$([char]27)[0;0H" | |
| cursorReturn = "$([char]27)[0G" | |
| eraseLine = "$([char]27)[2K" | |
| eraseScreen = "$([char]27)[2J" | |
| eraseScreenAfterCursor = "$([char]27)[0J" | |
| colorReset = "$([char]27)[0m" | |
| altBufferEnable = "$([char]27)[?1049h" | |
| altBufferDisable = "$([char]27)[?1049l" | |
| # colors ; # background colors | |
| black = "$([char]27)[30m"; bgBlack = "$([char]27)[40m" | |
| red = "$([char]27)[31m"; bgRed = "$([char]27)[41m" | |
| green = "$([char]27)[32m"; bgGreen = "$([char]27)[42m" | |
| yellow = "$([char]27)[33m"; bgYellow = "$([char]27)[43m" | |
| blue = "$([char]27)[34m"; bgBlue = "$([char]27)[44m" | |
| magenta = "$([char]27)[35m"; bgMagenta = "$([char]27)[45m" | |
| cyan = "$([char]27)[36m"; bgCyan = "$([char]27)[46m" | |
| white = "$([char]27)[37m"; bgWhite = "$([char]27)[47m" | |
| default = "$([char]27)[39m"; bgDefault = "$([char]27)[49m" | |
| # text style | |
| bold = "$([char]27)[1m" | |
| underline = "$([char]27)[4m" | |
| blink = "$([char]27)[5m" | |
| inverse = "$([char]27)[7m" | |
| hidden = "$([char]27)[8m" | |
| strike = "$([char]27)[9m" | |
| noBold = "$([char]27)[22m" | |
| noUnderline = "$([char]27)[24m" | |
| noBlink = "$([char]27)[25m" | |
| noInverse = "$([char]27)[27m" | |
| noHidden = "$([char]27)[28m" | |
| noStrike = "$([char]27)[29m" | |
| } | |
| # Specify common color schema | |
| $ansi_ui = @{ | |
| TextColor = $ansi.white | |
| WarningColor = $ansi.yellow | |
| ExceptionColor = $ansi.red | |
| SuccessColor = $ansi.green | |
| EmphasisColor = $ansi.cyan | |
| menu_textColor = $ansi.white | |
| menu_bodyColor = "$($ansi.esc)[48;5;24m" + $ansi.white | |
| menu_borderColor = "$($ansi.esc)[48;5;237m" + $ansi.white | |
| } | |
| # Set a default object for its current instance. | |
| $this = @{} | |
| $this.Collection = New-Object System.Collections.ArrayList | |
| $this.HotKeyCollection = New-Object System.Collections.ArrayList | |
| # Starting, Running, Suspend, Exit | |
| $this.State = "Starting" | |
| # Define actions based on whats in the ActionCollection | |
| $this.Action = $null | |
| $this.Interrupt = $null | |
| # Define hotkeys | |
| $this.HotKeyCollection = @( | |
| @{ | |
| Key = "A" | |
| Modifier = 'Control' | |
| Description = "Add Clipboard to Pipeline" | |
| Action = "AddClipboardToPipeline", "RefreshScreen" | |
| }, | |
| @{ | |
| Key = "D" | |
| Modifier = 'Control' | |
| Description = "Remove Clipboard to Pipeline" | |
| Action = "RemoveClipboardFromPipeline", "RefreshScreen" | |
| } | |
| @{ | |
| Key = "R" | |
| Modifier = 'Control' | |
| Description = "Refresh the object data" | |
| Action = "RefreshScreen" | |
| }, | |
| @{ | |
| Key = "X" | |
| Modifier = 'Control' | |
| Description = "Exit" | |
| Action = "Exit" | |
| } | |
| ) | |
| $this.Methods = @{ | |
| #New | |
| InvokeScriptBlock = { | |
| param( | |
| [ScriptBlock] $ScriptBlock, | |
| [hashtable] $Parameters | |
| ) | |
| # We should not process anything if the state has an interrupt | |
| if ($this.State -eq "Exit") { | |
| return | |
| } | |
| try { | |
| if ($Parameters) { | |
| & $ScriptBlock @Parameters | |
| } else { | |
| & $ScriptBlock | |
| } | |
| } | |
| catch { | |
| $this.State = "Exit" | |
| # Bubble up the exception to the console | |
| [System.Console]::WriteLine("$($ansi.altBufferDisable)$($ansi_ui.ExceptionColor)An Exception was thrown:") | |
| [System.Console]::Write(($_ | Out-String)) | |
| [System.Console]::Write("$($ansi.colorReset)") | |
| } | |
| } | |
| #New | |
| Draw_Menu_Top = { | |
| #set cursor to 0,0 | |
| [System.Console]::Write("$($ansi.cursorHome)") | |
| # If the title is a scriptblock, invoke it | |
| if ($Title -is [ScriptBlock]) { | |
| & $this.Methods.InvokeScriptBlock -ScriptBlock $Title | |
| return | |
| } | |
| # Draw top menu bar (This doesnt doing anything and its the equivalant of ASCII Art so you can see that you are in the cmdlet menu) | |
| [System.Console]::Write("$($ansi_ui.menu_borderColor)$($ansi.eraseLine)`n") | |
| [System.Console]::Write(("$($ansi_ui.menu_bodyColor)$($ansi.eraseLine)`n"*2)) | |
| # Write CommandName | |
| # We dont like adding new lines to the title so we will replace them with spaces | |
| $message = "$Title" -replace "`n", " " | |
| if ($message.Count -ge $host.UI.RawUI.WindowSize.Width) { | |
| $message = $message.Substring(0, $host.UI.RawUI.WindowSize.Width - 5) + '...' | |
| } | |
| $padding = (' ' * (($host.UI.RawUI.WindowSize.Width / 2) - $message.Length/2)) | |
| [System.Console]::Write("$($ansi_ui.menu_bodyColor)$($ansi.eraseLine)$padding $message`n") | |
| [System.Console]::Write(("$($ansi_ui.menu_bodyColor)$($ansi.eraseLine)`n"*2)) | |
| [System.Console]::Write("$($ansi_ui.menu_borderColor)$($ansi.eraseLine)`n") | |
| [System.Console]::Write("$($ansi.colorReset)") | |
| } | |
| # New | |
| Draw_Menu_Body = { | |
| [System.Console]::Write($ansi.eraseScreenAfterCursor) | |
| if ($this.Collection.Count -eq 0) { | |
| [System.Console]::Write("$($ansi_ui.WarningColor)No objects in the pipeline`n") | |
| return | |
| } | |
| if ($FormatTable) { | |
| & $this.Methods.InvokeScriptBlock ` | |
| -ScriptBlock { | |
| param($Collection, $FormatTable) | |
| $Collection | Format-Table -Property $FormatTable | |
| } ` | |
| -Parameters @{ | |
| Collection = $this.Collection; | |
| FormatTable = $FormatTable | |
| } | |
| } else { | |
| & $this.Methods.InvokeScriptBlock ` | |
| -ScriptBlock { | |
| param($Collection) | |
| $Collection | Out-Default | |
| } ` | |
| -Parameters @{ | |
| Collection = $this.Collection | |
| } | |
| } | |
| } | |
| # New Ish | |
| Draw_Menu_Bottom = { | |
| # Draw the hotkeys at the bottom of the screen | |
| [System.Console]::Write("$($ansi_ui.TextColor)Hotkeys:`n") | |
| foreach ($hotKey in $this.HotKeyCollection) { | |
| [System.Console]::Write("`t$($ansi_ui.EmphasisColor)$($hotKey.Modifier)$($ansi_ui.TextColor)+$($ansi_ui.EmphasisColor+$hotKey.Key)$($ansi_ui.TextColor) : $($hotKey.Description)`n") | |
| } | |
| [System.Console]::Write("$($ansi.colorReset)") | |
| } | |
| # New | |
| WaitRefreshInterval = { | |
| if ($this.Interrupt -or $this.State -eq "Exit") { | |
| return | |
| } | |
| # Refresh Interval | |
| $timer = [Diagnostics.Stopwatch]::StartNew() | |
| if ($RefreshInterval) { | |
| do { | |
| # Show Status | |
| $timeRemaining = [System.Math]::Round($RefreshInterval - $timer.Elapsed.TotalSeconds, 1) | |
| [System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)Refreshing in $timeRemaining Seconds $($ansi.colorReset)") | |
| # Check for keypress | |
| & $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods.CheckKeyPress | |
| # Check for Interrupts or Exit or Interrupt | |
| if ($this.Interrupt -or $this.State -eq "Exit") { | |
| [System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)Skip Refreshing with $timeRemaining Seconds remaining$($ansi.colorReset)`n") | |
| break | |
| } | |
| Start-Sleep -MilliSeconds 100 # 10 times a second | |
| } until ($timer.Elapsed.TotalSeconds -ge $RefreshInterval) | |
| $timer.Stop() | |
| [System.Console]::Write("$($ansi.eraseLine)") | |
| } | |
| } | |
| # New | |
| CheckKeyPress = { | |
| if ([System.Console]::KeyAvailable) { | |
| $key = [System.Console]::ReadKey($true) | |
| foreach ($hotKey in $this.HotKeyCollection) { | |
| if ($key.Modifiers -eq $hotKey.Modifier -and $key.Key -eq $hotKey.Key) { | |
| $this.Interrupt = $hotkey.Action | |
| return | |
| } | |
| } | |
| } | |
| } | |
| # New | |
| RefreshScreen = { | |
| $this.Interrupt = $null | |
| & $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods.ProcessObjects | |
| } | |
| AddClipboardToPipeline = { | |
| $clipboard = ( Get-Clipboard ) -Split "`n" | ForEach-Object { if ($_ -match $InputPattern) { $matches[0] } } | Select-Object -Unique | |
| if ($clipboard.Count -eq 0) { | |
| [System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)No objects found in the clipboard`n") | |
| $this.Methods.Sleep.Invoke() | |
| return | |
| } | |
| $existingItems = $this.Collection | Where-Object { $_.PSObject.Properties.Item($PropertyKey).Value -in $clipboard } | |
| [System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.TextColor)Clipboard contains $($ansi_ui.EmphasisColor)$($clipboard.Count)$($ansi_ui.TextColor) unique objects $($ansi_ui.WarningColor)Skipping$($ansi_ui.TextColor) $($ansi_ui.EmphasisColor)$($clipboard.Count)$($ansi_ui.TextColor) objects`n") | |
| foreach ($i in $clipboard) { | |
| $itemExists = $this.Collection | Where-Object { $_.PSObject.Properties.Item($PropertyKey).Value -in $i } | |
| if ($itemExists) { | |
| [System.Console]::Write("$($ansi_ui.TextColor) Skipping item: $($ansi_ui.EmphasisColor)$i$($ansi_ui.TextColor)`n") | |
| continue | |
| } | |
| [System.Console]::Write("$($ansi_ui.TextColor) Adding item: $($ansi_ui.EmphasisColor)$i$($ansi_ui.TextColor) to buffer`n") | |
| $null = $this.Collection.Add( | |
| ([PSCustomObject]@{ $PropertyKey = $i } | Select-Object -Property $Property) | |
| ) | |
| } | |
| } | |
| RemoveClipboardFromPipeline = { | |
| $clipboard = ( Get-Clipboard ) -Split "`n" | ForEach-Object { if ($_ -match $InputPattern) { $matches[0] } } | Select-Object -Unique | |
| $objectsToRemove = $this.Collection | Where-Object { $_.PSObject.Properties.Item($PropertyKey).Value -in $clipboard -or $_ -eq $clipboard } | |
| if ($objectsToRemove.Count -eq 0) { | |
| [System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)No objects found to remove`n") | |
| $this.Methods.Sleep.Invoke() | |
| return | |
| } | |
| [System.Console]::Write("`n`n`n$($ansi_ui.WarningColor)Objects to be removed$($ansi.colorReset)`n") | |
| if ($FormatTable) { | |
| $objectsToRemove | Format-Table -Property $FormatTable | Out-Host | |
| } else { | |
| $objectsToRemove | Out-Default | |
| } | |
| $decision = $Host.UI.PromptForChoice("Warning: Removing items on the list", "This will remove $($objectsToRemove.Count) items from the pipeline", @('&Yes', '&No'), 1) | |
| if ($decision -ne 0) { | |
| return | |
| } | |
| $objectsToRemove | ForEach-Object { $null = $this.Collection.Remove( $_ ) } -ErrorAction SilentlyContinue | |
| } | |
| ProcessObjects = { | |
| # Endless snake of loops | |
| for ($i = 0; $i -lt $this.Collection.Count; $i++) { | |
| $obj = $this.Collection[$i] | |
| # Check keypress and invoke hotkey action | |
| & $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods.CheckKeyPress | |
| # Check for Interrupts or Exit or Interrupt | |
| if ($this.State -eq "Exit" -or $this.Interrupt) { | |
| [System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)Skip Processing on $i of $($this.Collection.Count)$($ansi.colorReset)") | |
| return | |
| } | |
| # Invoke and handle the ProcessBlock | |
| [System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)Processing on $i of $($this.Collection.Count) : ") | |
| try { | |
| & $ProcessBlock -InputObject $obj | |
| } | |
| catch { | |
| [System.Console]::Write("`n$($ansi_ui.ExceptionColor)Error Processing $($i+1) of $($this.Collection.Count) : $($ansi.colorReset)`n") | |
| $this.Methods.Sleep.Invoke() | |
| } | |
| } | |
| [System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.SuccessColor)Completed Processing $($this.Collection.Count) Records$($ansi.colorReset)`n$($ansi.eraseScreenAfterCursor)") | |
| } | |
| Sleep = { | |
| start-sleep 5 | |
| } | |
| Exit = { | |
| $this.State = "Exit" | |
| } | |
| } | |
| if ($PropertyKey -notin $Property) { | |
| $Property += $PropertyKey | |
| } | |
| } | |
| process { | |
| # Add the input object to the list | |
| foreach ($obj in $InputObject) { | |
| if ($obj -match $InputPattern) { | |
| $null = $this.Collection.Add( | |
| ([PSCustomObject]@{ $PropertyKey = $matches[0] } | Select-Object -Property $Property) | |
| ) | |
| continue | |
| } elseif ($obj -is [PSCustomObject] -and $obj.PSObject.Properties.Item($PropertyKey).Value -match $InputPattern) { | |
| $null = $this.Collection.Add($obj) | |
| continue | |
| } | |
| } | |
| } | |
| end { | |
| [System.Console]::Write($ansi.altBufferEnable) | |
| try { | |
| do { | |
| # Draw the Menu | |
| $this.Action = "Draw_Menu_Top", "Draw_Menu_Body", "Draw_Menu_Bottom", "WaitRefreshInterval", "ProcessObjects" | |
| if ($this.State -eq "Starting") { | |
| $this.Action = "Draw_Menu_Top", "Draw_Menu_Body", "Draw_Menu_Bottom", "ProcessObjects" | |
| $this.State = "Running" | |
| } | |
| foreach ($action in $this.Action) { | |
| if ($this.State -eq "Exit") { | |
| break | |
| } | |
| if ($this.Methods.ContainsKey($action)) { | |
| & $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods[$action] | |
| } else { | |
| Write-Warning "Action $action not found" | |
| $this.Methods.Sleep.Invoke() | |
| } | |
| # Check keypress and invoke hotkey action | |
| $this.Methods.CheckKeyPress.Invoke() | |
| } | |
| # Check for Interrupt and call the action | |
| if ($this.Interrupt) { | |
| # Store the interrupts in a variable to use later | |
| $interrupts = $this.Interrupt | |
| # Reset the interrupts to null so that we can properly run the actions | |
| $this.Interrupt = $null | |
| # Invoke all of the interrupts in the list | |
| foreach ($interrupt in $interrupts) { | |
| if ($this.Methods.ContainsKey($interrupt)) { | |
| & $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods[$interrupt] | |
| } | |
| } | |
| } | |
| } until ($this.State -eq "Exit") | |
| } | |
| catch { | |
| throw $_ | |
| } | |
| finally { | |
| # Clean up the console | |
| [System.Console]::Write($ansi.altBufferDisable) | |
| } | |
| if ($Passthru) { | |
| $this.Collection | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
