Public/Show-sqmToolGui.ps1

function Show-sqmToolGui
{
<#
.SYNOPSIS
    Launches a small graphical interface (WinForms) for all sqmSQLTool functions.
.DESCRIPTION
    Shows every exported module function grouped by category in a tree. After selecting a
    function its parameters are generated automatically as input fields. The user can fill in
    values, see a live command preview and run the command directly, copy it to the clipboard
    or display its help.

    The grouping comes from Public\category-map.ps1. Functions without an entry land under
    "Other". Read-only functions (Get-/Test-) are safe to run; state-changing functions that
    support -WhatIf automatically get a "WhatIf (simulation)" option that is enabled by default.

    The interface uses a Visual Studio "Dark" colour scheme.
.PARAMETER Filter
    Optional initial filter for the function list (wildcards allowed).
.EXAMPLE
    Show-sqmToolGui
    Opens the graphical interface with all functions.
.EXAMPLE
    Show-sqmToolGui -Filter '*AlwaysOn*'
    Opens the interface filtered directly to Always-On functions.
.NOTES
    Requires Windows PowerShell with WinForms (System.Windows.Forms). Runs synchronously in the
    current runspace: long operations block the interface while they execute.
#>

    [CmdletBinding()]
    param (
        [string]$Filter = '*'
    )

    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -AssemblyName System.Drawing

    # --- Visual Studio "Dark" colour palette ---------------------------------------
    $cWindow = [System.Drawing.Color]::FromArgb(30, 30, 30)    # editors / tree / output
    $cPanel  = [System.Drawing.Color]::FromArgb(45, 45, 48)    # form / panels
    $cText   = [System.Drawing.Color]::FromArgb(220, 220, 220) # foreground
    $cDim    = [System.Drawing.Color]::FromArgb(153, 153, 153) # secondary text
    $cBtn    = [System.Drawing.Color]::FromArgb(62, 62, 66)    # buttons
    $cAccent = [System.Drawing.Color]::FromArgb(0, 122, 204)   # VS blue
    $cBorder = [System.Drawing.Color]::FromArgb(63, 63, 70)

    $styleButton = {
        param ($b)
        $b.FlatStyle = 'Flat'
        $b.BackColor = $cBtn
        $b.ForeColor = $cText
        $b.FlatAppearance.BorderColor = $cBorder
        $b.FlatAppearance.MouseOverBackColor = $cAccent
    }

    # --- Load category mapping -----------------------------------------------------
    $categoryMap = @{ }
    $mapFile = Join-Path $PSScriptRoot 'category-map.ps1'
    if (Test-Path $mapFile)
    {
        . $mapFile   # defines $categoryMap
    }

    # --- Discover functions ---------------------------------------------------------
    # Authoritative source = the manifest's FunctionsToExport (the declared public API).
    # Never enumerate Get-Command alone: if the module was imported via the bare .psm1,
    # internal helpers (and even dbatools cmdlets) would leak in under "Other".
    $module = Get-Module -Name 'sqmSQLTool'
    if (-not $module)
    {
        [System.Windows.Forms.MessageBox]::Show(
            "No sqmSQLTool functions were found. Please run 'Import-Module sqmSQLTool' first.",
            'sqmSQLTool', 'OK', 'Warning') | Out-Null
        return
    }
    $publicNames = @()
    $manifestPath = Join-Path $module.ModuleBase 'sqmSQLTool.psd1'
    if (Test-Path $manifestPath)
    {
        $publicNames = (Import-PowerShellDataFile $manifestPath).FunctionsToExport
    }
    if (-not $publicNames) { $publicNames = $module.ExportedFunctions.Keys }

    $commands = $publicNames |
    ForEach-Object { Get-Command -Name $_ -Module 'sqmSQLTool' -ErrorAction SilentlyContinue } |
    Where-Object { $_ } | Sort-Object Name
    if (-not $commands)
    {
        [System.Windows.Forms.MessageBox]::Show(
            "No sqmSQLTool functions were found. Please run 'Import-Module sqmSQLTool' first.",
            'sqmSQLTool', 'OK', 'Warning') | Out-Null
        return
    }

    # Function -> category (fallback: Other)
    $funcByCat = @{ }
    foreach ($c in $commands)
    {
        $cat = if ($categoryMap.ContainsKey($c.Name)) { $categoryMap[$c.Name] } else { 'Other' }
        if (-not $funcByCat.ContainsKey($cat)) { $funcByCat[$cat] = [System.Collections.Generic.List[string]]::new() }
        $funcByCat[$cat].Add($c.Name)
    }

    # --- Main window ---------------------------------------------------------------
    $form = New-Object System.Windows.Forms.Form
    $yearSpan = "2025-$((Get-Date).ToString('yy'))"
    $form.Text = "sqmSQLTool - Function Browser v$($module.Version) [$($commands.Count)] | powershelldba.de - Janke (c) $yearSpan"
    $form.Size = New-Object System.Drawing.Size(1150, 720)
    $form.StartPosition = 'CenterScreen'
    $form.MinimumSize = New-Object System.Drawing.Size(900, 560)
    $form.Font = New-Object System.Drawing.Font('Segoe UI', 9)
    $form.BackColor = $cPanel
    $form.ForeColor = $cText

    # One shared ToolTip instance for the whole form (no per-parameter handle leak)
    $tip = New-Object System.Windows.Forms.ToolTip

    # Split: left (tree) / right (details). 33 / 66 ratio, kept on resize.
    $split = New-Object System.Windows.Forms.SplitContainer
    $split.Dock = 'Fill'
    $split.FixedPanel = 'None'
    $split.Panel1MinSize = 220
    $split.BackColor = $cBorder
    $form.Controls.Add($split)

    # --- Left side: search box + TreeView ------------------------------------------
    $searchBox = New-Object System.Windows.Forms.TextBox
    $searchBox.Dock = 'Top'
    $searchBox.Margin = '3,3,3,3'
    $searchBox.BackColor = $cWindow
    $searchBox.ForeColor = $cText
    $searchBox.BorderStyle = 'FixedSingle'
    $lblSearch = New-Object System.Windows.Forms.Label
    $lblSearch.Text = 'Search / filter:'
    $lblSearch.Dock = 'Top'
    $lblSearch.Height = 18
    $lblSearch.ForeColor = $cDim

    $tree = New-Object System.Windows.Forms.TreeView
    $tree.Dock = 'Fill'
    $tree.HideSelection = $false
    $tree.Font = New-Object System.Drawing.Font('Segoe UI', 9)
    $tree.BackColor = $cWindow
    $tree.ForeColor = $cText
    $tree.BorderStyle = 'None'
    $tree.LineColor = $cDim

    $split.Panel1.Controls.Add($tree)
    $split.Panel1.Controls.Add($searchBox)
    $split.Panel1.Controls.Add($lblSearch)
    $split.Panel1.BackColor = $cPanel

    # (Re-)populate the tree from a filter
    $populateTree = {
        param ($flt)
        $tree.BeginUpdate()
        $tree.Nodes.Clear()
        if ([string]::IsNullOrWhiteSpace($flt)) { $flt = '*' }
        if ($flt -notmatch '[\*\?]') { $flt = "*$flt*" }
        foreach ($cat in ($funcByCat.Keys | Sort-Object))
        {
            $matched = $funcByCat[$cat] | Where-Object { $_ -like $flt }
            if (-not $matched) { continue }
            $catNode = $tree.Nodes.Add("$cat ($($matched.Count))")
            $catNode.Tag = $null
            foreach ($fn in ($matched | Sort-Object))
            {
                $n = $catNode.Nodes.Add($fn)
                $n.Tag = $fn
            }
        }
        if ($tree.Nodes.Count -le 4) { $tree.ExpandAll() }
        $tree.EndUpdate()
    }

    # --- Right side: layout --------------------------------------------------------
    $right = New-Object System.Windows.Forms.TableLayoutPanel
    $right.Dock = 'Fill'
    $right.ColumnCount = 1
    $right.RowCount = 4
    $right.Padding = '6,6,6,6'
    $right.BackColor = $cPanel
    [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 130)))  # header
    [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 55)))     # parameters
    [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Absolute', 122)))   # preview + options + buttons
    [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle('Percent', 45)))     # output
    $split.Panel2.Controls.Add($right)
    $split.Panel2.BackColor = $cPanel

    # Header: function name + synopsis
    $header = New-Object System.Windows.Forms.Panel
    $header.Dock = 'Fill'
    $lblFunc = New-Object System.Windows.Forms.Label
    $lblFunc.Font = New-Object System.Drawing.Font('Segoe UI', 12, [System.Drawing.FontStyle]::Bold)
    $lblFunc.Dock = 'Top'
    $lblFunc.Height = 26
    $lblFunc.Text = 'Select a function on the left'
    $lblFunc.ForeColor = $cText
    $lblSyn = New-Object System.Windows.Forms.Label
    $lblSyn.Dock = 'Fill'
    $lblSyn.ForeColor = $cDim
    $lblSyn.AutoSize = $false
    $lblSyn.Padding = '0,4,0,0'
    $header.Controls.Add($lblSyn)
    $header.Controls.Add($lblFunc)
    $right.Controls.Add($header, 0, 0)

    # Parameter area (scrollable). FlowLayoutPanel mit einem festen Zeilen-Panel pro Parameter
    # -> Label und Eingabe stehen garantiert auf einer Linie (kein TableLayout-Zentrieren).
    $paramPanel = New-Object System.Windows.Forms.FlowLayoutPanel
    $paramPanel.Dock = 'Fill'
    $paramPanel.AutoScroll = $true
    $paramPanel.FlowDirection = 'TopDown'
    $paramPanel.WrapContents = $false
    $paramPanel.BackColor = $cPanel
    $grpParams = New-Object System.Windows.Forms.GroupBox
    $grpParams.Text = 'Parameters'
    $grpParams.Dock = 'Fill'
    $grpParams.ForeColor = $cText
    $grpParams.Controls.Add($paramPanel)
    $right.Controls.Add($grpParams, 0, 1)

    # Preview + buttons
    $midPanel = New-Object System.Windows.Forms.Panel
    $midPanel.Dock = 'Fill'
    $preview = New-Object System.Windows.Forms.TextBox
    $preview.Multiline = $true
    $preview.ReadOnly = $true
    $preview.Dock = 'Top'
    $preview.Height = 56
    $preview.ScrollBars = 'Vertical'
    $preview.BackColor = $cWindow
    $preview.ForeColor = [System.Drawing.Color]::FromArgb(86, 156, 214) # VS string blue
    $preview.BorderStyle = 'FixedSingle'
    $preview.Font = New-Object System.Drawing.Font('Consolas', 9)

    $btnPanel = New-Object System.Windows.Forms.FlowLayoutPanel
    $btnPanel.Dock = 'Bottom'
    $btnPanel.Height = 34
    $btnRun = New-Object System.Windows.Forms.Button
    $btnRun.Text = 'Run command'
    $btnRun.Width = 130
    $btnRun.Enabled = $false
    $btnCopy = New-Object System.Windows.Forms.Button
    $btnCopy.Text = 'Copy to clipboard'
    $btnCopy.Width = 130
    $btnCopy.Enabled = $false
    $btnHelp = New-Object System.Windows.Forms.Button
    $btnHelp.Text = 'Help'
    $btnHelp.Width = 80
    $btnHelp.Enabled = $false
    foreach ($b in @($btnRun, $btnCopy, $btnHelp)) { & $styleButton $b }
    $btnPanel.Controls.AddRange(@($btnRun, $btnCopy, $btnHelp))

    # WhatIf on its own line (full label always visible)
    $optPanel = New-Object System.Windows.Forms.Panel
    $optPanel.Dock = 'Bottom'
    $optPanel.Height = 26
    $chkWhatIf = New-Object System.Windows.Forms.CheckBox
    $chkWhatIf.Text = 'WhatIf (simulation - shows what would happen, changes nothing)'
    $chkWhatIf.Dock = 'Fill'
    $chkWhatIf.TextAlign = 'MiddleLeft'
    $chkWhatIf.Checked = $true
    $chkWhatIf.Visible = $false
    $chkWhatIf.ForeColor = $cText
    $optPanel.Controls.Add($chkWhatIf)

    # Dock order: buttons (very bottom) -> options (above buttons) -> preview (fills top)
    $midPanel.Controls.Add($btnPanel)
    $midPanel.Controls.Add($optPanel)
    $midPanel.Controls.Add($preview)
    $right.Controls.Add($midPanel, 0, 2)

    # Output area
    $grpOut = New-Object System.Windows.Forms.GroupBox
    $grpOut.Text = 'Output'
    $grpOut.Dock = 'Fill'
    $grpOut.ForeColor = $cText
    $output = New-Object System.Windows.Forms.TextBox
    $output.Multiline = $true
    $output.ReadOnly = $true
    $output.Dock = 'Fill'
    $output.ScrollBars = 'Both'
    $output.WordWrap = $false
    $output.BackColor = $cWindow
    $output.ForeColor = $cText
    $output.BorderStyle = 'FixedSingle'
    $output.Font = New-Object System.Drawing.Font('Consolas', 9)
    $grpOut.Controls.Add($output)
    $right.Controls.Add($grpOut, 0, 3)

    # --- State of the currently selected function ----------------------------------
    # Controls: regular inputs (textbox/checkbox/combobox). Creds: PSCredential parameters.
    $script:guiState = @{ Command = $null; Controls = @{ }; Creds = @{ } }
    $script:synCache = @{ }
    $common = [System.Management.Automation.PSCmdlet]::CommonParameters +
    [System.Management.Automation.PSCmdlet]::OptionalCommonParameters

    # Build the command preview from the inputs ------------------------------------
    $buildCommand = {
        if (-not $script:guiState.Command) { return $null }
        $fn = $script:guiState.Command.Name
        $parts = [System.Collections.Generic.List[string]]::new()
        $parts.Add($fn)
        foreach ($pname in $script:guiState.Controls.Keys)
        {
            $ctrl = $script:guiState.Controls[$pname]
            if ($ctrl -is [System.Windows.Forms.CheckBox])
            {
                if ($ctrl.Checked) { $parts.Add("-$pname") }
            }
            elseif ($ctrl -is [System.Windows.Forms.ComboBox])
            {
                if ($ctrl.SelectedItem) { $parts.Add("-$pname $($ctrl.SelectedItem)") }
            }
            else
            {
                $val = $ctrl.Text
                if (-not [string]::IsNullOrWhiteSpace($val))
                {
                    if ($val -match '[\s'']') { $val = "'" + ($val -replace "'", "''") + "'" }
                    $parts.Add("-$pname $val")
                }
            }
        }
        foreach ($cn in $script:guiState.Creds.Keys)
        {
            $cred = $script:guiState.Creds[$cn].Cred
            if ($cred) { $parts.Add("-$cn (Get-Credential -UserName '$($cred.UserName)' -Message '...')") }
        }
        if ($chkWhatIf.Visible -and $chkWhatIf.Checked) { $parts.Add('-WhatIf') }
        return ($parts -join ' ')
    }

    $updatePreview = { $preview.Text = (& $buildCommand) }

    # Build the parameter fields for a function -------------------------------------
    $loadFunction = {
        param ($fnName)
        $cmd = Get-Command $fnName -ErrorAction SilentlyContinue
        if (-not $cmd) { return }
        $script:guiState.Command = $cmd
        $script:guiState.Controls = @{ }
        $script:guiState.Creds = @{ }

        $lblFunc.Text = $fnName
        # Parse the synopsis quickly from the source file (Get-Help is much slower), cached.
        if (-not $script:synCache.ContainsKey($fnName))
        {
            $syn = ''
            $srcFile = $cmd.ScriptBlock.File
            if ($srcFile -and (Test-Path $srcFile))
            {
                $src = Get-Content $srcFile -Raw
                if ($src -match '(?ms)\.SYNOPSIS[ \t]*\r?\n(.*?)\r?\n[ \t]*(?:\.[A-Z]|#>)')
                {
                    $syn = ($Matches[1] -replace '(?m)^\s+', '').Trim()
                }
            }
            $script:synCache[$fnName] = $syn
        }
        $lblSyn.Text = $script:synCache[$fnName]

        # Show WhatIf only when the function supports ShouldProcess
        $supportsWhatIf = $cmd.Parameters.ContainsKey('WhatIf')
        $chkWhatIf.Visible = $supportsWhatIf
        $chkWhatIf.Checked = $supportsWhatIf

        $paramPanel.SuspendLayout()
        $paramPanel.Controls.Clear()
        $row = 0
        foreach ($p in $cmd.Parameters.Values)
        {
            if ($common -contains $p.Name) { continue }
            if ($p.Name -in @('WhatIf', 'Confirm')) { continue }

            $isMandatory = $false
            foreach ($a in $p.Attributes)
            {
                if ($a -is [System.Management.Automation.ParameterAttribute] -and $a.Mandatory) { $isMandatory = $true }
            }
            # ValidateSet -> allowed values for a dropdown
            $validValues = $null
            $vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } | Select-Object -First 1
            if ($vsAttr) { $validValues = $vsAttr.ValidValues }

            $lbl = New-Object System.Windows.Forms.Label
            $lbl.Text = $p.Name + $(if ($isMandatory) { ' *' } else { '' })
            $lbl.AutoSize = $false
            $lbl.Location = New-Object System.Drawing.Point(3, 6)
            $lbl.Width = 185
            $lbl.Height = 20
            $lbl.TextAlign = 'MiddleLeft'
            $lbl.ForeColor = if ($isMandatory) { $cText } else { $cDim }
            if ($isMandatory) { $lbl.Font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold) }
            $tip.SetToolTip($lbl, "$($p.ParameterType.Name)")

            $pt = $p.ParameterType
            $isCred = $false
            if ($pt -eq [switch] -or $pt -eq [bool])
            {
                $ctrl = New-Object System.Windows.Forms.CheckBox
                $ctrl.AutoSize = $true
                $ctrl.ForeColor = $cText
                $ctrl.Add_CheckedChanged($updatePreview)
            }
            elseif ($validValues -or $pt.IsEnum)
            {
                $ctrl = New-Object System.Windows.Forms.ComboBox
                $ctrl.DropDownStyle = 'DropDownList'
                $ctrl.Width = 280
                $ctrl.BackColor = $cWindow
                $ctrl.ForeColor = $cText
                $ctrl.FlatStyle = 'Flat'
                [void]$ctrl.Items.Add('')
                $values = if ($validValues) { $validValues } else { [Enum]::GetNames($pt) }
                foreach ($ev in $values) { [void]$ctrl.Items.Add($ev) }
                $ctrl.Add_SelectedIndexChanged($updatePreview)
            }
            elseif ($pt.Name -eq 'PSCredential')
            {
                # Credential: read-only username box + Set.../Clear buttons; the PSCredential
                # object is stored in $guiState.Creds, not in Controls.
                $isCred = $true
                $ctrl = New-Object System.Windows.Forms.FlowLayoutPanel
                $ctrl.AutoSize = $false
                $ctrl.Width = 320
                $ctrl.Height = 26
                $ctrl.WrapContents = $false
                $ctrl.Margin = '0,0,0,0'
                $txtUser = New-Object System.Windows.Forms.TextBox
                $txtUser.Width = 190
                $txtUser.ReadOnly = $true
                $txtUser.BackColor = $cWindow
                $txtUser.ForeColor = $cText
                $txtUser.BorderStyle = 'FixedSingle'
                $btnSet = New-Object System.Windows.Forms.Button
                $btnSet.Text = 'Set...'
                $btnSet.Width = 60
                & $styleButton $btnSet
                $btnClr = New-Object System.Windows.Forms.Button
                $btnClr.Text = 'Clear'
                $btnClr.Width = 55
                & $styleButton $btnClr
                $pn = $p.Name
                $btnSet.Add_Click({
                        $c = Get-Credential -Message "Credential for -$pn" -ErrorAction SilentlyContinue
                        if ($c) { $script:guiState.Creds[$pn].Cred = $c; $txtUser.Text = $c.UserName; & $updatePreview }
                    }.GetNewClosure())
                $btnClr.Add_Click({
                        $script:guiState.Creds[$pn].Cred = $null; $txtUser.Text = ''; & $updatePreview
                    }.GetNewClosure())
                $ctrl.Controls.AddRange(@($txtUser, $btnSet, $btnClr))
                $script:guiState.Creds[$pn] = @{ Cred = $null }
            }
            else
            {
                $ctrl = New-Object System.Windows.Forms.TextBox
                $ctrl.Width = 320
                $ctrl.BackColor = $cWindow
                $ctrl.ForeColor = $cText
                $ctrl.BorderStyle = 'FixedSingle'
                # Pre-fill instance parameters with the current machine name
                if ($p.Name -in @('SqlInstance', 'Instance')) { $ctrl.Text = $env:COMPUTERNAME }
                $ctrl.Add_TextChanged($updatePreview)
            }
            # Ein Zeilen-Panel pro Parameter: Label links, Eingabe rechts, auf einer Linie.
            $rowP = New-Object System.Windows.Forms.Panel
            $rowP.Width = 540
            $rowP.Height = 30
            $rowP.Margin = '0,0,0,2'
            $rowP.BackColor = $cPanel
            # Control vertikal mittig zur Zeile positionieren (CheckBox kleiner als Textbox)
            $ctrlHeight = if ($ctrl -is [System.Windows.Forms.CheckBox]) { 18 } else { $ctrl.Height }
            $ctrl.Location = New-Object System.Drawing.Point(195, [Math]::Max(2, [int]((30 - $ctrlHeight) / 2)))
            $rowP.Controls.Add($lbl)
            $rowP.Controls.Add($ctrl)
            $paramPanel.Controls.Add($rowP)
            if (-not $isCred) { $script:guiState.Controls[$p.Name] = $ctrl }
            $row++
        }
        if ($row -eq 0)
        {
            $none = New-Object System.Windows.Forms.Label
            $none.Text = '(No parameters)'
            $none.AutoSize = $true
            $none.ForeColor = $cDim
            $paramPanel.Controls.Add($none)
        }
        $paramPanel.ResumeLayout()

        $btnRun.Enabled = $true
        $btnCopy.Enabled = $true
        $btnHelp.Enabled = $true
        & $updatePreview

        # Output sofort aktualisieren: Funktionsname + Synopsis + Befehlsvorschau
        $sep = '-' * [Math]::Max($fnName.Length, 12)
        $output.Text = "$fnName`r`n$sep`r`n$($script:synCache[$fnName])`r`n`r`nVorschau:`r`n $(& $buildCommand)`r`n"
    }

    # --- Events --------------------------------------------------------------------
    $tree.Add_AfterSelect({
            $node = $tree.SelectedNode
            if ($node -and $node.Tag) { & $loadFunction $node.Tag }
        })

    # Auch ein erneuter Klick auf einen bereits ausgewaehlten Funktions-Node aktualisiert sofort
    $tree.Add_NodeMouseClick({
            param ($sender, $e)
            if ($e.Node -and $e.Node.Tag) { & $loadFunction $e.Node.Tag }
        })

    $searchBox.Add_TextChanged({ & $populateTree $searchBox.Text })

    $btnCopy.Add_Click({
            $cmd = & $buildCommand
            if ($cmd) { [System.Windows.Forms.Clipboard]::SetText($cmd) }
        })

    $btnHelp.Add_Click({
            if ($script:guiState.Command)
            {
                $h = Get-Help $script:guiState.Command.Name -Full -ErrorAction SilentlyContinue | Out-String
                $output.Text = $h
            }
        })

    $btnRun.Add_Click({
            if (-not $script:guiState.Command) { return }
            # Validate mandatory fields
            $missing = @()
            foreach ($pname in $script:guiState.Controls.Keys)
            {
                $p = $script:guiState.Command.Parameters[$pname]
                $man = $false
                foreach ($a in $p.Attributes) { if ($a -is [System.Management.Automation.ParameterAttribute] -and $a.Mandatory) { $man = $true } }
                if ($man)
                {
                    $ctrl = $script:guiState.Controls[$pname]
                    if ($ctrl -is [System.Windows.Forms.TextBox] -and [string]::IsNullOrWhiteSpace($ctrl.Text)) { $missing += $pname }
                }
            }
            foreach ($cn in $script:guiState.Creds.Keys)
            {
                $cp = $script:guiState.Command.Parameters[$cn]
                $man = $false
                foreach ($a in $cp.Attributes) { if ($a -is [System.Management.Automation.ParameterAttribute] -and $a.Mandatory) { $man = $true } }
                if ($man -and -not $script:guiState.Creds[$cn].Cred) { $missing += $cn }
            }
            if ($missing.Count -gt 0)
            {
                [System.Windows.Forms.MessageBox]::Show("Required parameters missing:`n - $($missing -join "`n - ")", 'Incomplete input', 'OK', 'Warning') | Out-Null
                return
            }

            # Build parameter hashtable (splatting)
            $params = @{ }
            foreach ($pname in $script:guiState.Controls.Keys)
            {
                $ctrl = $script:guiState.Controls[$pname]
                if ($ctrl -is [System.Windows.Forms.CheckBox]) { if ($ctrl.Checked) { $params[$pname] = $true } }
                elseif ($ctrl -is [System.Windows.Forms.ComboBox]) { if ($ctrl.SelectedItem) { $params[$pname] = [string]$ctrl.SelectedItem } }
                else { if (-not [string]::IsNullOrWhiteSpace($ctrl.Text)) { $params[$pname] = $ctrl.Text } }
            }
            foreach ($cn in $script:guiState.Creds.Keys)
            {
                if ($script:guiState.Creds[$cn].Cred) { $params[$cn] = $script:guiState.Creds[$cn].Cred }
            }
            if ($chkWhatIf.Visible -and $chkWhatIf.Checked) { $params['WhatIf'] = $true }

            $fn = $script:guiState.Command.Name
            $output.Text = ">> $(& $buildCommand)`r`n`r`n"
            $form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
            $btnRun.Enabled = $false
            # Detect connection/network errors and report them clearly
            $connHint = '(?i)network-related|server was not found|login failed|certificate chain|untrusted|timeout|connect to|connection.*(fail|refused|reset)|sql server.*not (found|accessible)|named pipes|tcp provider'
            try
            {
                $records = & $fn @params 2>&1
                $errRecords = $records | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }
                $normal = $records | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }

                $txt = ($normal | Out-String)
                if ($txt.Trim()) { $output.AppendText($txt) }

                foreach ($er in $errRecords)
                {
                    $msg = "$($er.Exception.Message) $($er.Exception.InnerException.Message)"
                    if ($msg -match $connHint)
                    {
                        $output.AppendText("`r`nSQL CONNECTION FAILED`r`nInstance '$($params['SqlInstance'])' is unreachable or login was refused.`r`nDetails: $($er.Exception.Message)`r`n")
                    }
                    else
                    {
                        $output.AppendText("`r`nERROR: $($er.Exception.Message)`r`n")
                    }
                }
                if (-not $txt.Trim() -and -not $errRecords) { $output.AppendText("(No result / no output)`r`n") }
            }
            catch
            {
                $msg = "$($_.Exception.Message) $($_.Exception.InnerException.Message)"
                if ($msg -match $connHint)
                {
                    $output.AppendText("SQL CONNECTION FAILED`r`nInstance '$($params['SqlInstance'])' is unreachable or login was refused.`r`nDetails: $($_.Exception.Message)`r`n")
                }
                else
                {
                    $output.AppendText("ERROR ($($_.Exception.GetType().Name)): $($_.Exception.Message)`r`n")
                }
            }
            finally
            {
                $form.Cursor = [System.Windows.Forms.Cursors]::Default
                $btnRun.Enabled = $true
            }
        })

    # Keep the 33 / 66 split ratio on show and on resize
    $applySplit = {
        if ($split.Width -gt ($split.Panel1MinSize + $split.Panel2MinSize + 10))
        {
            $split.SplitterDistance = [int]($split.Width * 0.33)
        }
    }
    $split.Add_SizeChanged($applySplit)

    # Bring the window to the front after loading and set the split ratio
    $form.Add_Shown({
            & $applySplit
            $form.Activate()
            $form.BringToFront()
            $form.TopMost = $true
            $form.TopMost = $false
        })

    # --- Initial population --------------------------------------------------------
    $searchBox.Text = if ($Filter -eq '*') { '' } else { $Filter }
    & $populateTree $searchBox.Text

    [void]$form.ShowDialog()
    $tip.Dispose()
    $form.Dispose()
}