RecMonTUI.ps1
|
<#PSScriptInfo .VERSION 1.0.7 .GUID 5b2a3cd0-8d4b-4c4f-9ef2-5bde600fa324 .AUTHOR Robert .COMPANYNAME OAOA-DEV .COPYRIGHT .TAGS Big TUI Tools PowerShellToolkit .LICENSEURI .PROJECTURI https://oaoa.dev/tools .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDMODULES .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA .DESCRIPTION A console TUI application for real-time Resource Monitor performance telemetry. #> <# .SYNOPSIS Provides a text user interface (TUI) inside the PowerShell console to monitor CPU, memory, disk, and network resources. .DESCRIPTION A console TUI application for real-time Resource Monitor performance telemetry. .PARAMETER None .EXAMPLE RecMonTUI #> # --- TUI LAYOUT ENGINE MODULE FUNCTIONS --- # Generic Reusable PowerShell Console TUI Library # Author: Antigravity $ESC = [char]27 # Global Layout Variables (defaults, dynamically updated on resize) $global:leftWidth = 35 # mainHeight will be dynamically updated $global:mainHeight = 25 # ANSI Color Utilities function Get-ANSIColor($name) { switch ($name) { 'Reset' { "$ESC[0m" } 'Bold' { "$ESC[1m" } 'Inverse' { "$ESC[7m" } 'SelectedActive' { "$ESC[37;44m" } # White on Blue 'SelectedInactive' { "$ESC[37;100m" } # White on Dark Gray 'Error' { "$ESC[91m" } # Bright Red 'Warning' { "$ESC[93m" } # Bright Yellow 'Info' { "$ESC[92m" } # Bright Green 'Gray' { "$ESC[90m" } # Dark Gray 'White' { "$ESC[97m" } # White 'Cyan' { "$ESC[96m" } # Cyan 'Blue' { "$ESC[94m" } # Blue 'Header' { "$ESC[30;47m" } # Black on Light Gray 'ErrorRow' { "$ESC[37;41m" } # White on Red background 'WarningRow' { "$ESC[30;43m" } # Black on Yellow background 'GreenRow' { "$ESC[32m" } # Green text default { "$ESC[0m" } } } # Position the cursor function Set-Cursor($x, $y) { [Console]::SetCursorPosition($x, $y) } # Write text at specific coordinates function Write-At($x, $y, $text, $colorName = 'Reset') { $color = Get-ANSIColor $colorName $reset = Get-ANSIColor 'Reset' Set-Cursor $x $y [Console]::Write("$color$text$reset") } # Initialize console settings for TUI function Initialize-Console { $global:originalCursorVisible = [Console]::CursorVisible $global:originalOutputEncoding = [Console]::OutputEncoding [Console]::CursorVisible = $false [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # Enter Alternate Screen Buffer to preserve user scrollback [Console]::Write("$ESC[?1049h") [Console]::Write("$ESC[?25l") # ANSI Hide Cursor [Console]::Write("$ESC[2J") # Clear entire screen } # Restore original console settings on exit function Restore-Console { # Exit Alternate Screen Buffer [Console]::Write("$ESC[?1049l") [Console]::Write("$ESC[?25h") # ANSI Show Cursor [Console]::CursorVisible = $global:originalCursorVisible [Console]::OutputEncoding = $global:originalOutputEncoding } # Generic Borders Drawing function Draw-Borders { param( [int]$Width, [int]$Height, [int]$LeftWidth, [int]$MainHeight, [int]$FocusArea, [string]$LeftTitle = "", [string]$RightTopTitle = "", [string]$RightBottomTitle = "", [bool]$HasVerticalDivider = $true, [bool]$HasHorizontalDivider = $true, [double]$RightTopWidthPercent = 1.0 ) $rightWidth = $Width - $LeftWidth - 4 # 1. Draw top border $topBorder = "┌" + ("─" * $LeftWidth) + $(if ($HasVerticalDivider) { "┬" } else { "─" }) + ("─" * $rightWidth) + "┐" Write-At 0 1 $topBorder 'Gray' # 2. Draw middle lines for ($y = 2; $y -le ($Height - 3); $y++) { Write-At 0 $y "│" 'Gray' if ($HasVerticalDivider) { Write-At ($LeftWidth + 1) $y "│" 'Gray' } Write-At ($Width - 2) $y "│" 'Gray' } # 3. Draw horizontal divider between Table and Details (right side only, if exists) if ($HasHorizontalDivider) { $dividerStart = if ($HasVerticalDivider) { $LeftWidth + 1 } else { 0 } $rightDivider = $(if ($HasVerticalDivider) { "├" } else { "│" }) + ("─" * $rightWidth) + "┤" Write-At $dividerStart ($MainHeight + 1) $rightDivider 'Gray' } # 4. Draw bottom border $bottomBorder = "└" + ("─" * $LeftWidth) + $(if ($HasVerticalDivider) { "┴" } else { "─" }) + ("─" * $rightWidth) + "┘" Write-At 0 ($Height - 2) $bottomBorder 'Gray' # 5. Draw secondary vertical divider if RightTopWidthPercent is < 1.0 (for Resource Monitor graph) if ($HasVerticalDivider -and $RightTopWidthPercent -lt 1.0 -and $RightTopWidthPercent -gt 0.0) { $listWidth = [int]($rightWidth * $RightTopWidthPercent) $dividerX = $LeftWidth + 2 + $listWidth for ($y = 2; $y -le $MainHeight; $y++) { Write-At $dividerX $y "│" 'Gray' } Write-At $dividerX 1 "┬" 'Gray' if ($HasHorizontalDivider) { Write-At $dividerX ($MainHeight + 1) "┴" 'Gray' } } # 6. Draw highlighted titles to indicate focus if ($LeftTitle) { $lTitleText = if ($FocusArea -eq 0) { "● $LeftTitle ●" } else { " $LeftTitle " } $lTitleX = [int](($LeftWidth - $lTitleText.Length) / 2) + 1 $lColor = if ($FocusArea -eq 0) { 'SelectedActive' } else { 'Header' } Write-At $lTitleX 1 $lTitleText $lColor } if ($RightTopTitle) { $rtTitleText = if ($FocusArea -eq 1) { "● $RightTopTitle ●" } else { " $RightTopTitle " } $rtWidth = if ($RightTopWidthPercent -lt 1.0) { [int]($rightWidth * $RightTopWidthPercent) } else { $rightWidth } $rtTitleX = $LeftWidth + 2 + [int](($rtWidth - $rtTitleText.Length) / 2) $rtColor = if ($FocusArea -eq 1) { 'SelectedActive' } else { 'Header' } Write-At $rtTitleX 1 $rtTitleText $rtColor } if ($RightBottomTitle -and $HasHorizontalDivider) { $rbTitleText = if ($FocusArea -eq 2) { "● $RightBottomTitle ●" } else { " $RightBottomTitle " } $rbTitleX = $LeftWidth + 2 + [int](($rightWidth - $rbTitleText.Length) / 2) $rbColor = if ($FocusArea -eq 2) { 'SelectedActive' } else { 'Header' } Write-At $rbTitleX ($MainHeight + 1) $rbTitleText $rbColor } } # Generic Menu Bar Drawing function Draw-Menu { param( [int]$Width, [string[]]$Items ) $menuColor = Get-ANSIColor 'Header' $reset = Get-ANSIColor 'Reset' $menuText = " " + ($Items -join " ") $paddedMenu = $menuText.PadRight($Width).Substring(0, $Width) Set-Cursor 0 0 [Console]::Write("$menuColor$paddedMenu$reset") } # Generic Status Bar Drawing function Draw-Status { param( [string]$StatusText, [string]$FocusAreaText, [string]$HelpText, [int]$Width, [int]$Height ) $statusColor = Get-ANSIColor 'Header' $reset = Get-ANSIColor 'Reset' $focusPart = if ($FocusAreaText) { "[$FocusAreaText]" } else { "" } $helpPart = if ($HelpText) { $HelpText + " │ " } else { "" } $maxStatusLen = $Width - $focusPart.Length - $helpPart.Length - 10 $leftText = $StatusText if ($leftText.Length -gt $maxStatusLen) { $leftText = $leftText.Substring(0, $maxStatusLen - 3) + "..." } $paddedStatusText = " " + $helpPart + $leftText # Pad status bar to exactly Width - 2 to prevent console host auto-scroll $remainingSpaces = ($Width - 2) - $paddedStatusText.Length - $focusPart.Length if ($remainingSpaces -lt 0) { $remainingSpaces = 0 } $fullStatus = $paddedStatusText + (" " * $remainingSpaces) + $focusPart if ($fullStatus.Length -gt ($Width - 2)) { $fullStatus = $fullStatus.Substring(0, $Width - 2) } Set-Cursor 0 ($Height - 1) [Console]::Write("$statusColor$fullStatus$reset") } # Recalculate dimensions function Update-LayoutDimensions { param( [int]$Width, [int]$Height ) $global:mainHeight = $Height - 16 if ($global:mainHeight -lt 5) { $global:mainHeight = 5 } $global:leftWidth = 35 $maxWidth = [int]($Width * 0.4) if ($global:leftWidth -gt $maxWidth) { $global:leftWidth = $maxWidth } if ($global:leftWidth -lt 15) { $global:leftWidth = 15 } } # --- TREE VIEW COMPONENTS --- function Get-VisibleNodes($node) { $result = [System.Collections.ArrayList]::new() function Add-Node($n) { $result.Add($n) | Out-Null if (!$n.IsLeaf -and $n.IsExpanded -and $n.Children) { foreach ($child in $n.Children) { Add-Node $child } } } Add-Node $node return $result } function Draw-TreeView { param( [System.Collections.ArrayList]$VisibleNodes, [int]$SelectedIndex, [int]$ScrollOffset, [int]$LeftWidth, [int]$TreeHeight, [bool]$IsFocused = $true ) $startX = 1 $startY = 2 $reset = Get-ANSIColor 'Reset' for ($i = 0; $i -lt $TreeHeight; $i++) { $nodeIndex = $ScrollOffset + $i $y = $startY + $i if ($nodeIndex -lt $VisibleNodes.Count) { $node = $VisibleNodes[$nodeIndex] $prefix = if ($node.IsLeaf) { " " } else { if ($node.IsExpanded) { "- " } else { "+ " } } $indent = " " * ($node.Level + 1) $displayText = $indent + $prefix + $node.Label if ($displayText.Length -gt $LeftWidth) { $displayText = $displayText.Substring(0, $LeftWidth - 3) + "..." } $paddedText = $displayText.PadRight($LeftWidth) if ($nodeIndex -eq $SelectedIndex) { $color = if ($IsFocused) { Get-ANSIColor 'SelectedActive' } else { Get-ANSIColor 'SelectedInactive' } } else { $color = Get-ANSIColor 'Reset' } Write-At $startX $y $paddedText # Set highlight Set-Cursor $startX $y [Console]::Write("$color$paddedText$reset") } else { Write-At $startX $y (" " * $LeftWidth) } } } # --- GENERIC TABLE VIEW COMPONENTS --- # Helper to format bytes function Format-Bytes { param($bytes) if ($null -eq $bytes) { return "0 B" } try { $bytes = [double]$bytes } catch { return "0 B" } if ($bytes -ge 1GB) { return "{0:N2} GB" -f ($bytes / 1GB) } if ($bytes -ge 1MB) { return "{0:N2} MB" -f ($bytes / 1MB) } if ($bytes -ge 1KB) { return "{0:N2} KB" -f ($bytes / 1KB) } return "{0:N0} B" -f $bytes } # Helper to format date strings like "20260527" -> "2026-05-27" function Format-DateString { param($str) if ($null -eq $str -or $str.Length -ne 8) { return $str } return "$($str.Substring(0,4))-$($str.Substring(4,2))-$($str.Substring(6,2))" } # Draw generic table header function Draw-TableHeader { param( [int]$StartX, [int]$StartY, [array]$Columns, # @{ Label="Name"; Width=15; Align="Left" } [int]$Width ) $reset = Get-ANSIColor 'Reset' $headerColor = Get-ANSIColor 'Header' $headerParts = @() foreach ($col in $Columns) { $label = $col.Label $w = $col.Width if ($label.Length -gt $w) { $label = $label.Substring(0, $w) } $padded = if ($col.Align -eq 'Right') { $label.PadLeft($w) } else { $label.PadRight($w) } $headerParts += $padded } $headerText = $headerParts -join "│" $paddedHeader = $headerText.PadRight($Width).Substring(0, $Width) Set-Cursor $startX $startY [Console]::Write("$headerColor$paddedHeader$reset") } # Draw generic table rows function Draw-TableRows { param( [System.Collections.ArrayList]$Items, [int]$SelectedIndex, [int]$ScrollOffset, [array]$Columns, [int]$StartX, [int]$StartY, [int]$Width, [int]$Height, [bool]$IsFocused = $true, [scriptblock]$RowColorScript = $null ) $reset = Get-ANSIColor 'Reset' for ($i = 0; $i -lt $Height; $i++) { $itemIndex = $ScrollOffset + $i $y = $startY + $i if ($itemIndex -lt $Items.Count) { $item = $Items[$itemIndex] $rowParts = @() foreach ($col in $Columns) { $val = $null if ($null -ne $item) { if ($item -is [hashtable]) { $val = $item[$col.Prop] } else { $val = $item.$($col.Prop) } } if ($null -eq $val) { $val = "" } # Format cell values $formattedVal = switch ($col.Format) { 'Percent' { "{0:F1} %" -f $val } 'MB' { "{0:N1} MB" -f ($val / 1MB) } 'Bytes' { Format-Bytes $val } 'Decimal' { "{0:F1}" -f $val } 'DateTime' { if ($val -is [DateTime]) { $val.ToString("yyyy-MM-dd HH:mm:ss") } else { $val.ToString() } } 'RegistryDate'{ Format-DateString $val } default { $val.ToString() } } # Strip control characters to prevent cursor shifting $formattedVal = $formattedVal -replace "`r", "" -replace "`n", "" -replace "`t", " " $w = $col.Width if ($formattedVal.Length -gt $w) { $formattedVal = if ($w -gt 2) { $formattedVal.Substring(0, $w - 2) + ".." } else { $formattedVal.Substring(0, $w) } } $padded = if ($col.Align -eq 'Right') { $formattedVal.PadLeft($w) } else { $formattedVal.PadRight($w) } $rowParts += $padded } $rowText = $rowParts -join "│" # Select background based on active selection if ($itemIndex -eq $SelectedIndex) { $color = if ($IsFocused) { Get-ANSIColor 'SelectedActive' } else { Get-ANSIColor 'SelectedInactive' } } else { if ($null -ne $RowColorScript) { $colorName = & $RowColorScript $item $color = Get-ANSIColor $colorName } else { $color = Get-ANSIColor 'Reset' } } $paddedRow = $rowText.PadRight($Width).Substring(0, $Width) Set-Cursor $StartX $y [Console]::Write("$color$paddedRow$reset") } else { # Empty row Set-Cursor $StartX $y [Console]::Write(" " * $Width) } } } # --- DETAILS VIEW COMPONENTS --- function Get-DetailPropertyLine($lbl1, $val1, $lbl2, $val2, $colWidth) { $lblW = 12 $valW = $colWidth - $lblW if ($valW -lt 5) { $valW = 5 } $lbl1Str = $lbl1.PadRight($lblW).Substring(0, $lblW) $val1Str = if ($null -ne $val1) { $val1.ToString() } else { "" } if ($val1Str.Length -gt $valW) { $val1Str = $val1Str.Substring(0, $valW - 3) + "..." } $col1Text = ($lbl1Str + $val1Str).PadRight($colWidth) $lbl2Str = $lbl2.PadRight($lblW).Substring(0, $lblW) $val2Str = if ($null -ne $val2) { $val2.ToString() } else { "" } if ($val2Str.Length -gt $valW) { $val2Str = $val2Str.Substring(0, $valW - 3) + "..." } $col2Text = ($lbl2Str + $val2Str).PadRight($colWidth) return "$col1Text │ $col2Text" } # Wrap text lines helper function Wrap-Text($text, $maxLength) { if ([string]::IsNullOrEmpty($text)) { return @("") } $paragraphs = $text.Split(@("`r`n", "`n"), [System.StringSplitOptions]::None) $lines = [System.Collections.ArrayList]::new() foreach ($para in $paragraphs) { if ($para.Length -le $maxLength) { $lines.Add($para) | Out-Null continue } $words = $para.Split(' ') $currentLine = "" foreach ($word in $words) { if ($currentLine.Length -eq 0) { $currentLine = $word } elseif (($currentLine.Length + 1 + $word.Length) -le $maxLength) { $currentLine += " " + $word } else { $lines.Add($currentLine) | Out-Null $currentLine = $word } } if ($currentLine.Length -gt 0) { $lines.Add($currentLine) | Out-Null } } return $lines } function Draw-Details { param( [array]$HeaderLines, [array]$MessageLines, [int]$ScrollOffset, [int]$StartX, [int]$StartY, [int]$Width, [int]$Height ) $hasHeader = ($null -ne $HeaderLines -and $HeaderLines.Count -gt 0) if ($hasHeader) { # Draw the header lines (up to 4) $headerCount = $HeaderLines.Count for ($i = 0; $i -lt 4; $i++) { $y = $StartY + $i $lineText = if ($i -lt $headerCount) { $HeaderLines[$i] } else { "" } if ($lineText.Length -gt $Width) { $lineText = $lineText.Substring(0, $Width) } $lineText = $lineText -replace "`r", "" -replace "`n", "" -replace "`t", " " Set-Cursor $StartX $y [Console]::Write($lineText.PadRight($Width)) } # Draw divider line $yDivider = $StartY + 4 $reset = Get-ANSIColor 'Reset' $gray = Get-ANSIColor 'Gray' Set-Cursor $StartX $yDivider [Console]::Write("$gray" + ("─" * $Width) + "$reset") $scrollAreaHeight = $Height - 5 $bodyStartY = $StartY + 5 } else { $scrollAreaHeight = $Height $bodyStartY = $StartY } # Draw scrollable body message lines $messageCount = if ($null -eq $MessageLines) { 0 } else { $MessageLines.Count } for ($i = 0; $i -lt $scrollAreaHeight; $i++) { $lineIndex = $ScrollOffset + $i $y = $bodyStartY + $i $lineText = if ($lineIndex -lt $messageCount) { $MessageLines[$lineIndex] } else { "" } if ($lineText.Length -gt $Width) { $lineText = $lineText.Substring(0, $Width) } $lineText = $lineText -replace "`r", "" -replace "`n", "" -replace "`t", " " Set-Cursor $StartX $y [Console]::Write($lineText.PadRight($Width)) } # Fill remaining space for ($y = $bodyStartY + $scrollAreaHeight; $y -lt $StartY + $Height; $y++) { Set-Cursor $StartX $y [Console]::Write(" " * $Width) } } # --- TEXT DIALOG INPUT BOX --- function Show-InputBox { param( [string]$Prompt, [string]$Title, [int]$Width, [int]$Height ) $boxW = 60 $boxH = 5 $boxX = [int](($Width - $boxW) / 2) $boxY = [int](($Height - $boxH) / 2) $reset = Get-ANSIColor 'Reset' $borderC = Get-ANSIColor 'White' $titleC = Get-ANSIColor 'SelectedActive' # 1. Draw outer frames of the box $topB = "╔" + ("═" * ($boxW - 2)) + "╗" $midB = "║" + (" " * ($boxW - 2)) + "║" $botB = "╚" + ("═" * ($boxW - 2)) + "╝" Write-At $boxX $boxY $topB 'White' Write-At $boxX ($boxY + 1) $midB 'White' Write-At $boxX ($boxY + 2) $midB 'White' Write-At $boxX ($boxY + 3) $midB 'White' Write-At $boxX ($boxY + 4) $botB 'White' # Draw title $titleText = " $Title " $titleX = $boxX + [int](($boxW - $titleText.Length) / 2) Write-At $titleX $boxY $titleText 'SelectedActive' # Draw prompt text Write-At ($boxX + 2) ($boxY + 1) $Prompt $inputText = "" $inputX = $boxX + 2 $inputY = $boxY + 2 $maxInputLen = $boxW - 4 # Temporarily show cursor [Console]::Write("$ESC[?25h") [Console]::CursorVisible = $true while ($true) { # Render current text Set-Cursor $inputX $inputY $displayText = $inputText if ($displayText.Length -gt $maxInputLen) { $displayText = "..." + $displayText.Substring($displayText.Length - $maxInputLen + 3) } [Console]::Write($displayText.PadRight($maxInputLen)) $cursorX = $inputX + [Math]::Min($displayText.Length, $maxInputLen) Set-Cursor $cursorX $inputY $key = [Console]::ReadKey($true) if ($key.Key -eq 'Enter') { break } if ($key.Key -eq 'Escape') { $inputText = $null break } if ($key.Key -eq 'Backspace') { if ($inputText.Length -gt 0) { $inputText = $inputText.Substring(0, $inputText.Length - 1) } } else { if ($key.KeyChar -ge 32 -and $key.KeyChar -le 126) { $inputText += $key.KeyChar } } } [Console]::Write("$ESC[?25l") [Console]::CursorVisible = $false return $inputText } # --- SCROLLABLE CHECKLIST DIALOG WITH SEARCH FILTERING --- function Show-CheckListDialog { param( [string]$Prompt, [string]$Title, [System.Collections.ArrayList]$Items, # Array of @{ Label = 'Name'; Checked = $true/$false } [int]$Width, [int]$Height ) $boxW = 80 if ($boxW -gt ($Width - 4)) { $boxW = $Width - 4 } if ($boxW -lt 40) { $boxW = 40 } $boxH = 18 $boxX = [int](($Width - $boxW) / 2) if ($boxX -lt 0) { $boxX = 0 } $boxY = [int](($Height - $boxH) / 2) if ($boxY -lt 1) { $boxY = 1 } $reset = Get-ANSIColor 'Reset' $topB = "╔" + ("═" * ($boxW - 2)) + "╗" $midB = "║" + (" " * ($boxW - 2)) + "║" $botB = "╚" + ("═" * ($boxW - 2)) + "╝" $searchQuery = "" $selectedIndex = 0 $scrollOffset = 0 $listHeight = $boxH - 8 $focusedControl = 'Search' # 'Search' or 'List' while ($true) { # 1. Draw outer frame Write-At $boxX $boxY $topB 'White' for ($i = 1; $i -lt ($boxH - 1); $i++) { Write-At $boxX ($boxY + $i) $midB 'White' } Write-At $boxX ($boxY + $boxH - 1) $botB 'White' # Title $titleText = " $Title " $titleX = $boxX + [int](($boxW - $titleText.Length) / 2) Write-At $titleX $boxY $titleText 'SelectedActive' # Prompt Write-At ($boxX + 2) ($boxY + 1) $Prompt # Search Box label and input field $searchLabel = "Search Filter: " $inputW = $boxW - 22 if ($inputW -lt 10) { $inputW = 10 } $dispQuery = $searchQuery if ($dispQuery.Length -gt ($inputW - 2)) { $dispQuery = $dispQuery.Substring($dispQuery.Length - ($inputW - 2)) } if ($focusedControl -eq 'Search') { Write-At ($boxX + 2) ($boxY + 2) $searchLabel 'White' $fieldVal = " $dispQuery" + "_" $fieldVal = $fieldVal.PadRight($inputW) Write-At ($boxX + 17) ($boxY + 2) $fieldVal 'SelectedActive' } else { Write-At ($boxX + 2) ($boxY + 2) $searchLabel 'Gray' $fieldVal = " $dispQuery" $fieldVal = $fieldVal.PadRight($inputW) Write-At ($boxX + 17) ($boxY + 2) $fieldVal 'SelectedInactive' } Write-At ($boxX + 2) ($boxY + 3) ("─" * ($boxW - 4)) 'Gray' # Filter items based on search query $filteredItems = [System.Collections.ArrayList]::new() foreach ($item in $Items) { if ([string]::IsNullOrEmpty($searchQuery) -or $item.Label.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { $filteredItems.Add($item) | Out-Null } } # Bounds check if ($selectedIndex -ge $filteredItems.Count) { $selectedIndex = [Math]::Max(0, $filteredItems.Count - 1) } if ($selectedIndex -lt 0) { $selectedIndex = 0 } if ($selectedIndex -lt $scrollOffset) { $scrollOffset = $selectedIndex } if ($selectedIndex -ge ($scrollOffset + $listHeight)) { $scrollOffset = $selectedIndex - $listHeight + 1 } if ($scrollOffset -lt 0) { $scrollOffset = 0 } # Draw Checklist items for ($i = 0; $i -lt $listHeight; $i++) { $itemIdx = $scrollOffset + $i $y = $boxY + 4 + $i if ($itemIdx -lt $filteredItems.Count) { $item = $filteredItems[$itemIdx] $chk = if ($item.Checked) { "[x]" } else { "[ ]" } $text = " $chk $($item.Label) " if ($text.Length -gt ($boxW - 6)) { $text = $text.Substring(0, $boxW - 9) + "..." } $paddedText = $text.PadRight($boxW - 6) if ($itemIdx -eq $selectedIndex) { $colorName = if ($focusedControl -eq 'List') { 'SelectedActive' } else { 'SelectedInactive' } $color = Get-ANSIColor $colorName } else { $color = Get-ANSIColor 'Reset' } Set-Cursor ($boxX + 3) $y [Console]::Write("$color$paddedText$reset") } else { Write-At ($boxX + 3) $y (" " * ($boxW - 6)) } } Write-At ($boxX + 2) ($boxY + $boxH - 3) ("─" * ($boxW - 4)) 'Gray' # Show shortcut guidelines dynamically based on focus if ($focusedControl -eq 'Search') { $shortcuts = " [Tab] List | [Backspace] Delete | [Enter] OK | [Esc] Cancel" } else { $shortcuts = " [Tab] Search | [Space] Toggle | [Arrows] Move | [Enter] OK | [Esc] Cancel" } $paddedShortcuts = $shortcuts.PadRight($boxW - 4) Write-At ($boxX + 2) ($boxY + $boxH - 2) $paddedShortcuts 'Header' # Read Key $key = [Console]::ReadKey($true) if ($key.Key -eq 'Escape') { return $null } if ($key.Key -eq 'Enter') { return $Items } if ($key.Key -eq 'Tab') { if ($focusedControl -eq 'Search') { $focusedControl = 'List' } else { $focusedControl = 'Search' } } elseif ($focusedControl -eq 'Search') { if ($key.Key -eq 'Backspace') { if ($searchQuery.Length -gt 0) { $searchQuery = $searchQuery.Substring(0, $searchQuery.Length - 1) $selectedIndex = 0 $scrollOffset = 0 } } elseif ($key.Key -eq 'Space') { $searchQuery += " " $selectedIndex = 0 $scrollOffset = 0 } else { if ($key.KeyChar -ge 32 -and $key.KeyChar -le 126) { $searchQuery += $key.KeyChar $selectedIndex = 0 $scrollOffset = 0 } } } elseif ($focusedControl -eq 'List') { if ($key.Key -eq 'Space') { if ($filteredItems.Count -gt 0) { $selectedItem = $filteredItems[$selectedIndex] $selectedItem.Checked = -not $selectedItem.Checked } } elseif ($key.Key -eq 'UpArrow') { if ($selectedIndex -gt 0) { $selectedIndex-- } } elseif ($key.Key -eq 'DownArrow') { if ($selectedIndex -lt ($filteredItems.Count - 1)) { $selectedIndex++ } } elseif ($key.Key -eq 'PageUp') { $selectedIndex = [Math]::Max(0, $selectedIndex - $listHeight) } elseif ($key.Key -eq 'PageDown') { $selectedIndex = [Math]::Min($filteredItems.Count - 1, $selectedIndex + $listHeight) } } } } # Export functions # --- MAIN UTILITY DRIVER EXECUTION --- # PowerShell TUI Resource Monitor Main Driver # Run this script to start the Resource Monitor console interface. # Author: Antigravity $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path # --- PERFORMANCE DATA COLLECTOR ENGINE --- $global:perfRunspace = $null $global:perfPowerShell = $null $global:perfAsyncResult = $null function Start-PerformanceDataCollector { $global:sharedData = [hashtable]::Synchronized(@{ NewDataAvailable = $false Running = $true CPUUsage = 0.0 MemoryAvailableBytes = 0.0 MemoryCommittedBytes = 0.0 MemoryCommitLimit = 0.0 MemoryPagesPerSec = 0.0 MemoryPageFaultsPerSec = 0.0 MemoryTotalPhysical = 0.0 MemoryUsedPhysical = 0.0 DiskReadBytesPerSec = 0.0 DiskWriteBytesPerSec = 0.0 DiskPercentTime = 0.0 NetworkBytesTotalPerSec = 0.0 OverviewProcesses = [System.Collections.ArrayList]::new() CPUProcesses = [System.Collections.ArrayList]::new() MemoryProcesses = [System.Collections.ArrayList]::new() DiskProcesses = [System.Collections.ArrayList]::new() NetworkProcesses = [System.Collections.ArrayList]::new() NetworkInterfaces = [System.Collections.ArrayList]::new() ErrorMsg = "" }) $collectorScript = { param($shared) try { Add-Type -AssemblyName Microsoft.VisualBasic -ErrorAction SilentlyContinue $compInfo = New-Object Microsoft.VisualBasic.Devices.ComputerInfo $shared.MemoryTotalPhysical = $compInfo.TotalPhysicalMemory } catch { $shared.MemoryTotalPhysical = 8GB # fallback } $numProcs = [Environment]::ProcessorCount if ($numProcs -le 0) { $numProcs = 1 } $warmupCounters = @( '\Processor(_Total)\% Processor Time', '\Process(*)\% Processor Time' ) Get-Counter -Counter $warmupCounters -ErrorAction SilentlyContinue | Out-Null while ($shared.Running) { $startLoop = Get-Date try { $counters = @( '\Processor(_Total)\% Processor Time', '\Memory\Available Bytes', '\Memory\Committed Bytes', '\Memory\Commit Limit', '\Memory\Pages/sec', '\Memory\Page Faults/sec', '\PhysicalDisk(_Total)\Disk Read Bytes/sec', '\PhysicalDisk(_Total)\Disk Write Bytes/sec', '\PhysicalDisk(_Total)\% Disk Time', '\Network Interface(*)\Bytes Total/sec', '\Process(*)\ID Process', '\Process(*)\% Processor Time', '\Process(*)\Working Set', '\Process(*)\Working Set - Private', '\Process(*)\Private Bytes', '\Process(*)\Thread Count', '\Process(*)\Handle Count', '\Process(*)\IO Read Bytes/sec', '\Process(*)\IO Write Bytes/sec', '\Process(*)\IO Other Bytes/sec', '\Process(*)\Page Faults/sec' ) $data = Get-Counter -Counter $counters -ErrorAction SilentlyContinue if ($null -ne $data) { $procHashes = @{} $netInterfaces = @{} $netTotal = 0.0 foreach ($sample in $data.CounterSamples) { $path = $sample.Path $cooked = $sample.CookedValue if ($path -like '*\processor(_total)\% processor time*') { $shared.CPUUsage = $cooked } elseif ($path -like '*\memory\available bytes*') { $shared.MemoryAvailableBytes = $cooked $shared.MemoryUsedPhysical = $shared.MemoryTotalPhysical - $cooked } elseif ($path -like '*\memory\committed bytes*') { $shared.MemoryCommittedBytes = $cooked } elseif ($path -like '*\memory\commit limit*') { $shared.MemoryCommitLimit = $cooked } elseif ($path -like '*\memory\pages/sec*') { $shared.MemoryPagesPerSec = $cooked } elseif ($path -like '*\memory\page faults/sec*') { $shared.MemoryPageFaultsPerSec = $cooked } elseif ($path -like '*\physicaldisk(_total)\disk read bytes/sec*') { $shared.DiskReadBytesPerSec = $cooked } elseif ($path -like '*\physicaldisk(_total)\disk write bytes/sec*') { $shared.DiskWriteBytesPerSec = $cooked } elseif ($path -like '*\physicaldisk(_total)\% disk time*') { $shared.DiskPercentTime = $cooked } elseif ($path -match '\\network interface\(([^)]+)\)\\bytes total/sec') { $interfaceName = $Matches[1] if ($interfaceName -ne '_total') { $netInterfaces[$interfaceName] = $cooked } $netTotal += $cooked } elseif ($path -match '\\process\(([^)]+)\)\\(.+)$') { $instance = $Matches[1] $counterName = $Matches[2].ToLower().Trim() if ($instance -eq '_total' -or $instance -eq 'idle') { continue } if (-not $procHashes.ContainsKey($instance)) { $procHashes[$instance] = @{ Instance = $instance Name = $instance -replace '#\d+$', '' PID = $null CPU = 0.0 WS = 0.0 Private = 0.0 Commit = 0.0 Threads = 0 Handles = 0 IORead = 0.0 IOWrite = 0.0 Network = 0.0 PageFaults = 0.0 } } switch ($counterName) { 'id process' { $procHashes[$instance]['PID'] = [int]$cooked } '% processor time' { $procHashes[$instance]['CPU'] = [double]$cooked / $numProcs } 'working set' { $procHashes[$instance]['WS'] = [double]$cooked } 'working set - private' { $procHashes[$instance]['Private'] = [double]$cooked } 'private bytes' { $procHashes[$instance]['Commit'] = [double]$cooked } 'thread count' { $procHashes[$instance]['Threads'] = [int]$cooked } 'handle count' { $procHashes[$instance]['Handles'] = [int]$cooked } 'io read bytes/sec' { $procHashes[$instance]['IORead'] = [double]$cooked } 'io write bytes/sec' { $procHashes[$instance]['IOWrite'] = [double]$cooked } 'io other bytes/sec' { $procHashes[$instance]['Network'] = [double]$cooked } 'page faults/sec' { $procHashes[$instance]['PageFaults'] = [double]$cooked } } } } $shared.NetworkBytesTotalPerSec = $netTotal $allProcs = [System.Collections.ArrayList]::new() foreach ($h in $procHashes.Values) { if ($null -ne $h['PID'] -and $h['PID'] -gt 0) { # Calculate total disk for overview $h['DiskTotal'] = $h['IORead'] + $h['IOWrite'] $allProcs.Add([PSCustomObject]$h) | Out-Null } } $shared.OverviewProcesses = [System.Collections.ArrayList]::new(($allProcs | Sort-Object CPU -Descending)) $shared.CPUProcesses = $shared.OverviewProcesses $shared.MemoryProcesses = [System.Collections.ArrayList]::new(($allProcs | Sort-Object WS -Descending)) $shared.DiskProcesses = [System.Collections.ArrayList]::new(($allProcs | Sort-Object DiskTotal -Descending)) $shared.NetworkProcesses = [System.Collections.ArrayList]::new(($allProcs | Sort-Object Network -Descending)) $netInterfaceList = [System.Collections.ArrayList]::new() foreach ($k in $netInterfaces.Keys) { $netInterfaceList.Add([PSCustomObject]@{ Interface = $k BytesTotalPerSec = $netInterfaces[$k] }) | Out-Null } $shared.NetworkInterfaces = $netInterfaceList $shared.NewDataAvailable = $true $shared.ErrorMsg = "" } } catch { $shared.ErrorMsg = $_.ToString() } $elapsed = (Get-Date) - $startLoop $sleepMs = 1000 - $elapsed.TotalMilliseconds if ($sleepMs -lt 100) { $sleepMs = 100 } $sleepSteps = [int]($sleepMs / 50) for ($s = 0; $s -lt $sleepSteps; $s++) { if (-not $shared.Running) { break } Start-Sleep -Milliseconds 50 } } } $global:perfRunspace = [runspacefactory]::CreateRunspace() $global:perfRunspace.Open() $global:perfPowerShell = [PowerShell]::Create() $global:perfPowerShell.Runspace = $global:perfRunspace [void]$global:perfPowerShell.AddScript($collectorScript).AddArgument($global:sharedData) $global:perfAsyncResult = $global:perfPowerShell.BeginInvoke() } function Stop-PerformanceDataCollector { if ($null -ne $global:sharedData) { $global:sharedData.Running = $false } if ($null -ne $global:perfPowerShell) { try { $global:perfPowerShell.EndInvoke($global:perfAsyncResult) } catch {} $global:perfPowerShell.Dispose() $global:perfPowerShell = $null } if ($null -ne $global:perfRunspace) { $global:perfRunspace.Close() $global:perfRunspace.Dispose() $global:perfRunspace = $null } } function Get-ActiveResMonData { if ($null -eq $global:sharedData) { return $false } if ($global:sharedData.NewDataAvailable) { $global:sharedData.NewDataAvailable = $false $global:cpuUsage = $global:sharedData.CPUUsage $global:memAvailable = $global:sharedData.MemoryAvailableBytes $global:memCommitted = $global:sharedData.MemoryCommittedBytes $global:memCommitLimit = $global:sharedData.MemoryCommitLimit $global:memPagesPerSec = $global:sharedData.MemoryPagesPerSec $global:memPageFaults = $global:sharedData.MemoryPageFaultsPerSec $global:memTotal = $global:sharedData.MemoryTotalPhysical $global:memUsed = $global:sharedData.MemoryUsedPhysical $global:diskRead = $global:sharedData.DiskReadBytesPerSec $global:diskWrite = $global:sharedData.DiskWriteBytesPerSec $global:diskActive = $global:sharedData.DiskPercentTime $global:networkTotal = $global:sharedData.NetworkBytesTotalPerSec $global:networkInterfaces = $global:sharedData.NetworkInterfaces.Clone() $global:overviewProcs = $global:sharedData.OverviewProcesses.Clone() $global:cpuProcs = $global:sharedData.CPUProcesses.Clone() $global:memProcs = $global:sharedData.MemoryProcesses.Clone() $global:diskProcs = $global:sharedData.DiskProcesses.Clone() $global:networkProcs = $global:sharedData.NetworkProcesses.Clone() return $true } return $false } # --- ROLLING GRAPH RENDERER --- function Draw-RollingGraph($startX, $startY, $width, $height, $history, $maxVal, $colorName, $unit) { if ($maxVal -le 0) { $maxVal = 1 } $axisWidth = 0 if ($width -ge 18) { $axisWidth = 11 } $drawWidth = $width - $axisWidth $historyCount = if ($null -eq $history) { 0 } else { $history.Count } $data = [double[]]::new($drawWidth) for ($i = 0; $i -lt $drawWidth; $i++) { $histIndex = $historyCount - $drawWidth + $i if ($histIndex -ge 0 -and $histIndex -lt $historyCount) { $data[$i] = [double]$history[$histIndex] } else { $data[$i] = 0.0 } } $reset = Get-ANSIColor 'Reset' $color = Get-ANSIColor $colorName $axisColor = Get-ANSIColor 'Gray' for ($row = $height - 1; $row -ge 0; $row--) { $axisPart = "" if ($axisWidth -gt 0) { $label = "" $tick = "│" if ($row -eq $height - 1) { if ($unit -eq '%') { $label = "100 %" } else { if ($maxVal -ge 1GB) { $maxStr = "{0:N1} GB" -f ($maxVal / 1GB) } elseif ($maxVal -ge 1MB) { $maxStr = "{0:N1} MB" -f ($maxVal / 1MB) } elseif ($maxVal -ge 1KB) { $maxStr = "{0:N1} KB" -f ($maxVal / 1KB) } else { $maxStr = "{0:N0} B" -f $maxVal } $label = "$maxStr/s" } $tick = "┤" } elseif ($row -eq 0) { if ($unit -eq '%') { $label = "0 %" } else { $label = "0 B/s" } $tick = "┴" } elseif ($row -eq [int]($height / 2)) { if ($unit -eq '%') { $label = "50 %" } else { $midVal = $maxVal / 2 if ($midVal -ge 1GB) { $midStr = "{0:N1} GB" -f ($midVal / 1GB) } elseif ($midVal -ge 1MB) { $midStr = "{0:N1} MB" -f ($midVal / 1MB) } elseif ($midVal -ge 1KB) { $midStr = "{0:N1} KB" -f ($midVal / 1KB) } else { $midStr = "{0:N0} B" -f $midVal } $label = "$midStr/s" } $tick = "┤" } $paddedLabel = $label.PadLeft(10) $axisPart = "$axisColor$paddedLabel$tick$reset" } $lineChars = [char[]]::new($drawWidth) for ($col = 0; $col -lt $drawWidth; $col++) { $val = $data[$col] $h = ($val / $maxVal) * $height if ($h -gt $row) { $lineChars[$col] = '+' } else { $lineChars[$col] = ' ' } } $y = $startY + ($height - 1 - $row) Set-Cursor $startX $y $rowStr = [string]::new($lineChars) [Console]::Write("$axisPart$color$rowStr$reset") } } # --- DETAILS / HEALTH ALERTS PANEL --- function Draw-ResMonSummary($tabIndex, $width, $height) { $startX = $global:leftWidth + 2 $startY = $global:mainHeight + 2 $summaryHeight = $height - $global:mainHeight - 4 if ($summaryHeight -lt 1) { return } $reset = Get-ANSIColor 'Reset' $lines = [System.Collections.ArrayList]::new() $alertColor = 'Reset' $alertStartIdx = $null switch ($tabIndex) { 0 { $lines.Add(" SYSTEM OVERVIEW METRICS") | Out-Null $lines.Add(" ───────────────────────") | Out-Null $lines.Add(" Global CPU Usage: $("{0:F1} %" -f $global:cpuUsage)") | Out-Null $memPct = if ($global:memTotal -gt 0) { ($global:memUsed / $global:memTotal) * 100 } else { 0 } $lines.Add(" Physical RAM Used: $(Format-Bytes $global:memUsed) / $(Format-Bytes $global:memTotal) ($("{0:F1} %" -f $memPct))") | Out-Null $lines.Add(" Commit Charge: $(Format-Bytes $global:memCommitted) / $(Format-Bytes $global:memCommitLimit)") | Out-Null $lines.Add(" Disk Read/Write: Read: $(Format-Bytes $global:diskRead)/s | Write: $(Format-Bytes $global:diskWrite)/s") | Out-Null $lines.Add(" Disk Active Time: $("{0:F1} %" -f $global:diskActive)") | Out-Null $lines.Add(" Network Bandwidth: Total: $(Format-Bytes $global:networkTotal)/s") | Out-Null } 1 { $lines.Add(" CPU METRICS & SUMMARY") | Out-Null $lines.Add(" ─────────────────────") | Out-Null $lines.Add(" Logical Processor Count: $([Environment]::ProcessorCount)") | Out-Null $lines.Add(" Overall CPU Usage: $("{0:F1} %" -f $global:cpuUsage)") | Out-Null if ($global:cpuProcs -and $global:cpuProcs.Count -gt 0) { $top = $global:cpuProcs[0] $lines.Add(" Top Process CPU Hog: $($top.Name) (PID: $($top.PID)) - $("{0:F1} %" -f $top.CPU)") | Out-Null } else { $lines.Add(" Top Process CPU Hog: None") | Out-Null } } 2 { $lines.Add(" PHYSICAL RAM & SWAP PAGE FILE") | Out-Null $lines.Add(" ─────────────────────────────") | Out-Null $lines.Add(" Available Physical Memory: $(Format-Bytes $global:memAvailable)") | Out-Null $lines.Add(" Used Physical Memory: $(Format-Bytes $global:memUsed) / $(Format-Bytes $global:memTotal)") | Out-Null $commitPct = if ($global:memCommitLimit -gt 0) { ($global:memCommitted / $global:memCommitLimit) * 100 } else { 0 } $lines.Add(" Commit Charge Status: $(Format-Bytes $global:memCommitted) / $(Format-Bytes $global:memCommitLimit) ($("{0:F1} %" -f $commitPct))") | Out-Null $lines.Add(" Memory Paging Rates: Pages/sec: $("{0:F1}" -f $global:memPagesPerSec) | Page Faults/sec: $("{0:F1}" -f $global:memPageFaults)") | Out-Null $lines.Add("") | Out-Null $isExhausted = ($global:memAvailable -lt 512MB) -or ($commitPct -gt 95) $isThrashing = ($commitPct -gt 80) -and (($global:memPagesPerSec -gt 150) -or ($global:memPageFaults -gt 8000)) if ($isExhausted) { $lines.Add(" ┌─────────────────────────────────────────────────────────────┐") | Out-Null $lines.Add(" │ CRITICAL ALERT: PHYSICAL MEMORY EXHAUSTED │") | Out-Null $lines.Add(" │ Available RAM is critically low. Close active programs. │") | Out-Null $lines.Add(" └─────────────────────────────────────────────────────────────┘") | Out-Null $alertColor = 'Error' $alertStartIdx = $lines.Count - 4 } elseif ($isThrashing) { $lines.Add(" ┌─────────────────────────────────────────────────────────────┐") | Out-Null $lines.Add(" │ WARNING ALERT: RAM THRASHING DETECTED │") | Out-Null $lines.Add(" │ High paging rates. Hard disk swapping is slowing down. │") | Out-Null $lines.Add(" └─────────────────────────────────────────────────────────────┘") | Out-Null $alertColor = 'Warning' $alertStartIdx = $lines.Count - 4 } else { $lines.Add(" ┌─────────────────────────────────────────────────────────────┐") | Out-Null $lines.Add(" │ STATUS: SYSTEM MEMORY HEALTHY │") | Out-Null $lines.Add(" │ RAM utilization and paging activity are within normal range.│") | Out-Null $lines.Add(" └─────────────────────────────────────────────────────────────┘") | Out-Null $alertColor = 'Info' $alertStartIdx = $lines.Count - 4 } } 3 { $lines.Add(" DISK ACTIVITY METRICS") | Out-Null $lines.Add(" ─────────────────────") | Out-Null $lines.Add(" Total Disk Read Rate: $(Format-Bytes $global:diskRead)/s") | Out-Null $lines.Add(" Total Disk Write Rate: $(Format-Bytes $global:diskWrite)/s") | Out-Null $lines.Add(" Physical Disk Active %: $("{0:F1} %" -f $global:diskActive)") | Out-Null } 4 { $lines.Add(" NETWORK BANDWIDTH SUMMARY") | Out-Null $lines.Add(" ─────────────────────────") | Out-Null $lines.Add(" Total Interface Activity: $(Format-Bytes $global:networkTotal)/s") | Out-Null $lines.Add("") | Out-Null $lines.Add(" Network Interfaces:") | Out-Null $activeIfaces = $global:networkInterfaces | Where-Object { $_.BytesTotalPerSec -gt 0 } | Sort-Object BytesTotalPerSec -Descending if ($activeIfaces) { foreach ($iface in $activeIfaces) { $lines.Add(" - $($iface.Interface): $(Format-Bytes $iface.BytesTotalPerSec)/s") | Out-Null } } else { $allIfaces = $global:networkInterfaces | Sort-Object Interface if ($allIfaces) { foreach ($iface in ($allIfaces | Select-Object -First 3)) { $lines.Add(" - $($iface.Interface): 0 B/s") | Out-Null } } else { $lines.Add(" - No network interfaces detected.") | Out-Null } } } } for ($i = 0; $i -lt $summaryHeight; $i++) { $y = $startY + $i $lineText = "" $color = 'Reset' if ($i -lt $lines.Count) { $lineText = $lines[$i] if ($tabIndex -eq 2 -and $null -ne $alertStartIdx -and $i -ge $alertStartIdx) { $color = $alertColor } } $padded = $lineText.PadRight($width).Substring(0, $width) Write-At $startX $y $padded $color } } # --- STATE VARIABLES --- $global:resMonSelectedIndex = 0 $resMonSelectedIndex = 0 $resMonScrollOffset = 0 $global:cpuUsage = 0.0 $global:memAvailable = 0.0 $global:memCommitted = 0.0 $global:memCommitLimit = 0.0 $global:memPagesPerSec = 0.0 $global:memPageFaults = 0.0 $global:memTotal = 0.0 $global:memUsed = 0.0 $global:diskRead = 0.0 $global:diskWrite = 0.0 $global:diskActive = 0.0 $global:networkTotal = 0.0 $global:overviewProcs = [System.Collections.ArrayList]::new() $global:cpuProcs = [System.Collections.ArrayList]::new() $global:memProcs = [System.Collections.ArrayList]::new() $global:diskProcs = [System.Collections.ArrayList]::new() $global:networkProcs = [System.Collections.ArrayList]::new() $global:networkInterfaces = [System.Collections.ArrayList]::new() $global:cpuHistory = [System.Collections.ArrayList]::new() $global:memHistory = [System.Collections.ArrayList]::new() $global:diskHistory = [System.Collections.ArrayList]::new() $global:networkHistory = [System.Collections.ArrayList]::new() $resMonTabs = [System.Collections.ArrayList]::new(@( [PSCustomObject]@{ Label = "Overview"; Level = 0; IsLeaf = $true } [PSCustomObject]@{ Label = "CPU"; Level = 0; IsLeaf = $true } [PSCustomObject]@{ Label = "Memory"; Level = 0; IsLeaf = $true } [PSCustomObject]@{ Label = "Disk"; Level = 0; IsLeaf = $true } [PSCustomObject]@{ Label = "Network"; Level = 0; IsLeaf = $true } )) # Columns config $overviewCols = @( @{ Label = "Image Name"; Prop = "Name"; Align = "Left"; Width = 15 } @{ Label = "PID"; Prop = "PID"; Align = "Right"; Width = 8 } @{ Label = "CPU %"; Prop = "CPU"; Align = "Right"; Width = 8; Format = "Percent" } @{ Label = "Working Set"; Prop = "WS"; Align = "Right"; Width = 12; Format = "MB" } @{ Label = "Disk Total"; Prop = "DiskTotal"; Align = "Right"; Width = 12; Format = "Bytes" } ) $cpuCols = @( @{ Label = "Image Name"; Prop = "Name"; Align = "Left"; Width = 15 } @{ Label = "PID"; Prop = "PID"; Align = "Right"; Width = 8 } @{ Label = "CPU %"; Prop = "CPU"; Align = "Right"; Width = 8; Format = "Percent" } @{ Label = "Threads"; Prop = "Threads"; Align = "Right"; Width = 8 } @{ Label = "Handles"; Prop = "Handles"; Align = "Right"; Width = 8 } ) $memCols = @( @{ Label = "Image Name"; Prop = "Name"; Align = "Left"; Width = 15 } @{ Label = "PID"; Prop = "PID"; Align = "Right"; Width = 8 } @{ Label = "Commit"; Prop = "Commit"; Align = "Right"; Width = 12; Format = "MB" } @{ Label = "Working Set"; Prop = "WS"; Align = "Right"; Width = 12; Format = "MB" } @{ Label = "Private"; Prop = "Private"; Align = "Right"; Width = 12; Format = "MB" } @{ Label = "Page Faults"; Prop = "PageFaults"; Align = "Right"; Width = 12; Format = "Decimal" } ) $diskCols = @( @{ Label = "Image Name"; Prop = "Name"; Align = "Left"; Width = 15 } @{ Label = "PID"; Prop = "PID"; Align = "Right"; Width = 8 } @{ Label = "Read Bytes"; Prop = "IORead"; Align = "Right"; Width = 12; Format = "Bytes" } @{ Label = "Write Bytes"; Prop = "IOWrite"; Align = "Right"; Width = 12; Format = "Bytes" } @{ Label = "Total Bytes"; Prop = "DiskTotal"; Align = "Right"; Width = 12; Format = "Bytes" } ) $networkCols = @( @{ Label = "Image Name"; Prop = "Name"; Align = "Left"; Width = 15 } @{ Label = "PID"; Prop = "PID"; Align = "Right"; Width = 8 } @{ Label = "I/O Other"; Prop = "Network"; Align = "Right"; Width = 15; Format = "Bytes" } ) # Start background performance runspace collector Start-PerformanceDataCollector $width = [Console]::WindowWidth $height = [Console]::WindowHeight Update-LayoutDimensions $width $height $rightWidth = $width - $global:leftWidth - 4 $global:focusArea = 0 Initialize-Console [Console]::Write("$ESC[2J") $redrawAll = $true $needsTreeRedraw = $true $needsTableRedraw = $true $needsDetailsRedraw = $true $needsStatusRedraw = $true $statusText = "Gathering telemetry... Use Up/Down to navigate tabs." try { while ($true) { # 1. Resize Check $newWidth = [Console]::WindowWidth $newHeight = [Console]::WindowHeight if ($newWidth -ne $width -or $newHeight -ne $height) { $width = $newWidth $height = $newHeight Update-LayoutDimensions $width $height $rightWidth = $width - $global:leftWidth - 4 [Console]::Write("$ESC[2J") $redrawAll = $true } # 2. Telemetry Refresh (once per second) if (Get-ActiveResMonData) { $tableWidth = [int]($rightWidth * 0.6) $graphWidth = $rightWidth - $tableWidth - 1 $maxHistory = $graphWidth if ($maxHistory -lt 10) { $maxHistory = 10 } # CPU $global:cpuHistory.Add($global:cpuUsage) | Out-Null while ($global:cpuHistory.Count -gt $maxHistory) { $global:cpuHistory.RemoveAt(0) } # Memory $memPercent = if ($global:memTotal -gt 0) { ($global:memUsed / $global:memTotal) * 100 } else { 0 } $global:memHistory.Add($memPercent) | Out-Null while ($global:memHistory.Count -gt $maxHistory) { $global:memHistory.RemoveAt(0) } # Disk $global:diskHistory.Add($global:diskActive) | Out-Null while ($global:diskHistory.Count -gt $maxHistory) { $global:diskHistory.RemoveAt(0) } # Network $global:networkHistory.Add($global:networkTotal) | Out-Null while ($global:networkHistory.Count -gt $maxHistory) { $global:networkHistory.RemoveAt(0) } $needsTableRedraw = $true $needsDetailsRedraw = $true } # 3. Redraw Screen if ($redrawAll) { Draw-Borders $width $height $global:leftWidth $global:mainHeight $global:focusArea "CATEGORIES" "PROCESS LIST" "SYSTEM SUMMARY" -RightTopWidthPercent 0.6 Draw-Menu $width @("Ctrl+1: Overview", "Ctrl+2: CPU", "Ctrl+3: Memory", "Ctrl+4: Disk", "Ctrl+5: Network", "Ctrl+Q: Exit") $needsTreeRedraw = $true $needsTableRedraw = $true $needsDetailsRedraw = $true $needsStatusRedraw = $true $redrawAll = $false } if ($needsTreeRedraw) { # Use Tree component mapping Draw-TreeView $resMonTabs $global:resMonSelectedIndex 0 $global:leftWidth ($height - 4) ($global:focusArea -eq 0) $needsTreeRedraw = $false } if ($needsTableRedraw) { $tableWidth = [int]($rightWidth * 0.6) $cols = switch ($global:resMonSelectedIndex) { 0 { $overviewCols } 1 { $cpuCols } 2 { $memCols } 3 { $diskCols } 4 { $networkCols } } $activeProcs = switch ($global:resMonSelectedIndex) { 0 { $global:overviewProcs } 1 { $global:cpuProcs } 2 { $global:memProcs } 3 { $global:diskProcs } 4 { $global:networkProcs } } # Dynamically size 'Name' column to fill table width $totalW = 0 foreach ($col in $cols) { if ($col.Prop -ne 'Name') { $totalW += $col.Width + 1 } } $nameCol = $cols | Where-Object { $_.Prop -eq 'Name' } if ($nameCol) { $nameCol.Width = $tableWidth - $totalW - 1 if ($nameCol.Width -lt 8) { $nameCol.Width = 8 } } Draw-TableHeader ($global:leftWidth + 2) 2 $cols $tableWidth Draw-TableRows $activeProcs $resMonSelectedIndex $resMonScrollOffset $cols ($global:leftWidth + 2) 3 $tableWidth ($global:mainHeight - 2) ($global:focusArea -eq 1) # Draw Rolling Graph $graphWidth = $rightWidth - $tableWidth - 1 $graphX = $global:leftWidth + 3 + $tableWidth $graphHistory = switch ($global:resMonSelectedIndex) { 0 { $global:cpuHistory } 1 { $global:cpuHistory } 2 { $global:memHistory } 3 { $global:diskHistory } 4 { $global:networkHistory } } $maxVal = switch ($global:resMonSelectedIndex) { 0 { 100.0 } 1 { 100.0 } 2 { 100.0 } 3 { 100.0 } 4 { $maxN = 100KB foreach ($v in $global:networkHistory) { if ($v -gt $maxN) { $maxN = $v } } $maxN } } $colorName = switch ($global:resMonSelectedIndex) { 0 { 'Cyan' } 1 { 'Cyan' } 2 { 'Warning' } 3 { 'Info' } 4 { 'Blue' } } $unit = if ($global:resMonSelectedIndex -eq 4) { '/s' } else { '%' } Draw-RollingGraph $graphX 2 $graphWidth ($global:mainHeight - 1) $graphHistory $maxVal $colorName $unit $needsTableRedraw = $false } if ($needsDetailsRedraw) { Draw-ResMonSummary $global:resMonSelectedIndex $rightWidth $height $needsDetailsRedraw = $false } if ($needsStatusRedraw) { $focusTxt = switch ($global:focusArea) { 0 { "Categories" } 1 { "Process List" } 2 { "Summary" } } Draw-Status $statusText $focusTxt "Tab: Switch Pane" $width $height $needsStatusRedraw = $false } Set-Cursor 0 0 # 4. Input handling if (-not [Console]::KeyAvailable) { Start-Sleep -Milliseconds 15 continue } $key = [Console]::ReadKey($true) $isCtrl = ($key.Modifiers -band [System.ConsoleModifiers]::Control) -eq [System.ConsoleModifiers]::Control if ($isCtrl) { if ($key.Key -eq 'D1') { $global:resMonSelectedIndex = 0 $resMonSelectedIndex = 0 $resMonScrollOffset = 0 $statusText = "Selected Overview category." $redrawAll = $true continue } if ($key.Key -eq 'D2') { $global:resMonSelectedIndex = 1 $resMonSelectedIndex = 0 $resMonScrollOffset = 0 $statusText = "Selected CPU category." $redrawAll = $true continue } if ($key.Key -eq 'D3') { $global:resMonSelectedIndex = 2 $resMonSelectedIndex = 0 $resMonScrollOffset = 0 $statusText = "Selected Memory category." $redrawAll = $true continue } if ($key.Key -eq 'D4') { $global:resMonSelectedIndex = 3 $resMonSelectedIndex = 0 $resMonScrollOffset = 0 $statusText = "Selected Disk category." $redrawAll = $true continue } if ($key.Key -eq 'D5') { $global:resMonSelectedIndex = 4 $resMonSelectedIndex = 0 $resMonScrollOffset = 0 $statusText = "Selected Network category." $redrawAll = $true continue } if ($key.Key -eq 'Q') { break } } # Focus Panel keys if ($global:focusArea -eq 0) { # Categories if ($key.Key -eq 'UpArrow') { if ($global:resMonSelectedIndex -gt 0) { $global:resMonSelectedIndex-- $resMonSelectedIndex = 0 $resMonScrollOffset = 0 $needsTreeRedraw = $true $needsTableRedraw = $true $needsDetailsRedraw = $true } } elseif ($key.Key -eq 'DownArrow') { if ($global:resMonSelectedIndex -lt 4) { $global:resMonSelectedIndex++ $resMonSelectedIndex = 0 $resMonScrollOffset = 0 $needsTreeRedraw = $true $needsTableRedraw = $true $needsDetailsRedraw = $true } } elseif ($key.Key -eq 'Tab') { $global:focusArea = 1 $redrawAll = $true } } elseif ($global:focusArea -eq 1) { # Process List $tableHeight = $global:mainHeight - 1 $activeProcs = switch ($global:resMonSelectedIndex) { 0 { $global:overviewProcs } 1 { $global:cpuProcs } 2 { $global:memProcs } 3 { $global:diskProcs } 4 { $global:networkProcs } } $procsCount = if ($null -eq $activeProcs) { 0 } else { $activeProcs.Count } if ($key.Key -eq 'UpArrow') { if ($resMonSelectedIndex -gt 0) { $resMonSelectedIndex-- if ($resMonSelectedIndex -lt $resMonScrollOffset) { $resMonScrollOffset = $resMonSelectedIndex } $needsTableRedraw = $true } } elseif ($key.Key -eq 'DownArrow') { if ($resMonSelectedIndex -lt ($procsCount - 1)) { $resMonSelectedIndex++ if ($resMonSelectedIndex -ge ($resMonScrollOffset + $tableHeight)) { $resMonScrollOffset = $resMonSelectedIndex - $tableHeight + 1 } $needsTableRedraw = $true } } elseif ($key.Key -eq 'PageUp') { $resMonSelectedIndex = [Math]::Max(0, $resMonSelectedIndex - $tableHeight) $resMonScrollOffset = [Math]::Max(0, $resMonScrollOffset - $tableHeight) if ($resMonSelectedIndex -lt $resMonScrollOffset) { $resMonSelectedIndex = $resMonScrollOffset } $needsTableRedraw = $true } elseif ($key.Key -eq 'PageDown') { $resMonSelectedIndex = [Math]::Min($procsCount - 1, $resMonSelectedIndex + $tableHeight) $resMonScrollOffset = [Math]::Min($procsCount - $tableHeight, $resMonScrollOffset + $tableHeight) if ($resMonScrollOffset -lt 0) { $resMonScrollOffset = 0 } if ($resMonSelectedIndex -ge ($resMonScrollOffset + $tableHeight)) { $resMonSelectedIndex = $resMonScrollOffset + $tableHeight - 1 } $needsTableRedraw = $true } elseif ($key.Key -eq 'Escape') { $global:focusArea = 0 $redrawAll = $true } elseif ($key.Key -eq 'Tab') { $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift $global:focusArea = if ($isShift) { 0 } else { 2 } $redrawAll = $true } } elseif ($global:focusArea -eq 2) { # Summary if ($key.Key -eq 'Tab') { $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift $global:focusArea = if ($isShift) { 1 } else { 0 } $redrawAll = $true } elseif ($key.Key -eq 'Escape') { $global:focusArea = 0 $redrawAll = $true } } } } finally { Stop-PerformanceDataCollector Restore-Console } |