Public/system/Show-SystemMonitor.ps1
|
#Requires -Version 5.1 function Show-SystemMonitor { <# .SYNOPSIS Displays an interactive real-time system monitor inspired by htop .DESCRIPTION Renders a full-screen terminal UI showing per-core CPU usage bars, memory and page file utilization, and a sortable process list refreshed at a configurable interval. Designed for use over SSH, remoting sessions, or any terminal where Task Manager is not available. Press Q to quit, or use C/M/P/N keys to change the sort column interactively. .PARAMETER RefreshInterval Number of seconds between display updates. Valid range is 1 to 60. Defaults to 2 seconds. .PARAMETER ProcessCount Maximum number of processes to display. Valid range is 5 to 100. Defaults to 25. .PARAMETER NoColor Disables ANSI color output for terminals that do not support escape sequences. .EXAMPLE Show-SystemMonitor Launches the monitor with default settings (2-second refresh, top 25 processes). .EXAMPLE Show-SystemMonitor -RefreshInterval 5 -ProcessCount 40 Refreshes every 5 seconds and shows the top 40 processes. .EXAMPLE Show-SystemMonitor -NoColor Launches without color for terminals that do not support ANSI escape sequences. .OUTPUTS None. This function renders an interactive TUI and does not produce pipeline output. .NOTES Author: Franck SALLET Version: 1.3.0 Last Modified: 2026-04-06 Requires: PowerShell 5.1+ / Windows only Requires: Interactive console (not ISE or redirected output) .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/powershell/module/cimcmdlets/get-ciminstance #> [CmdletBinding()] # Variables defined in begin{} are used in process{} via string interpolation "${var}" # and [Console] method calls — PSScriptAnalyzer cannot track cross-block or interpolation usage [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param( [Parameter()] [ValidateRange(1, 60)] [int]$RefreshInterval = 2, [Parameter()] [ValidateRange(5, 100)] [int]$ProcessCount = 25, [Parameter()] [switch]$NoColor ) begin { # ---- Console check ---- if ($Host.Name -eq 'Windows PowerShell ISE Host') { Write-Error -Message "[$($MyInvocation.MyCommand)] ISE is not supported. Use Windows Terminal, ConHost, or a remote SSH session." return } $sortMode = 'CPU' $running = $true } process { if ($Host.Name -eq 'Windows PowerShell ISE Host') { return } $previousCtrlC = [Console]::TreatControlCAsInput $previousCursorVisible = [Console]::CursorVisible try { [Console]::TreatControlCAsInput = $true [Console]::CursorVisible = $false [Console]::Clear() while ($running) { $frameStart = [Diagnostics.Stopwatch]::StartNew() # ============================================================ # DATA GATHERING # ============================================================ $os = Get-CimInstance -ClassName 'Win32_OperatingSystem' -ErrorAction SilentlyContinue # Guard against WMI failure — skip frame instead of crashing if (-not $os) { Start-Sleep -Seconds 1 continue } $cpuCores = @( Get-CimInstance -ClassName 'Win32_PerfFormattedData_PerfOS_Processor' -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne '_Total' } | Sort-Object { [int]$_.Name } ) $cpuTotal = Get-CimInstance -ClassName 'Win32_PerfFormattedData_PerfOS_Processor' -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq '_Total' } $processes = @( Get-CimInstance -ClassName 'Win32_PerfFormattedData_PerfProc_Process' -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne '_Total' -and $_.Name -ne 'Idle' -and $_.IDProcess -ne 0 } ) $coreCount = [math]::Max(1, $cpuCores.Count) $width = [math]::Max(80, [Console]::WindowWidth) $height = [math]::Max(24, [Console]::WindowHeight) # Memory (KB) $totalMemKB = $os.TotalVisibleMemorySize $freeMemKB = $os.FreePhysicalMemory $usedMemKB = $totalMemKB - $freeMemKB $memPercent = [math]::Round(($usedMemKB / $totalMemKB) * 100) # Page file (KB) $totalPageKB = $os.SizeStoredInPagingFiles $freePageKB = $os.FreeSpaceInPagingFiles $usedPageKB = $totalPageKB - $freePageKB $pagePercent = if ($totalPageKB -gt 0) { [math]::Round(($usedPageKB / $totalPageKB) * 100) } else { 0 } # Uptime $uptime = (Get-Date) - $os.LastBootUpTime $uptimeStr = '{0}d {1:D2}:{2:D2}:{3:D2}' -f $uptime.Days, $uptime.Hours, $uptime.Minutes, $uptime.Seconds # Total CPU % $totalCpuPercent = if ($cpuTotal) { [int]$cpuTotal.PercentProcessorTime } else { 0 } # Process list $procList = foreach ($proc in $processes) { $cpuPct = [math]::Round($proc.PercentProcessorTime / $coreCount, 1) $memMB = [math]::Round($proc.WorkingSetPrivate / 1MB, 1) $cleanName = $proc.Name -replace '#\d+$', '' [PSCustomObject]@{ PID = $proc.IDProcess CPU = $cpuPct MemMB = $memMB Name = $cleanName } } $sortedProcs = switch ($sortMode) { 'Memory' { $procList | Sort-Object -Property 'MemMB' -Descending } 'PID' { $procList | Sort-Object -Property 'PID' } 'Name' { $procList | Sort-Object -Property 'Name' } default { $procList | Sort-Object -Property 'CPU' -Descending } } $topProcs = @($sortedProcs | Select-Object -First $ProcessCount) # ============================================================ # FRAME RENDERING — delegate to pure formatter # ============================================================ $timeStr = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $frameContent = Format-SystemMonitorFrame ` -CpuTotalPercent $totalCpuPercent ` -CpuCores $cpuCores ` -MemPercent $memPercent ` -MemUsedKB $usedMemKB ` -MemTotalKB $totalMemKB ` -PagePercent $pagePercent ` -PageUsedKB $usedPageKB ` -PageTotalKB $totalPageKB ` -UptimeStr $uptimeStr ` -TimeStr $timeStr ` -ProcessCount $processes.Count ` -TopProcesses $topProcs ` -SortMode $sortMode ` -Width $width ` -Height $height ` -RefreshInterval $RefreshInterval ` -NoColor:$NoColor [Console]::Write($frameContent) $frameStart.Stop() # ============================================================ # INPUT HANDLING # ============================================================ $sleepMs = [math]::Max(100, ($RefreshInterval * 1000) - $frameStart.ElapsedMilliseconds) $inputTimer = [Diagnostics.Stopwatch]::StartNew() while ($inputTimer.ElapsedMilliseconds -lt $sleepMs) { if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) # Ctrl+C — checked first so it always wins if ($key.Key -eq 'C' -and ($key.Modifiers -band [ConsoleModifiers]::Control)) { $running = $false break } if ($key.Key -eq 'Q' -or $key.Key -eq 'Escape') { $running = $false } elseif ($key.Key -eq 'C') { $sortMode = 'CPU' } elseif ($key.Key -eq 'M') { $sortMode = 'Memory' } elseif ($key.Key -eq 'P') { $sortMode = 'PID' } elseif ($key.Key -eq 'N') { $sortMode = 'Name' } if (-not $running) { break } } Start-Sleep -Milliseconds 50 } } } finally { [Console]::CursorVisible = $previousCursorVisible [Console]::TreatControlCAsInput = $previousCtrlC [Console]::Clear() Write-Information -MessageData 'System monitor stopped.' -InformationAction Continue } } } |