DeviceManagerTUI.ps1

<#PSScriptInfo
    .VERSION 1.0.9
    .GUID 8c2a3921-9dfa-41e9-8a3a-bc12b84fa85c
    .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 Device Manager.
#>


<#
.SYNOPSIS
    Provides a text user interface (TUI) inside the PowerShell console to view and manage system devices, drivers, and hardware.
.DESCRIPTION
    A console TUI application for Windows Device Manager.
.PARAMETER
    None
.EXAMPLE
    DeviceManagerTUI
#>


# --- 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 Device Manager Main Driver
# Run this script to start the Device Manager console interface.
# Author: Antigravity

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

# --- DEVICE UTILITIES ---

function Get-DevicesAndTree($byConnection, $showHidden) {
    # 1. Fetch devices
    $devices = Get-PnpDevice -ErrorAction SilentlyContinue
    if (-not $showHidden) {
        $devices = $devices | Where-Object { $_.Present }
    }
    
    $computerName = [Environment]::MachineName
    $rootNode = @{
        Id = 'ComputerRoot'
        Label = "$computerName (Local Computer)"
        IsLeaf = $false
        IsExpanded = $true
        Children = [System.Collections.ArrayList]::new()
        Level = -1
        Device = $null
    }
    
    if ($byConnection) {
        # 2. Build tree by connection
        # Query parents
        $parentProps = $devices | Get-PnpDeviceProperty -KeyName DEVPKEY_Device_Parent -ErrorAction SilentlyContinue
        $parentMap = @{}
        foreach ($prop in $parentProps) {
            $parentMap[$prop.InstanceId] = $prop.Data
        }
        
        # Create node list
        $nodes = @{}
        foreach ($dev in $devices) {
            $label = if ($dev.FriendlyName) { $dev.FriendlyName } else { $dev.InstanceId }
            if ($dev.Problem -eq 22) {
                $label = "[x] " + $label + " [Disabled]"
            }
            $nodes[$dev.InstanceId] = @{
                Id = $dev.InstanceId
                Label = $label
                IsLeaf = $true # default to leaf, will clear if children added
                IsExpanded = $false
                Children = [System.Collections.ArrayList]::new()
                Level = 0
                Device = $dev
                IsDisabled = ($dev.Problem -eq 22)
                IsHidden = (-not $dev.Present)
            }
        }
        
        # Assemble hierarchy
        foreach ($id in $nodes.Keys) {
            $node = $nodes[$id]
            $parentID = $parentMap[$id]
            
            if ($parentID -and $nodes.ContainsKey($parentID)) {
                $parentNode = $nodes[$parentID]
                $parentNode.IsLeaf = $false
                $node.Parent = $parentNode
                $parentNode.Children.Add($node) | Out-Null
            } else {
                $node.Parent = $rootNode
                $rootNode.Children.Add($node) | Out-Null
            }
        }
    } else {
        # 3. Build tree by type (group by class with friendly devmgmt.msc category names)
        $classFriendlyNames = @{
            'AudioEndpoint'    = 'Audio inputs and outputs'
            'Battery'          = 'Batteries'
            'Biometric'        = 'Biometric devices'
            'Bluetooth'        = 'Bluetooth'
            'Camera'           = 'Cameras'
            'Computer'         = 'Computer'
            'DiskDrive'        = 'Disk drives'
            'Display'          = 'Display adapters'
            'CDROM'            = 'DVD/CD-ROM drives'
            'FloppyDisk'       = 'Floppy disk drives'
            'FDC'              = 'Floppy disk controllers'
            'GPS'              = 'GPS devices'
            'HIDClass'         = 'Human Interface Devices'
            'Image'            = 'Imaging devices'
            'Keyboard'         = 'Keyboards'
            'Media'            = 'Sound, video and game controllers'
            'Monitor'          = 'Monitors'
            'Mouse'            = 'Mice and other pointing devices'
            'MTD'              = 'Memory technology devices'
            'Net'              = 'Network adapters'
            'Ports'            = 'Ports (COM & LPT)'
            'Printer'          = 'Printers'
            'PrintQueue'       = 'Print queues'
            'Processor'        = 'Processors'
            'SCSIAdapter'      = 'Storage controllers'
            'SecurityDevices'  = 'Security devices'
            'Sensor'           = 'Sensors'
            'SmartCardReader'  = 'Smart card readers'
            'SoftwareDevice'   = 'Software devices'
            'System'           = 'System devices'
            'USB'              = 'Universal Serial Bus controllers'
            'USBDevice'        = 'Universal Serial Bus devices'
            'Volume'           = 'Volumes'
            'VolumeSnapshot'   = 'Volume shadow copies'
            'WSDPrintDevice'   = 'WSD Print Devices'
        }
        
        $classes = $devices | Group-Object Class
        $classNodes = [System.Collections.ArrayList]::new()
        
        foreach ($group in $classes) {
            $rawClass = $group.Name
            $classLabel = if ($rawClass) {
                if ($classFriendlyNames.ContainsKey($rawClass)) {
                    $classFriendlyNames[$rawClass]
                } else {
                    $rawClass
                }
            } else {
                "Unknown"
            }
            
            $classNode = @{
                Id = "Class-$classLabel"
                Label = $classLabel
                IsLeaf = $false
                IsExpanded = $false
                Children = [System.Collections.ArrayList]::new()
                Level = 0
                Parent = $rootNode
                Device = $null
            }
            
            foreach ($dev in $group.Group) {
                $label = if ($dev.FriendlyName) { $dev.FriendlyName } else { $dev.InstanceId }
                if ($dev.Problem -eq 22) {
                    $label = "[x] " + $label + " [Disabled]"
                }
                $devNode = @{
                    Id = $dev.InstanceId
                    Label = $label
                    IsLeaf = $true
                    Level = 1
                    Parent = $classNode
                    Device = $dev
                    IsDisabled = ($dev.Problem -eq 22)
                    IsHidden = (-not $dev.Present)
                }
                $classNode.Children.Add($devNode) | Out-Null
            }
            
            $classNodes.Add($classNode) | Out-Null
        }
        
        # Sort classes alphabetically by friendly labels
        $sortedClasses = $classNodes | Sort-Object { $_.Label }
        foreach ($cNode in $sortedClasses) {
            $rootNode.Children.Add($cNode) | Out-Null
        }
    }
    
    # Sort children alphabetically
    function Sort-NodeChildren($n) {
        if ($n.Children -and $n.Children.Count -gt 0) {
            $sorted = $n.Children | Sort-Object Label
            $n.Children.Clear()
            foreach ($child in $sorted) {
                $n.Children.Add($child) | Out-Null
                $child.Level = $n.Level + 1
                Sort-NodeChildren $child
            }
        }
    }
    
    Sort-NodeChildren $rootNode
    return $rootNode
}

