ServicesTUI.ps1

<#PSScriptInfo
    .VERSION 1.0.5
    .GUID 3dbf1245-8fe2-4bc2-89fa-c91823ab2efb
    .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 Services Manager.
#>


<#
.SYNOPSIS
    Provides a text user interface (TUI) inside the PowerShell console to monitor and manage Windows services.
.DESCRIPTION
    A console TUI application for Windows Services Manager.
.PARAMETER
    None
.EXAMPLE
    ServicesTUI
#>


# --- 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 - 3
    
    # 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 - 1) $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
    $remainingSpaces = $Width - $paddedStatusText.Length - $focusPart.Length - 2
    if ($remainingSpaces -lt 0) { $remainingSpaces = 0 }
    
    $fullStatus = $paddedStatusText + (" " * $remainingSpaces) + $focusPart + " "
    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() }
                }
                
                $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
    )
    # Draw the header lines (up to 4)
    $headerCount = if ($null -eq $HeaderLines) { 0 } else { $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)
        }
        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")
    
    # Draw scrollable body message lines
    $scrollAreaHeight = $Height - 5
    $messageCount = if ($null -eq $MessageLines) { 0 } else { $MessageLines.Count }
    for ($i = 0; $i -lt $scrollAreaHeight; $i++) {
        $lineIndex = $ScrollOffset + $i
        $y = $StartY + 5 + $i
        
        $lineText = if ($lineIndex -lt $messageCount) { $MessageLines[$lineIndex] } else { "" }
        if ($lineText.Length -gt $Width) {
            $lineText = $lineText.Substring(0, $Width)
        }
        Set-Cursor $StartX $y
        [Console]::Write($lineText.PadRight($Width))
    }
    
    # Fill remaining space
    for ($y = $StartY + 5 + $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 = 66
    $boxH = 18
    $boxX = [int](($Width - $boxW) / 2)
    $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
    
    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
        Write-At ($boxX + 2) ($boxY + 2) "Search Filter: $searchQuery" 'Cyan'
        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) {
                    $color = Get-ANSIColor 'SelectedActive'
                } 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'
        Write-At ($boxX + 2) ($boxY + $boxH - 2) " [Space] Toggle | [Arrows] Navigate | [A-Z] Filter | [Enter] OK | [Esc] Cancel" '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 '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)
        }
        elseif ($key.Key -eq 'Space') {
            if ($filteredItems.Count -gt 0) {
                $selectedItem = $filteredItems[$selectedIndex]
                $selectedItem.Checked = -not $selectedItem.Checked
            }
        }
        elseif ($key.Key -eq 'Backspace') {
            if ($searchQuery.Length -gt 0) {
                $searchQuery = $searchQuery.Substring(0, $searchQuery.Length - 1)
                $selectedIndex = 0
                $scrollOffset = 0
            }
        }
        else {
            if ($key.KeyChar -ge 32 -and $key.KeyChar -le 126) {
                $searchQuery += $key.KeyChar
                $selectedIndex = 0
                $scrollOffset = 0
            }
        }
    }
}

# Export functions

# --- MAIN UTILITY DRIVER EXECUTION ---
# PowerShell TUI Services Manager Main Driver
# Run this script to start the Services console interface.
# Author: Antigravity

$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path

# --- DATA RETRIEVAL ---
function Get-ServicesList {
    return [System.Collections.ArrayList]::new((Get-CimInstance Win32_Service | Sort-Object DisplayName))
}

# --- CATEGORIES TREE ---
$categoriesTree = @(
    [PSCustomObject]@{ Id = "All"; Label = "All Services"; Level = 0; IsLeaf = $true }
    [PSCustomObject]@{ Id = "Running"; Label = "Running"; Level = 0; IsLeaf = $true }
    [PSCustomObject]@{ Id = "Stopped"; Label = "Stopped"; Level = 0; IsLeaf = $true }
    [PSCustomObject]@{ Id = "Auto"; Label = "Automatic"; Level = 0; IsLeaf = $true }
    [PSCustomObject]@{ Id = "Manual"; Label = "Manual"; Level = 0; IsLeaf = $true }
    [PSCustomObject]@{ Id = "Disabled"; Label = "Disabled"; Level = 0; IsLeaf = $true }
)

