EventViewerTUI.ps1
|
<#PSScriptInfo .VERSION 1.0.9 .GUID 6ab35451-d419-4ba3-ab5f-40e94ffeb34e .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 Windows Event Viewer log inspection. #> <# .SYNOPSIS Provides a text user interface (TUI) inside the PowerShell console to inspect event logs. .DESCRIPTION A console TUI application for Windows Event Viewer log inspection. .PARAMETER None .EXAMPLE EventViewerTUI #> # --- 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 { $isDisabled = $false $isHidden = $false if ($node -is [System.Collections.IDictionary]) { if ($node.ContainsKey('IsDisabled')) { $isDisabled = $node['IsDisabled'] } if ($node.ContainsKey('IsHidden')) { $isHidden = $node['IsHidden'] } } else { try { $isDisabled = $node.IsDisabled } catch {} try { $isHidden = $node.IsHidden } catch {} } if ($isDisabled) { $color = Get-ANSIColor 'Error' } elseif ($isHidden) { $color = Get-ANSIColor 'Gray' } 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, [switch]$ReadOnly, [switch]$HideSearch ) $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 = if ($HideSearch) { $boxH - 5 } else { $boxH - 8 } $focusedControl = if ($HideSearch) { 'List' } else { 'Search' } 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 if (-not $HideSearch) { # 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 = if ($HideSearch) { $boxY + 2 + $i } else { $boxY + 4 + $i } if ($itemIdx -lt $filteredItems.Count) { $item = $filteredItems[$itemIdx] $text = if ($ReadOnly) { " $($item.Label) " } else { $chk = if ($item.Checked) { "[x]" } else { "[ ]" } " $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 { $shortcutParts = @() if (-not $HideSearch) { $shortcutParts += "[Tab] Search" } if (-not $ReadOnly) { $shortcutParts += "[Space] Toggle" } $shortcutParts += "[Arrows] Move" $shortcutParts += "[Enter] OK" $shortcutParts += "[Esc] Cancel" $shortcuts = " " + ($shortcutParts -join " | ") } $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 (-not $HideSearch) { 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 (-not $ReadOnly) { 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) } } } } # --- LIST SELECTION DIALOG (NO SEARCH) --- function Show-ListSelectionDialog { param( [string]$Prompt, [string]$Title, [System.Collections.ArrayList]$Items, # Array of strings or PSCustomObjects with a Label property [int]$SelectedIndex = 0, [int]$Width, [int]$Height ) $boxW = 50 $boxH = 6 + $Items.Count if ($boxH -lt 8) { $boxH = 8 } $boxX = [int]((($Width - $boxW) / 2)) $boxY = [int]((($Height - $boxH) / 2)) if ($boxX -lt 0) { $boxX = 0 } if ($boxY -lt 1) { $boxY = 1 } $reset = Get-ANSIColor 'Reset' while ($true) { # Draw outer frames of the box $topB = "╔" + ("═" * ($boxW - 2)) + "╗" $midB = "║" + (" " * ($boxW - 2)) + "║" $botB = "╚" + ("═" * ($boxW - 2)) + "╝" 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' # 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 Write-At ($boxX + 2) ($boxY + 2) ("─" * ($boxW - 4)) 'Gray' # Draw items for ($i = 0; $i -lt $Items.Count; $i++) { $item = $Items[$i] $itemLabel = if ($item -is [PSCustomObject] -or $item -is [hashtable]) { $item.Label } else { $item } $text = " $itemLabel " if ($text.Length -gt ($boxW - 6)) { $text = $text.Substring(0, $boxW - 9) + "..." } $paddedText = $text.PadRight($boxW - 6) $y = $boxY + 3 + $i if ($i -eq $SelectedIndex) { Write-At ($boxX + 3) $y $paddedText 'SelectedActive' } else { Write-At ($boxX + 3) $y $paddedText 'Reset' } } Write-At ($boxX + 2) ($boxY + $boxH - 3) ("─" * ($boxW - 4)) 'Gray' # Draw help bar at the bottom $shortcuts = " [Arrows] Move │ [Enter] Select │ [Esc] Cancel" $paddedShortcuts = $shortcuts.PadRight($boxW - 4) Write-At ($boxX + 2) ($boxY + $boxH - 2) $paddedShortcuts 'Header' $key = [Console]::ReadKey($true) if ($key.Key -eq 'Escape') { return $null } if ($key.Key -eq 'Enter') { return $SelectedIndex } if ($key.Key -eq 'UpArrow') { if ($SelectedIndex -gt 0) { $SelectedIndex-- } } elseif ($key.Key -eq 'DownArrow') { if ($SelectedIndex -lt ($Items.Count - 1)) { $SelectedIndex++ } } } } # Export functions # --- MAIN UTILITY DRIVER EXECUTION --- # PowerShell TUI Event Viewer Main Driver # Run this script to start the Event Viewer console interface. # Author: Antigravity $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path # Helper to map levels function Get-EventLevelText($level) { switch ($level) { 1 { "● Critical" } 2 { "● Error" } 3 { "▲ Warning" } 4 { "○ Info" } 5 { "○ Verbose" } 0 { "○ LogAlways" } default { "○ Info" } } } # --- TREE CONSTRUCTION LOGIC --- function Build-EventLogTree($logNames) { $root = @{ Id = 'Root' Label = 'Event Viewer (Local)' IsLeaf = $false IsExpanded = $true Children = [System.Collections.ArrayList]::new() Level = -1 } $winLogsFolder = @{ Id = 'WindowsLogs' Label = 'Windows Logs' IsLeaf = $false IsExpanded = $true Children = [System.Collections.ArrayList]::new() Level = 0 Parent = $root } $root.Children.Add($winLogsFolder) | Out-Null $appLogsFolder = @{ Id = 'AppLogs' Label = 'Applications and Services Logs' IsLeaf = $false IsExpanded = $false Children = [System.Collections.ArrayList]::new() Level = 0 Parent = $root } $root.Children.Add($appLogsFolder) | Out-Null $microsoftFolder = @{ Id = 'Microsoft' Label = 'Microsoft' IsLeaf = $false IsExpanded = $false Children = [System.Collections.ArrayList]::new() Level = 1 Parent = $appLogsFolder } $windowsFolder = @{ Id = 'Windows' Label = 'Windows' IsLeaf = $false IsExpanded = $false Children = [System.Collections.ArrayList]::new() Level = 2 Parent = $microsoftFolder } $microsoftFolder.Children.Add($windowsFolder) | Out-Null $classicLogs = @('Application', 'Security', 'System', 'Setup', 'ForwardedEvents') foreach ($logName in $logNames) { if ($classicLogs -contains $logName) { $node = @{ Id = $logName Label = $logName IsLeaf = $true LogName = $logName Level = 1 Parent = $winLogsFolder } $winLogsFolder.Children.Add($node) | Out-Null continue } if ($logName.StartsWith('Microsoft-Windows-', [System.StringComparison]::OrdinalIgnoreCase)) { if (-not ($appLogsFolder.Children | Where-Object { $_.Id -eq 'Microsoft' })) { $appLogsFolder.Children.Add($microsoftFolder) | Out-Null } $subPath = $logName.Substring(18) $parts = $subPath.Split('/') $folderName = $parts[0] $catFolder = $windowsFolder.Children | Where-Object { $_.Label -eq $folderName } if ($null -eq $catFolder) { $catFolder = @{ Id = "MS-Win-$folderName" Label = $folderName IsLeaf = $false IsExpanded = $false Children = [System.Collections.ArrayList]::new() Level = 3 Parent = $windowsFolder } $windowsFolder.Children.Add($catFolder) | Out-Null } if ($parts.Length -gt 1) { $channelName = $parts[1] $node = @{ Id = $logName Label = $channelName IsLeaf = $true LogName = $logName Level = 4 Parent = $catFolder } $catFolder.Children.Add($node) | Out-Null } else { $catFolder.IsLeaf = $true $catFolder.LogName = $logName } continue } $parts = $logName.Split('/') if ($parts.Length -gt 1) { $folderName = $parts[0] $channelName = $parts[1] $folder = $appLogsFolder.Children | Where-Object { $_.Label -eq $folderName } if ($null -eq $folder) { $folder = @{ Id = "AppLog-$folderName" Label = $folderName IsLeaf = $false IsExpanded = $false Children = [System.Collections.ArrayList]::new() Level = 1 Parent = $appLogsFolder } $appLogsFolder.Children.Add($folder) | Out-Null } $node = @{ Id = $logName Label = $channelName IsLeaf = $true LogName = $logName Level = 2 Parent = $folder } $folder.Children.Add($node) | Out-Null } else { $node = @{ Id = $logName Label = $logName IsLeaf = $true LogName = $logName Level = 1 Parent = $appLogsFolder } $appLogsFolder.Children.Add($node) | Out-Null } } function Sort-NodeChildren($n) { if ($n.Children -and $n.Children.Count -gt 0) { $sorted = $n.Children | Sort-Object { $_.IsLeaf }, { $_.Label } $n.Children.Clear() foreach ($child in $sorted) { $n.Children.Add($child) | Out-Null Sort-NodeChildren $child } } } Sort-NodeChildren $root if ($appLogsFolder.Children.Count -eq 0) { $root.Children.Remove($appLogsFolder) | Out-Null } return $root } # --- EVENT LOADING ENGINE --- function Load-LogEvents($logName, $filterLevels, $filterIds, $filterSources, $maxEvents = 1000) { $xpathTerms = @() # Levels (Critical = 1, Error = 2, Warning = 3, Information = 4, Verbose = 5) if ($null -ne $filterLevels -and $filterLevels.Count -gt 0) { $levelQueries = @() foreach ($lvl in $filterLevels) { $levelQueries += "System/Level=$lvl" } $xpathTerms += "(" + ($levelQueries -join " or ") + ")" } # Event IDs (comma-separated or single) if ($null -ne $filterIds -and $filterIds -ne "") { $idQueries = @() foreach ($id in $filterIds.Split(',')) { $trimmedId = $id.Trim() if ($trimmedId -match '^\d+$') { $idQueries += "System/EventID=$trimmedId" } } if ($idQueries.Count -gt 0) { $xpathTerms += "(" + ($idQueries -join " or ") + ")" } } # Sources if ($null -ne $filterSources -and $filterSources.Count -gt 0) { $srcQueries = @() foreach ($src in $filterSources) { $srcQueries += "System/Provider[@Name='$src']" } $xpathTerms += "(" + ($srcQueries -join " or ") + ")" } $xpath = "*" if ($xpathTerms.Count -gt 0) { $xpath = "*[ " + ($xpathTerms -join " and ") + " ]" } $events = [System.Collections.ArrayList]::new() try { $query = New-Object System.Diagnostics.Eventing.Reader.EventLogQuery($logName, [System.Diagnostics.Eventing.Reader.PathType]::LogName, $xpath) $query.ReverseDirection = $true $reader = New-Object System.Diagnostics.Eventing.Reader.EventLogReader($query) for ($i = 0; $i -lt $maxEvents; $i++) { $ev = $reader.ReadEvent() if ($null -eq $ev) { break } # Format description safely ahead of time for table $levelText = Get-EventLevelText $ev.Level $date = if ($null -ne $ev.TimeCreated) { $ev.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") } else { "" } $source = if ($null -eq $ev.ProviderName) { "" } else { $ev.ProviderName } $category = if ($null -ne $ev.TaskDisplayName) { $ev.TaskDisplayName } elseif ($null -ne $ev.Task) { $ev.Task.ToString() } else { "None" } $mapped = [PSCustomObject]@{ RawEvent = $ev Level = $ev.Level LevelText = $levelText FormattedDate = $date ProviderName = $source Id = $ev.Id TaskDisplayName = $category } $events.Add($mapped) | Out-Null } } catch { # Gracefully handle exceptions } return $events } # --- DETAIL PANEL FORMATTING --- function Format-EventDetailsMapped($mappedItem, $width) { $colWidth = [int](($width - 4) / 2) if ($null -eq $mappedItem) { return [PSCustomObject]@{ HeaderLines = @( (Get-DetailPropertyLine "Log Name:" "N/A" "Date:" "N/A" $colWidth), (Get-DetailPropertyLine "Source:" "N/A" "Event ID:" "N/A" $colWidth), (Get-DetailPropertyLine "Level:" "N/A" "User:" "N/A" $colWidth), (Get-DetailPropertyLine "Computer:" "N/A" "Category:" "N/A" $colWidth) ) MessageLines = @("No event selected.") } } $ev = $mappedItem.RawEvent $logName = $ev.LogName if ($null -eq $logName) { $logName = "N/A" } $source = $mappedItem.ProviderName $date = $mappedItem.FormattedDate $eventId = $mappedItem.Id.ToString() $level = $mappedItem.LevelText $category = $mappedItem.TaskDisplayName # Resolve user principal safely $user = "N/A" if ($null -ne $ev.UserId) { try { $user = $ev.UserId.Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { $user = $ev.UserId.ToString() } } $computer = $ev.MachineName if ($null -eq $computer) { $computer = "N/A" } $headerLines = [System.Collections.ArrayList]::new() $headerLines.Add((Get-DetailPropertyLine "Log Name:" $logName "Date:" $date $colWidth)) | Out-Null $headerLines.Add((Get-DetailPropertyLine "Source:" $source "Event ID:" $eventId $colWidth)) | Out-Null $headerLines.Add((Get-DetailPropertyLine "Level:" $level "User:" $user $colWidth)) | Out-Null $headerLines.Add((Get-DetailPropertyLine "Computer:" $computer "Category:" $category $colWidth)) | Out-Null # Event Description Message $msg = "" try { $msg = $ev.FormatDescription() } catch { } if ([string]::IsNullOrEmpty($msg)) { $msg = "Description could not be loaded. (The event source resource DLL may not be registered)" try { if ($null -ne $ev.Properties -and $ev.Properties.Count -gt 0) { $msg += "`n`nRaw Event Data:" foreach ($prop in $ev.Properties) { if ($null -ne $prop.Value) { $msg += "`n - $($prop.Value.ToString())" } } } } catch { } } # Wrap text $messageLines = [System.Collections.ArrayList]::new() $messageLines.Add("Description:") | Out-Null $wrappedMsg = Wrap-Text $msg ($width - 2) foreach ($line in $wrappedMsg) { $messageLines.Add($line) | Out-Null } return [PSCustomObject]@{ HeaderLines = $headerLines MessageLines = $messageLines } } # --- MAIN EXECUTION --- # Fetch all logs $logNames = @() try { $logNames = [System.Diagnostics.Eventing.Reader.EventLogSession]::GlobalSession.GetLogNames() | Where-Object { $_ -and $_.Trim() -ne "" } | Select-Object -Unique } catch { $logNames = @('Application', 'Security', 'System', 'Setup') } $rootNode = Build-EventLogTree $logNames $treeVisibleNodes = Get-VisibleNodes $rootNode $treeSelectedIndex = 0 $treeScrollOffset = 0 # Table State $loadedEvents = [System.Collections.ArrayList]::new() $filteredEvents = [System.Collections.ArrayList]::new() $tableSelectedIndex = 0 $tableScrollOffset = 0 $selectedLogName = "" # Filter Configuration State $activeFilterLevels = [System.Collections.ArrayList]::new() $activeFilterSources = [System.Collections.ArrayList]::new() $activeFilterIds = "" $searchQuery = "" # UI Layout Configuration $width = [Console]::WindowWidth $height = [Console]::WindowHeight Update-LayoutDimensions $width $height $rightWidth = $width - $global:leftWidth - 4 $detailLines = Format-EventDetailsMapped $null $rightWidth $detailsScrollOffset = 0 # Focus State: 0 = Tree, 1 = Table, 2 = Details $global:focusArea = 0 # Setup console Initialize-Console # Initial full drawing [Console]::Write("$ESC[2J") Draw-Borders $width $height $global:leftWidth $global:mainHeight $global:focusArea "EVENT LOGS" "LOG ENTRIES" "EVENT DETAILS" Draw-Menu $width @("Ctrl+1: Filter", "Ctrl+2: Search", "Ctrl+3: Refresh", "Ctrl+Q: Exit") $statusText = "Ready. Use arrows to browse logs. Press Enter to load." Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height # Precompute columns width based on right panel size $levelW = 12 $dateW = 20 $idW = 9 $dividers = 4 $categoryW = [int](($rightWidth - ($levelW + $dateW + $idW + $dividers)) * 0.5) $sourceW = $rightWidth - ($levelW + $dateW + $idW + $dividers) - $categoryW if ($categoryW -lt 10) { $categoryW = 10 } if ($sourceW -lt 10) { $sourceW = 10 } $columns = @( @{ Label = "Level"; Prop = "LevelText"; Align = "Left"; Width = $levelW } @{ Label = "Date and Time"; Prop = "FormattedDate"; Align = "Left"; Width = $dateW } @{ Label = "Source"; Prop = "ProviderName"; Align = "Left"; Width = $sourceW } @{ Label = "Event ID"; Prop = "Id"; Align = "Right"; Width = $idW } @{ Label = "Task Category"; Prop = "TaskDisplayName"; Align = "Left"; Width = $categoryW } ) $eventRowColorScript = { param($item) if ($item.Level -eq 1 -or $item.Level -eq 2) { return 'ErrorRow' } if ($item.Level -eq 3) { return 'WarningRow' } return 'Reset' } # Redraw flags $redrawAll = $false $needsTreeRedraw = $true $needsTableRedraw = $true $needsDetailsRedraw = $true $needsStatusRedraw = $true 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 # Recalculate columns $categoryW = [int](($rightWidth - ($levelW + $dateW + $idW + $dividers)) * 0.5) $sourceW = $rightWidth - ($levelW + $dateW + $idW + $dividers) - $categoryW if ($categoryW -lt 10) { $categoryW = 10 } if ($sourceW -lt 10) { $sourceW = 10 } $columns[2].Width = $sourceW $columns[4].Width = $categoryW $selectedEvent = if ($filteredEvents.Count -gt 0 -and $tableSelectedIndex -lt $filteredEvents.Count) { $filteredEvents[$tableSelectedIndex] } else { $null } $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth [Console]::Write("$ESC[2J") $redrawAll = $true } # 2. Redraw Components if ($redrawAll) { Draw-Borders $width $height $global:leftWidth $global:mainHeight $global:focusArea "EVENT LOGS" "LOG ENTRIES" "EVENT DETAILS" Draw-Menu $width @("Ctrl+1: Filter", "Ctrl+2: Search", "Ctrl+3: Refresh", "Ctrl+Q: Exit") $needsTreeRedraw = $true $needsTableRedraw = $true $needsDetailsRedraw = $true $needsStatusRedraw = $true $redrawAll = $false } if ($needsTreeRedraw) { Draw-TreeView $treeVisibleNodes $treeSelectedIndex $treeScrollOffset $global:leftWidth ($height - 4) ($global:focusArea -eq 0) $needsTreeRedraw = $false } if ($needsTableRedraw) { Draw-TableHeader ($global:leftWidth + 2) 2 $columns $rightWidth $tableHeight = $global:mainHeight - 2 Draw-TableRows $filteredEvents $tableSelectedIndex $tableScrollOffset $columns ($global:leftWidth + 2) 3 $rightWidth $tableHeight ($global:focusArea -eq 1) $eventRowColorScript $needsTableRedraw = $false } if ($needsDetailsRedraw) { $detailHeight = $height - $global:mainHeight - 4 Draw-Details $detailLines.HeaderLines $detailLines.MessageLines $detailsScrollOffset ($global:leftWidth + 2) ($global:mainHeight + 2) $rightWidth $detailHeight $needsDetailsRedraw = $false } if ($needsStatusRedraw) { $focusTxt = switch ($global:focusArea) { 0 { "Tree View" } 1 { "Log Entries" } 2 { "Event Details" } } Draw-Status $statusText $focusTxt "Tab: Switch Pane" $width $height $needsStatusRedraw = $false } Set-Cursor 0 0 # 3. Read Keyboard Input if (-not [Console]::KeyAvailable) { Start-Sleep -Milliseconds 20 continue } $key = [Console]::ReadKey($true) $isCtrl = ($key.Modifiers -band [System.ConsoleModifiers]::Control) -eq [System.ConsoleModifiers]::Control if ($isCtrl) { if ($key.Key -eq 'D1') { # --- ADVANCED CHECKLIST FILTER DIALOG --- if ($selectedLogName -eq "") { $statusText = "Please select a log and press Enter first." $needsStatusRedraw = $true continue } $statusText = "Loading sources for $selectedLogName..." $needsStatusRedraw = $true Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height # Get unique sources $logSources = @() try { $logSources = (New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration($selectedLogName)).ProviderNames | Sort-Object } catch {} if ($logSources.Count -eq 0) { $logSources = @('Application', 'System', 'Security', '.NET Runtime') } # Step 1: Select Event Levels $levelItems = [System.Collections.ArrayList]::new(@( [PSCustomObject]@{ Label = "1 - Critical"; Value = 1; Checked = $activeFilterLevels -contains 1 } [PSCustomObject]@{ Label = "2 - Error"; Value = 2; Checked = $activeFilterLevels -contains 2 } [PSCustomObject]@{ Label = "3 - Warning"; Value = 3; Checked = $activeFilterLevels -contains 3 } [PSCustomObject]@{ Label = "4 - Information"; Value = 4; Checked = $activeFilterLevels -contains 4 } [PSCustomObject]@{ Label = "5 - Verbose"; Value = 5; Checked = $activeFilterLevels -contains 5 } )) $resLevels = Show-CheckListDialog "Select Event Levels to include (Space to toggle):" "Filter Levels" $levelItems $width $height if ($null -eq $resLevels) { $statusText = "Filter setup cancelled." $redrawAll = $true continue } # Step 2: Select Event Sources $sourceItems = [System.Collections.ArrayList]::new() foreach ($src in $logSources) { $sourceItems.Add([PSCustomObject]@{ Label = $src; Checked = $activeFilterSources -contains $src }) | Out-Null } $resSources = Show-CheckListDialog "Select Event Sources to include (Type to filter list):" "Filter Sources" $sourceItems $width $height if ($null -eq $resSources) { $statusText = "Filter setup cancelled." $redrawAll = $true continue } # Step 3: Event ID Input $resIds = Show-InputBox "Filter by Event IDs (e.g. 4624, 7036 - Empty = All):" "Filter Event IDs" $width $height if ($null -eq $resIds) { $statusText = "Filter setup cancelled." $redrawAll = $true continue } # Step 4: Message Search Query $resSearch = Show-InputBox "Filter by search text (Message content - Empty = None):" "Search Text" $width $height if ($null -eq $resSearch) { $statusText = "Filter setup cancelled." $redrawAll = $true continue } # Apply Filters $activeFilterLevels.Clear() foreach ($item in $resLevels) { if ($item.Checked) { $activeFilterLevels.Add($item.Value) | Out-Null } } $activeFilterSources.Clear() foreach ($item in $resSources) { if ($item.Checked) { $activeFilterSources.Add($item.Label) | Out-Null } } $activeFilterIds = $resIds.Trim() $searchQuery = $resSearch.Trim() $statusText = "Loading log events with filters..." $needsStatusRedraw = $true Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height $loadedEvents = Load-LogEvents $selectedLogName $activeFilterLevels $activeFilterIds $activeFilterSources # Apply search filter locally $filteredEvents = [System.Collections.ArrayList]::new() if ($searchQuery -ne "") { foreach ($ev in $loadedEvents) { $msg = ""; try { $msg = $ev.RawEvent.FormatDescription() } catch {} if ($ev.ProviderName.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or ($null -ne $msg -and $msg.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)) { $filteredEvents.Add($ev) | Out-Null } } } else { $filteredEvents = $loadedEvents } $tableSelectedIndex = 0 $tableScrollOffset = 0 $selectedEvent = if ($filteredEvents.Count -gt 0) { $filteredEvents[0] } else { $null } $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth $detailsScrollOffset = 0 $statusText = "Loaded $($filteredEvents.Count) events (filtered)." $redrawAll = $true continue } if ($key.Key -eq 'D2') { # Quick Search $searchIn = Show-InputBox "Quick Search (Message/Source, Empty = Clear):" "Search Message" $width $height if ($null -ne $searchIn) { $searchQuery = $searchIn.Trim() $filteredEvents = [System.Collections.ArrayList]::new() if ($searchQuery -ne "") { foreach ($ev in $loadedEvents) { $msg = ""; try { $msg = $ev.RawEvent.FormatDescription() } catch {} if ($ev.ProviderName.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or ($null -ne $msg -and $msg.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)) { $filteredEvents.Add($ev) | Out-Null } } } else { $filteredEvents = $loadedEvents } $tableSelectedIndex = 0 $tableScrollOffset = 0 $selectedEvent = if ($filteredEvents.Count -gt 0) { $filteredEvents[0] } else { $null } $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth $detailsScrollOffset = 0 $statusText = "Quick Search found $($filteredEvents.Count) matches." } $redrawAll = $true continue } if ($key.Key -eq 'D3') { # Refresh if ($selectedLogName -ne "") { $statusText = "Refreshing events for $selectedLogName..." $needsStatusRedraw = $true Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height $loadedEvents = Load-LogEvents $selectedLogName $activeFilterLevels $activeFilterIds $activeFilterSources $filteredEvents = [System.Collections.ArrayList]::new() if ($searchQuery -ne "") { foreach ($ev in $loadedEvents) { $msg = ""; try { $msg = $ev.RawEvent.FormatDescription() } catch {} if ($ev.ProviderName.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or ($null -ne $msg -and $msg.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)) { $filteredEvents.Add($ev) | Out-Null } } } else { $filteredEvents = $loadedEvents } $tableSelectedIndex = 0 $tableScrollOffset = 0 $selectedEvent = if ($filteredEvents.Count -gt 0) { $filteredEvents[0] } else { $null } $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth $detailsScrollOffset = 0 $statusText = "Refreshed log. Loaded $($filteredEvents.Count) events." } $redrawAll = $true continue } if ($key.Key -eq 'Q') { break } } # --- FOCUS NAVIGATION --- if ($global:focusArea -eq 0) { # --- TREE VIEW --- $node = $treeVisibleNodes[$treeSelectedIndex] if ($key.Key -eq 'UpArrow') { if ($treeSelectedIndex -gt 0) { $treeSelectedIndex-- if ($treeSelectedIndex -lt $treeScrollOffset) { $treeScrollOffset = $treeSelectedIndex } $needsTreeRedraw = $true } } elseif ($key.Key -eq 'DownArrow') { if ($treeSelectedIndex -lt ($treeVisibleNodes.Count - 1)) { $treeSelectedIndex++ $treeHeight = $height - 4 if ($treeSelectedIndex -ge ($treeScrollOffset + $treeHeight)) { $treeScrollOffset = $treeSelectedIndex - $treeHeight + 1 } $needsTreeRedraw = $true } } elseif ($key.Key -eq 'PageUp') { $treeHeight = $height - 4 $treeSelectedIndex = [Math]::Max(0, $treeSelectedIndex - $treeHeight) $treeScrollOffset = [Math]::Max(0, $treeScrollOffset - $treeHeight) if ($treeSelectedIndex -lt $treeScrollOffset) { $treeSelectedIndex = $treeScrollOffset } $needsTreeRedraw = $true } elseif ($key.Key -eq 'PageDown') { $treeHeight = $height - 4 $treeSelectedIndex = [Math]::Min($treeVisibleNodes.Count - 1, $treeSelectedIndex + $treeHeight) $treeScrollOffset = [Math]::Min($treeVisibleNodes.Count - $treeHeight, $treeScrollOffset + $treeHeight) if ($treeScrollOffset -lt 0) { $treeScrollOffset = 0 } if ($treeSelectedIndex -ge ($treeScrollOffset + $treeHeight)) { $treeSelectedIndex = $treeScrollOffset + $treeHeight - 1 } $needsTreeRedraw = $true } elseif ($key.Key -eq 'RightArrow') { if (-not $node.IsLeaf -and -not $node.IsExpanded) { $node.IsExpanded = $true $treeVisibleNodes = Get-VisibleNodes $rootNode $needsTreeRedraw = $true $statusText = "Expanded $($node.Label)." $needsStatusRedraw = $true } } elseif ($key.Key -eq 'LeftArrow') { if (-not $node.IsLeaf -and $node.IsExpanded) { $node.IsExpanded = $false $treeVisibleNodes = Get-VisibleNodes $rootNode $needsTreeRedraw = $true $statusText = "Collapsed $($node.Label)." $needsStatusRedraw = $true } elseif ($null -ne $node.Parent -and $node.Parent.Id -ne 'Root') { $parentIndex = $treeVisibleNodes.IndexOf($node.Parent) if ($parentIndex -ge 0) { $treeSelectedIndex = $parentIndex if ($treeSelectedIndex -lt $treeScrollOffset) { $treeScrollOffset = $treeSelectedIndex } $needsTreeRedraw = $true } } } elseif ($key.Key -eq 'Enter') { if ($node.IsLeaf) { $selectedLogName = $node.LogName $statusText = "Loading log: $selectedLogName..." $needsStatusRedraw = $true Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height # Reset filter configs $activeFilterLevels.Clear() $activeFilterSources.Clear() $activeFilterIds = "" $searchQuery = "" $loadedEvents = Load-LogEvents $selectedLogName $null $null $null $filteredEvents = $loadedEvents $tableSelectedIndex = 0 $tableScrollOffset = 0 $selectedEvent = if ($filteredEvents.Count -gt 0) { $filteredEvents[0] } else { $null } $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth $detailsScrollOffset = 0 $global:focusArea = 1 $statusText = "Loaded $($filteredEvents.Count) events from $selectedLogName." $redrawAll = $true } } elseif ($key.Key -eq 'Tab') { $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift if ($isShift) { $global:focusArea = 2 } else { $global:focusArea = if ($filteredEvents.Count -gt 0) { 1 } else { 2 } } $redrawAll = $true } } elseif ($global:focusArea -eq 1) { # --- TABLE VIEW --- $tableHeight = $global:mainHeight - 2 if ($key.Key -eq 'UpArrow') { if ($tableSelectedIndex -gt 0) { $tableSelectedIndex-- if ($tableSelectedIndex -lt $tableScrollOffset) { $tableScrollOffset = $tableSelectedIndex } $selectedEvent = $filteredEvents[$tableSelectedIndex] $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth $detailsScrollOffset = 0 $needsTableRedraw = $true $needsDetailsRedraw = $true } } elseif ($key.Key -eq 'DownArrow') { if ($tableSelectedIndex -lt ($filteredEvents.Count - 1)) { $tableSelectedIndex++ if ($tableSelectedIndex -ge ($tableScrollOffset + $tableHeight)) { $tableScrollOffset = $tableSelectedIndex - $tableHeight + 1 } $selectedEvent = $filteredEvents[$tableSelectedIndex] $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth $detailsScrollOffset = 0 $needsTableRedraw = $true $needsDetailsRedraw = $true } } elseif ($key.Key -eq 'PageUp') { $tableSelectedIndex = [Math]::Max(0, $tableSelectedIndex - $tableHeight) $tableScrollOffset = [Math]::Max(0, $tableScrollOffset - $tableHeight) if ($tableSelectedIndex -lt $tableScrollOffset) { $tableSelectedIndex = $tableScrollOffset } $selectedEvent = $filteredEvents[$tableSelectedIndex] $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth $detailsScrollOffset = 0 $needsTableRedraw = $true $needsDetailsRedraw = $true } elseif ($key.Key -eq 'PageDown') { $tableSelectedIndex = [Math]::Min($filteredEvents.Count - 1, $tableSelectedIndex + $tableHeight) $tableScrollOffset = [Math]::Min($filteredEvents.Count - $tableHeight, $tableScrollOffset + $tableHeight) if ($tableScrollOffset -lt 0) { $tableScrollOffset = 0 } if ($tableSelectedIndex -ge ($tableScrollOffset + $tableHeight)) { $tableSelectedIndex = $tableScrollOffset + $tableHeight - 1 } $selectedEvent = $filteredEvents[$tableSelectedIndex] $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth $detailsScrollOffset = 0 $needsTableRedraw = $true $needsDetailsRedraw = $true } elseif ($key.Key -eq 'Escape') { $global:focusArea = 0 $redrawAll = $true $statusText = "Focused on event logs tree." } 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) { # --- DETAILS VIEW --- $detailHeight = $height - $global:mainHeight - 4 $scrollAreaHeight = $detailHeight - 5 if ($scrollAreaHeight -lt 1) { $scrollAreaHeight = 1 } if ($key.Key -eq 'UpArrow') { if ($detailsScrollOffset -gt 0) { $detailsScrollOffset-- $needsDetailsRedraw = $true } } elseif ($key.Key -eq 'DownArrow') { $maxOffset = $detailLines.MessageLines.Count - $scrollAreaHeight if ($maxOffset -lt 0) { $maxOffset = 0 } if ($detailsScrollOffset -lt $maxOffset) { $detailsScrollOffset++ $needsDetailsRedraw = $true } } elseif ($key.Key -eq 'PageUp') { $detailsScrollOffset = [Math]::Max(0, $detailsScrollOffset - $scrollAreaHeight) $needsDetailsRedraw = $true } elseif ($key.Key -eq 'PageDown') { $maxOffset = $detailLines.MessageLines.Count - $scrollAreaHeight if ($maxOffset -lt 0) { $maxOffset = 0 } $detailsScrollOffset = [Math]::Min($maxOffset, $detailsScrollOffset + $scrollAreaHeight) $needsDetailsRedraw = $true } elseif ($key.Key -eq 'Escape') { $global:focusArea = 0 $redrawAll = $true $statusText = "Focused on event logs tree." } elseif ($key.Key -eq 'Tab') { $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift $global:focusArea = if ($isShift) { if ($filteredEvents.Count -gt 0) { 1 } else { 0 } } else { 0 } $redrawAll = $true } } } } finally { Restore-Console } |