function Get-DeviceDetails($dev) {
    if ($null -eq $dev) {
        return @("No device selected.")
    }
    
    # Fetch driver properties dynamically for the selected device
    $props = Get-PnpDeviceProperty -InstanceId $dev.InstanceId -ErrorAction SilentlyContinue
    $driverVersion = ($props | Where-Object { $_.KeyName -eq 'DEVPKEY_Device_DriverVersion' }).Data
    $driverDate = ($props | Where-Object { $_.KeyName -eq 'DEVPKEY_Device_DriverDate' }).Data
    $driverProvider = ($props | Where-Object { $_.KeyName -eq 'DEVPKEY_Device_DriverProvider' }).Data
    $driverInf = ($props | Where-Object { $_.KeyName -eq 'DEVPKEY_Device_DriverInfPath' }).Data
    
    $lines = [System.Collections.ArrayList]::new()
    $lines.Add("Device Information") | Out-Null
    $lines.Add("──────────────────") | Out-Null
    $statusStr = if ($dev.Problem -eq 22) { "Disabled" } else { $dev.Status }
    $lines.Add("Name: $($dev.FriendlyName)") | Out-Null
    $lines.Add("Class: $($dev.Class) ($($dev.ClassGuid))") | Out-Null
    $lines.Add("Instance ID: $($dev.InstanceId)") | Out-Null
    $lines.Add("Status: $statusStr (Problem Code: $($dev.Problem))") | Out-Null
    $lines.Add("Manufacturer: $($dev.Manufacturer)") | Out-Null
    $lines.Add("Service: $($dev.Service)") | Out-Null
    $lines.Add("") | Out-Null
    $lines.Add("Driver Details") | Out-Null
    $lines.Add("──────────────") | Out-Null
    $lines.Add("Provider: $driverProvider") | Out-Null
    $lines.Add("Version: $driverVersion") | Out-Null
    $lines.Add("Driver Date: $driverDate") | Out-Null
    $lines.Add("INF Path: $driverInf") | Out-Null
    $lines.Add("") | Out-Null
    
    $hwIds = if ($dev.HardwareID) { $dev.HardwareID -join ", " } else { "None" }
    $lines.Add("Hardware IDs: $hwIds") | Out-Null
    
    return $lines
}