$serviceCols = @(
    @{ Label = "Display Name"; Prop = "DisplayName"; Align = "Left"; Width = 30 }
    @{ Label = "Service Name"; Prop = "Name"; Align = "Left"; Width = 15 }
    @{ Label = "Status"; Prop = "State"; Align = "Left"; Width = 10 }
    @{ Label = "Startup Type"; Prop = "StartMode"; Align = "Left"; Width = 12 }
    @{ Label = "Log On As"; Prop = "StartName"; Align = "Left"; Width = 20 }
)

$serviceRowColorScript = {
    param($item)
    if ($item.State -eq 'Running') {
        return 'GreenRow'
    }
    return 'Reset'
}

# --- STARTUP AND INITIALIZATION ---
$statusText = "Querying services information..."
$width = [Console]::WindowWidth
$height = [Console]::WindowHeight
Update-LayoutDimensions $width $height

$servicesList = Get-ServicesList
$selectedCategoryIndex = 0

$tableSelectedIndex = 0
$tableScrollOffset = 0
$detailsScrollOffset = 0

$global:focusArea = 0 # 0: Categories, 1: Table, 2: Details

Initialize-Console
[Console]::Write("$ESC[2J")

$redrawAll = $true
$needsTreeRedraw = $true
$needsTableRedraw = $true
$needsDetailsRedraw = $true
$needsStatusRedraw = $true