# --- DEVICE PROPERTIES DIALOG ---
function Show-DevicePropertiesDialog($dev, $screenWidth, $screenHeight) {
    if ($null -eq $dev) { return }
    
    $boxW = 70
    $boxH = 17
    $boxX = [int](($screenWidth - $boxW) / 2)
    $boxY = [int](($screenHeight - $boxH) / 2)
    if ($boxY -lt 1) { $boxY = 1 }
    if ($boxX -lt 0) { $boxX = 0 }
    
    $reset = Get-ANSIColor 'Reset'
    
    # Fetch driver properties
    $props = Get-PnpDeviceProperty -InstanceId $dev.InstanceId -ErrorAction SilentlyContinue
    $driverVersion = ($props | Where-Object { $_.KeyName -eq 'DEVPKEY_Device_DriverVersion' }).Data
    if ($null -eq $driverVersion) { $driverVersion = "Not Available" }
    $driverDate = ($props | Where-Object { $_.KeyName -eq 'DEVPKEY_Device_DriverDate' }).Data
    if ($null -eq $driverDate) { $driverDate = "Not Available" }
    $driverProvider = ($props | Where-Object { $_.KeyName -eq 'DEVPKEY_Device_DriverProvider' }).Data
    if ($null -eq $driverProvider) { $driverProvider = "Not Available" }
    $driverInf = ($props | Where-Object { $_.KeyName -eq 'DEVPKEY_Device_DriverInfPath' }).Data
    if ($null -eq $driverInf) { $driverInf = "Not Available" }
    $hwIds = @(if ($dev.HardwareID) { $dev.HardwareID } else { "None" })
    
    $tabs = @("General", "Driver", "Details")
    $currentTab = 0
    
    while ($true) {
        # 1. Draw outer frame
        $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'
        
        # Window Title
        $titleText = " $($dev.FriendlyName) Properties "
        if ($titleText.Length -gt ($boxW - 4)) { $titleText = $titleText.Substring(0, $boxW - 7) + "... " }
        $titleX = $boxX + [int](($boxW - $titleText.Length) / 2)
        Write-At $titleX $boxY $titleText 'SelectedActive'
        
        # 2. Draw Tabs
        $tabX = $boxX + 4
        $tabY = $boxY + 2
        for ($t = 0; $t -lt $tabs.Length; $t++) {
            $tabLabel = " $($tabs[$t]) "
            if ($t -eq $currentTab) {
                Write-At $tabX $tabY $tabLabel 'SelectedActive'
            } else {
                Write-At $tabX $tabY $tabLabel 'Header'
            }
            $tabX += $tabLabel.Length + 2
        }
        
        # Divider below tabs
        Write-At ($boxX + 2) ($boxY + 3) ("─" * ($boxW - 4)) 'Gray'
        
        # 3. Draw Tab Content
        $contentY = $boxY + 5
        switch ($currentTab) {
            0 { # General
                $statusStr = if ($dev.Problem -eq 22) { "Disabled" } else { $dev.Status }
                Write-At ($boxX + 4) ($contentY + 0) "Device Type: $($dev.Class)" 'White'
                Write-At ($boxX + 4) ($contentY + 1) "Manufacturer: $($dev.Manufacturer)" 'White'
                Write-At ($boxX + 4) ($contentY + 2) "Status: $statusStr" 'White'
                
                $probCode = if ($dev.Problem -eq 22) {
                    "This device is disabled. (Code 22)"
                } elseif ($dev.Problem -ne 0) {
                    "This device has a problem (Code $($dev.Problem))."
                } else {
                    "This device is working properly."
                }
                Write-At ($boxX + 4) ($contentY + 4) "Device Status:" 'Gray'
                # Status box
                Write-At ($boxX + 4) ($contentY + 5) "┌────────────────────────────────────────────────────────────┐" 'Gray'
                $paddedProb = " $probCode".PadRight(60).Substring(0, 60)
                Write-At ($boxX + 4) ($contentY + 6) "│$paddedProb│" 'Gray'
                Write-At ($boxX + 4) ($contentY + 7) "└────────────────────────────────────────────────────────────┘" 'Gray'
            }
            1 { # Driver
                Write-At ($boxX + 4) ($contentY + 0) "Driver Provider: $driverProvider" 'White'
                Write-At ($boxX + 4) ($contentY + 1) "Driver Date: $driverDate" 'White'
                Write-At ($boxX + 4) ($contentY + 2) "Driver Version: $driverVersion" 'White'
                Write-At ($boxX + 4) ($contentY + 3) "INF Name: $driverInf" 'White'
            }
            2 { # Details (Hardware IDs)
                Write-At ($boxX + 4) ($contentY + 0) "Hardware IDs:" 'Gray'
                for ($h = 0; $h -lt 5; $h++) {
                    $idLine = ""
                    if ($h -lt $hwIds.Count) { $idLine = [string]$hwIds[$h] }
                    $paddedId = $idLine.PadRight($boxW - 10).Substring(0, $boxW - 10)
                    Write-At ($boxX + 4) ($contentY + 2 + $h) $paddedId 'White'
                }
            }
        }
        
        # 4. Draw Footer
        Write-At ($boxX + 2) ($boxY + $boxH - 3) ("─" * ($boxW - 4)) 'Gray'
        Write-At ($boxX + 2) ($boxY + $boxH - 2) " [Left/Right] Switch Tabs | [Enter/Esc] Close properties" 'Header'
        
        # Read Key
        $key = [Console]::ReadKey($true)
        if ($key.Key -eq 'Escape' -or $key.Key -eq 'Enter') {
            break
        }
        elseif ($key.Key -eq 'LeftArrow') {
            $currentTab = ($currentTab - 1 + $tabs.Length) % $tabs.Length
        }
        elseif ($key.Key -eq 'RightArrow') {
            $currentTab = ($currentTab + 1) % $tabs.Length
        }
    }
}