try {
    while ($true) {
        # Filter services
        $cat = $categoriesTree[$selectedCategoryIndex]
        $activeItems = [System.Collections.ArrayList]::new()
        
        foreach ($srv in $servicesList) {
            $matchCat = switch ($cat.Id) {
                "All"      { $true }
                "Running"  { $srv.State -eq 'Running' }
                "Stopped"  { $srv.State -eq 'Stopped' }
                "Auto"     { $srv.StartMode -eq 'Auto' }
                "Manual"   { $srv.StartMode -eq 'Manual' }
                "Disabled" { $srv.StartMode -eq 'Disabled' }
            }
            if ($matchCat) { $activeItems.Add($srv) | Out-Null }
        }
        
        # Bounds check selection
        if ($tableSelectedIndex -ge $activeItems.Count) {
            $tableSelectedIndex = [Math]::Max(0, $activeItems.Count - 1)
        }
        
        # 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
            [Console]::Write("$ESC[2J")
            $redrawAll = $true
        }
        
        # 2. Redraw Components
        if ($redrawAll) {
            $rightWidth = $width - $global:leftWidth - 3
            Draw-Borders $width $height $global:leftWidth $global:mainHeight $global:focusArea "CATEGORIES" "SYSTEM SERVICES" "SERVICE DESCRIPTION"
            Draw-Menu $width @("S: Start", "T: Stop", "P: Pause", "R: Restart", "A: Auto", "M: Manual", "D: Disable", "I: Info", "Ctrl+Q: Exit")
            $needsTreeRedraw = $true
            $needsTableRedraw = $true
            $needsDetailsRedraw = $true
            $needsStatusRedraw = $true
            $redrawAll = $false
        }
        
        if ($needsTreeRedraw) {
            Draw-TreeView [System.Collections.ArrayList]::new($categoriesTree) $selectedCategoryIndex 0 $global:leftWidth ($height - 4) ($global:focusArea -eq 0)
            $needsTreeRedraw = $false
        }
        
        if ($needsTableRedraw) {
            $rightWidth = $width - $global:leftWidth - 3
            
            # Dynamically size columns
            $totalW = 0
            foreach ($col in $serviceCols) {
                if ($col.Prop -ne 'DisplayName') { $totalW += $col.Width + 1 }
            }
            $nameCol = $serviceCols | Where-Object { $_.Prop -eq 'DisplayName' }
            if ($nameCol) {
                $nameCol.Width = $rightWidth - $totalW - 1
                if ($nameCol.Width -lt 10) { $nameCol.Width = 10 }
            }
            
            Draw-TableHeader ($global:leftWidth + 2) 2 $serviceCols $rightWidth
            Draw-TableRows $activeItems $tableSelectedIndex $tableScrollOffset $serviceCols ($global:leftWidth + 2) 3 $rightWidth ($global:mainHeight - 2) ($global:focusArea -eq 1) $serviceRowColorScript
            $needsTableRedraw = $false
        }
        
        if ($needsDetailsRedraw) {
            $rightWidth = $width - $global:leftWidth - 3
            $selectedItem = if ($activeItems.Count -gt 0 -and $tableSelectedIndex -lt $activeItems.Count) {
                $activeItems[$tableSelectedIndex]
            } else { $null }
            
            $detailsLines = [System.Collections.ArrayList]::new()
            if ($selectedItem) {
                $desc = if ($selectedItem.Description) { $selectedItem.Description } else { "No description available." }
                $wrappedDesc = Wrap-Text $desc ($rightWidth - 2)
                foreach ($line in $wrappedDesc) {
                    $detailsLines.Add($line) | Out-Null
                }
            } else {
                $detailsLines.Add("No service selected.") | Out-Null
            }
            
            $detailHeight = $height - $global:mainHeight - 4
            Draw-Details $null $detailsLines $detailsScrollOffset ($global:leftWidth + 2) ($global:mainHeight + 2) $rightWidth $detailHeight
            $needsDetailsRedraw = $false
        }
        
        if ($needsStatusRedraw) {
            $focusTxt = switch ($global:focusArea) {
                0 { "Categories" }
                1 { "Table" }
                2 { "Details" }
            }
            $statusText = "Services Count: $($activeItems.Count). Use hotkeys S/T/P/R/A/M/D to control states."
            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 'Q') {
                break
            }
        }
        
        $char = $key.KeyChar.ToString().ToUpper()
        
        if ($char -eq 'S' -or $char -eq 'T' -or $char -eq 'P' -or $char -eq 'R' -or $char -eq 'A' -or $char -eq 'M' -or $char -eq 'D') {
            $selectedItem = if ($activeItems.Count -gt 0 -and $tableSelectedIndex -lt $activeItems.Count) {
                $activeItems[$tableSelectedIndex]
            } else { $null }
            
            if ($selectedItem) {
                $sName = $selectedItem.Name
                $statusText = "Executing operation on $sName..."
                $needsStatusRedraw = $true
                Draw-Status $statusText "" "" $width $height
                
                try {
                    switch ($char) {
                        'S' { Start-Service -Name $sName; $statusText = "Service started successfully." }
                        'T' { Stop-Service -Name $sName -Force; $statusText = "Service stopped successfully." }
                        'P' { Suspend-Service -Name $sName; $statusText = "Service paused." }
                        'R' { Restart-Service -Name $sName -Force; $statusText = "Service restarted successfully." }
                        'A' { Set-Service -Name $sName -StartupType Automatic; $statusText = "Startup changed to Automatic." }
                        'M' { Set-Service -Name $sName -StartupType Manual; $statusText = "Startup changed to Manual." }
                        'D' { Set-Service -Name $sName -StartupType Disabled; $statusText = "Startup changed to Disabled." }
                    }
                } catch {
                    $statusText = "Operation failed: " + $_.Exception.Message
                }
                
                # Rescan and update items
                $servicesList = Get-ServicesList
            }
            $redrawAll = $true
            continue
        }
        
        if ($char -eq 'I') {
            # Info popup
            $selectedItem = if ($activeItems.Count -gt 0 -and $tableSelectedIndex -lt $activeItems.Count) {
                $activeItems[$tableSelectedIndex]
            } else { $null }
            
            if ($selectedItem) {
                $details = [System.Collections.ArrayList]::new(@(
                    "Service Name: $($selectedItem.Name)"
                    "Display Name: $($selectedItem.DisplayName)"
                    "State: $($selectedItem.State)"
                    "Start Mode: $($selectedItem.StartMode)"
                    "Start Name: $($selectedItem.StartName)"
                    "Path Name: $($selectedItem.PathName)"
                    "Process ID: $($selectedItem.ProcessId)"
                    "Description: $($selectedItem.Description)"
                ))
                Show-CheckListDialog "Service Properties:" "Properties" [System.Collections.ArrayList]::new(($details | ForEach-Object { [PSCustomObject]@{ Label = $_; Checked = $false } })) $width $height | Out-Null
            }
            $redrawAll = $true
            continue
        }
        
        # --- FOCUS NAVIGATION ---
        if ($global:focusArea -eq 0) {
            # Categories Tree
            if ($key.Key -eq 'UpArrow') {
                if ($selectedCategoryIndex -gt 0) {
                    $selectedCategoryIndex--
                    $tableSelectedIndex = 0
                    $tableScrollOffset = 0
                    $detailsScrollOffset = 0
                    $needsTreeRedraw = $true
                    $needsTableRedraw = $true
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'DownArrow') {
                if ($selectedCategoryIndex -lt ($categoriesTree.Count - 1)) {
                    $selectedCategoryIndex++
                    $tableSelectedIndex = 0
                    $tableScrollOffset = 0
                    $detailsScrollOffset = 0
                    $needsTreeRedraw = $true
                    $needsTableRedraw = $true
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'Tab') {
                if ($activeItems.Count -gt 0) {
                    $global:focusArea = 1
                } else {
                    $global:focusArea = 2
                }
                $redrawAll = $true
            }
        }
        elseif ($global:focusArea -eq 1) {
            # Items Table
            $tableHeight = $global:mainHeight - 2
            
            if ($key.Key -eq 'UpArrow') {
                if ($tableSelectedIndex -gt 0) {
                    $tableSelectedIndex--
                    if ($tableSelectedIndex -lt $tableScrollOffset) { $tableScrollOffset = $tableSelectedIndex }
                    $detailsScrollOffset = 0
                    $needsTableRedraw = $true
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'DownArrow') {
                if ($tableSelectedIndex -lt ($activeItems.Count - 1)) {
                    $tableSelectedIndex++
                    if ($tableSelectedIndex -ge ($tableScrollOffset + $tableHeight)) {
                        $tableScrollOffset = $tableSelectedIndex - $tableHeight + 1
                    }
                    $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 }
                $detailsScrollOffset = 0
                $needsTableRedraw = $true
                $needsDetailsRedraw = $true
            }
            elseif ($key.Key -eq 'PageDown') {
                $tableSelectedIndex = [Math]::Min($activeItems.Count - 1, $tableSelectedIndex + $tableHeight)
                $tableScrollOffset = [Math]::Min($activeItems.Count - $tableHeight, $tableScrollOffset + $tableHeight)
                if ($tableScrollOffset -lt 0) { $tableScrollOffset = 0 }
                if ($tableSelectedIndex -ge ($tableScrollOffset + $tableHeight)) {
                    $tableSelectedIndex = $tableScrollOffset + $tableHeight - 1
                }
                $detailsScrollOffset = 0
                $needsTableRedraw = $true
                $needsDetailsRedraw = $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) {
            # Description scrolling
            $detailHeight = $height - $global:mainHeight - 4
            
            if ($key.Key -eq 'UpArrow') {
                if ($detailsScrollOffset -gt 0) {
                    $detailsScrollOffset--
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'DownArrow') {
                if ($detailsScrollOffset -lt ($detailsLines.Count - $detailHeight)) {
                    $detailsScrollOffset++
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'Tab') {
                $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift
                $global:focusArea = if ($isShift) { if ($activeItems.Count -gt 0) { 1 } else { 0 } } else { 0 }
                $redrawAll = $true
            }
            elseif ($key.Key -eq 'Escape') {
                $global:focusArea = 0
                $redrawAll = $true
            }
        }
    }
} finally {
    Restore-Console
}