# --- STATE VARIABLES ---
$byConnection = $false
$showHidden = $false

$statusText = "Loading device manager tree..."
$width = [Console]::WindowWidth
$height = [Console]::WindowHeight
Update-LayoutDimensions $width $height
$global:leftWidth = $width - 4

# Tree State
$rootNode = Get-DevicesAndTree $byConnection $showHidden
$treeVisibleNodes = Get-VisibleNodes $rootNode
$treeSelectedIndex = 0
$treeScrollOffset = 0

# Details State
$selectedNode = $treeVisibleNodes[$treeSelectedIndex]
$detailLines = Get-DeviceDetails $selectedNode.Device
$detailsScrollOffset = 0

$global:focusArea = 0 # 0: Tree, 1: Details

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

$redrawAll = $true
$needsTreeRedraw = $true
$needsDetailsRedraw = $true
$needsStatusRedraw = $true
$statusText = "Ready. Use arrows to browse devices. Press Shift+V for View settings."

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
            $global:leftWidth = $width - 4
            
            $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
            $detailLines = Get-DeviceDetails $selectedNode.Device
            
            [Console]::Write("$ESC[2J")
            $redrawAll = $true
        }
        
        # 2. Redraw Components
        if ($redrawAll) {
            # Draw borders for the entire screen width
            Draw-Borders $width $height $global:leftWidth $global:mainHeight $global:focusArea "DEVICES TREE" "" "" -HasVerticalDivider $false -HasHorizontalDivider $false
            Draw-Menu $width @("Shift+V: View", "Shift+U: Update", "Shift+D: Disable", "Shift+E: Enable", "Shift+X: Uninstall", "Shift+S: Scan", "P/Enter: Prop", "Ctrl+Q: Exit")
            $needsTreeRedraw = $true
            $needsStatusRedraw = $true
            $redrawAll = $false
        }
        
        if ($needsTreeRedraw) {
            Draw-TreeView $treeVisibleNodes $treeSelectedIndex $treeScrollOffset $global:leftWidth ($height - 4) $true
            $needsTreeRedraw = $false
        }
        
        if ($needsStatusRedraw) {
            Draw-Status $statusText "Device Manager" "Enter: Properties / Toggle │ V: View" $width $height
            $needsStatusRedraw = $false
        }
        
        Set-Cursor 0 0
        
        # 3. Input Handling
        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
            }
        }
        
        # Bottom layout shortcuts check
        $char = $key.KeyChar.ToString().ToUpper()
        $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift
        
        if ($isShift -and $key.Key -eq 'V') {
            # --- VIEW DIALOG MENU ---
            $viewSelectedIndex = if ($byConnection) { 1 } else { 0 }
            while ($true) {
                $typeIndicator = if (-not $byConnection) { "(*)" } else { "( )" }
                $connIndicator = if ($byConnection) { "(*)" } else { "( )" }
                $hiddenIndicator = if ($showHidden) { "[x]" } else { "[ ]" }
                
                $viewItems = [System.Collections.ArrayList]::new(@(
                    "$typeIndicator Devices by Type"
                    "$connIndicator Devices by Connection"
                    "$hiddenIndicator Show Hidden Devices"
                ))
                
                $res = Show-ListSelectionDialog "Choose a view option and press Enter:" "View Menu" $viewItems $viewSelectedIndex $width $height
                if ($null -eq $res) {
                    break
                }
                
                $viewSelectedIndex = $res
                if ($res -eq 0) {
                    $byConnection = $false
                    break # Apply and exit
                }
                elseif ($res -eq 1) {
                    $byConnection = $true
                    break # Apply and exit
                }
                elseif ($res -eq 2) {
                    $showHidden = -not $showHidden
                    # Keep dialog open to allow further selection or toggle!
                }
            }
            
            # Apply changes if we exited by choice (not escape)
            if ($null -ne $res) {
                $statusText = "Updating device tree view..."
                $needsStatusRedraw = $true
                Draw-Status $statusText "" "" $width $height
                
                $rootNode = Get-DevicesAndTree $byConnection $showHidden
                $treeVisibleNodes = Get-VisibleNodes $rootNode
                $treeSelectedIndex = 0
                $treeScrollOffset = 0
                
                $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
                $detailLines = Get-DeviceDetails $selectedNode.Device
                $detailsScrollOffset = 0
                
                $statusText = "View updated."
            }
            $redrawAll = $true
            continue
        }
        
        if ($isShift -and $key.Key -eq 'U') {
            # Update Driver
            $selectedDevice = $treeVisibleNodes[$treeSelectedIndex].Device
            if ($null -eq $selectedDevice) {
                $statusText = "Please select a device node first."
                $needsStatusRedraw = $true
                continue
            }
            
            $confirm = Show-InputBox "Type 'confirm' to confirm updating driver for this device:" "Confirm Update Driver" $width $height
            if ($confirm -eq 'confirm') {
                $infPath = Show-InputBox "Enter path to driver INF file (e.g. C:\Drivers\device.inf):" "Update Driver" $width $height
                if (-not [string]::IsNullOrEmpty($infPath)) {
                    $statusText = "Updating driver using pnputil..."
                    $needsStatusRedraw = $true
                    Draw-Status $statusText "" "" $width $height
                    
                    try {
                        $out = pnputil /add-driver $infPath /install
                        $statusText = "PnPUtiL completed: " + $out[0]
                    } catch {
                        $statusText = "Error installing driver: " + $_.Exception.Message
                    }
                }
            } else {
                $statusText = "Update cancelled."
            }
            $redrawAll = $true
            continue
        }
        
        if ($isShift -and $key.Key -eq 'D') {
            # Disable Device
            $selectedDevice = $treeVisibleNodes[$treeSelectedIndex].Device
            if ($null -eq $selectedDevice) {
                $statusText = "Please select a device node first."
                $needsStatusRedraw = $true
                continue
            }
            
            $confirm = Show-InputBox "Type 'confirm' to confirm disabling this device:" "Confirm Disable Device" $width $height
            if ($confirm -eq 'confirm') {
                $statusText = "Disabling device: $($selectedDevice.FriendlyName)..."
                $needsStatusRedraw = $true
                Draw-Status $statusText "" "" $width $height
                
                try {
                    Disable-PnpDevice -InstanceId $selectedDevice.InstanceId -Confirm:$false -ErrorAction Stop
                    $statusText = "Device disabled successfully."
                } catch {
                    # Fall back to pnputil
                    try {
                        pnputil /disable-device $selectedDevice.InstanceId | Out-Null
                        $statusText = "Device disabled (via pnputil)."
                    } catch {
                        $statusText = "Failed to disable: " + $_.Exception.Message
                    }
                }
                # Reload tree
                $rootNode = Get-DevicesAndTree $byConnection $showHidden
                $treeVisibleNodes = Get-VisibleNodes $rootNode
                $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
                $detailLines = Get-DeviceDetails $selectedNode.Device
            } else {
                $statusText = "Disable cancelled."
            }
            $redrawAll = $true
            continue
        }
        
        if ($isShift -and $key.Key -eq 'E') {
            # Enable Device
            $selectedDevice = $treeVisibleNodes[$treeSelectedIndex].Device
            if ($null -eq $selectedDevice) {
                $statusText = "Please select a device node first."
                $needsStatusRedraw = $true
                continue
            }
            
            $confirm = Show-InputBox "Type 'confirm' to confirm enabling this device:" "Confirm Enable Device" $width $height
            if ($confirm -eq 'confirm') {
                $statusText = "Enabling device: $($selectedDevice.FriendlyName)..."
                $needsStatusRedraw = $true
                Draw-Status $statusText "" "" $width $height
                
                try {
                    Enable-PnpDevice -InstanceId $selectedDevice.InstanceId -Confirm:$false -ErrorAction Stop
                    $statusText = "Device enabled successfully."
                } catch {
                    # Fall back to pnputil
                    try {
                        pnputil /enable-device $selectedDevice.InstanceId | Out-Null
                        $statusText = "Device enabled (via pnputil)."
                    } catch {
                        $statusText = "Failed to enable: " + $_.Exception.Message
                    }
                }
                # Reload tree
                $rootNode = Get-DevicesAndTree $byConnection $showHidden
                $treeVisibleNodes = Get-VisibleNodes $rootNode
                $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
                $detailLines = Get-DeviceDetails $selectedNode.Device
            } else {
                $statusText = "Enable cancelled."
            }
            $redrawAll = $true
            continue
        }
        
        if ($isShift -and $key.Key -eq 'X') {
            # Uninstall Device
            $selectedDevice = $treeVisibleNodes[$treeSelectedIndex].Device
            if ($null -eq $selectedDevice) {
                $statusText = "Please select a device node first."
                $needsStatusRedraw = $true
                continue
            }
            
            $confirm = Show-InputBox "Type 'confirm' to confirm uninstalling this device:" "Confirm Uninstall Device" $width $height
            if ($confirm -eq 'confirm') {
                $statusText = "Uninstalling device..."
                $needsStatusRedraw = $true
                Draw-Status $statusText "" "" $width $height
                
                try {
                    pnputil /remove-device $selectedDevice.InstanceId | Out-Null
                    $statusText = "Device uninstalled successfully."
                } catch {
                    $statusText = "Failed to uninstall: " + $_.Exception.Message
                }
                
                # Reload tree
                $rootNode = Get-DevicesAndTree $byConnection $showHidden
                $treeVisibleNodes = Get-VisibleNodes $rootNode
                $treeSelectedIndex = 0
                $treeScrollOffset = 0
                $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
                $detailLines = Get-DeviceDetails $selectedNode.Device
            } else {
                $statusText = "Uninstall cancelled."
            }
            $redrawAll = $true
            continue
        }
        
        if ($isShift -and $key.Key -eq 'S') {
            # Scan for new hardware
            $statusText = "Scanning for hardware changes..."
            $needsStatusRedraw = $true
            Draw-Status $statusText "" "" $width $height
            
            try {
                pnputil /scan-devices | Out-Null
                $statusText = "Hardware scan completed successfully."
            } catch {
                $statusText = "Scan failed: " + $_.Exception.Message
            }
            
            # Reload tree
            $rootNode = Get-DevicesAndTree $byConnection $showHidden
            $treeVisibleNodes = Get-VisibleNodes $rootNode
            $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
            $detailLines = Get-DeviceDetails $selectedNode.Device
            
            $redrawAll = $true
            continue
        }
        
        if ($char -eq 'P' -or $key.Key -eq 'Enter') {
            $selectedDevice = $treeVisibleNodes[$treeSelectedIndex].Device
            if ($selectedDevice) {
                Show-DevicePropertiesDialog $selectedDevice $width $height
            } elseif ($key.Key -eq 'Enter') {
                # Toggle expansion of non-leaf category
                $node = $treeVisibleNodes[$treeSelectedIndex]
                if (-not $node.IsLeaf) {
                    $node.IsExpanded = -not $node.IsExpanded
                    $treeVisibleNodes = Get-VisibleNodes $rootNode
                    $needsTreeRedraw = $true
                }
            }
            $redrawAll = $true
            continue
        }
        
        # --- 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 }
                    $selectedNode = $treeVisibleNodes[$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
                    }
                    $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
                    $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 }
                $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
                $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
                }
                $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
                $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 'ComputerRoot') {
                    $parentIndex = $treeVisibleNodes.IndexOf($node.Parent)
                    if ($parentIndex -ge 0) {
                        $treeSelectedIndex = $parentIndex
                        if ($treeSelectedIndex -lt $treeScrollOffset) { $treeScrollOffset = $treeSelectedIndex }
                        $selectedNode = $treeVisibleNodes[$treeSelectedIndex]
                        $needsTreeRedraw = $true
                    }
                }
            }
            elseif ($key.Key -eq 'Tab') {
                $global:focusArea = 1
                $redrawAll = $true
            }
        }
        elseif ($global:focusArea -eq 1) {
            # Details View scrolling
            $detailHeight = $height - 4
            
            if ($key.Key -eq 'UpArrow') {
                if ($detailsScrollOffset -gt 0) {
                    $detailsScrollOffset--
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'DownArrow') {
                if ($detailsScrollOffset -lt ($detailLines.Count - $detailHeight)) {
                    $detailsScrollOffset++
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'Tab' -or $key.Key -eq 'Escape') {
                $global:focusArea = 0
                $redrawAll = $true
            }
        }
    }
} finally {
    Restore-Console
}