Terminal/SRxTerminalBoxControl.psm1
|
Import-Module (Join-Path $PSScriptRoot 'SRxConPty.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue Import-Module (Join-Path $PSScriptRoot 'SRxMonacoEditor.psm1') -DisableNameChecking -Force -ErrorAction Stop Import-Module (Join-Path $PSScriptRoot 'SRxVSCode.psm1') -DisableNameChecking -Force -ErrorAction Stop function Get-SRxLoadedAssembly { [CmdletBinding()] param([Parameter(Mandatory)][string]$SimpleName) return [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $SimpleName } | Select-Object -First 1 } function Import-SRxWebView2Assemblies { [CmdletBinding()] param( [string]$LibPath = (Join-Path $PSScriptRoot 'Lib\WebView2') ) if (Get-SRxLoadedAssembly 'Microsoft.Web.WebView2.WinForms') { return $true } try { $core = Join-Path $LibPath 'Microsoft.Web.WebView2.Core.dll' $winf = Join-Path $LibPath 'Microsoft.Web.WebView2.WinForms.dll' if (-not (Test-Path $core) -or -not (Test-Path $winf)) { Write-Host "Import-SRxWebView2Assemblies - SDK assemblies not found under '$LibPath'." return $false } Add-Type -Path $core -ErrorAction Stop Add-Type -Path $winf -ErrorAction Stop return $true } catch { Write-Host "Import-SRxWebView2Assemblies - $($_.Exception.Message)" return $false } } function Get-SRxWebView2Type { [CmdletBinding()] param([Parameter(Mandatory)][string]$ShortName) $assemblySimpleName = if ($ShortName -like 'WinForms.*') { 'Microsoft.Web.WebView2.WinForms' } else { 'Microsoft.Web.WebView2.Core' } $fullTypeName = "Microsoft.Web.WebView2.$ShortName" $asm = Get-SRxLoadedAssembly $assemblySimpleName if ($asm) { $t = $asm.GetType($fullTypeName) if ($t) { return $t } } try { return [type]::GetType("$fullTypeName, $assemblySimpleName") } catch { return $null } } class SRxTerminalBoxControl : System.Windows.Forms.UserControl { [string]$SourcePath = "" [hashtable]$Icons = @{} [System.Management.Automation.ScriptBlock]$OnStatus = $null [System.Management.Automation.ScriptBlock]$OnClose = $null [string]$StartupCommandLine = "powershell.exe -NoLogo -NoExit" [object]$Editors = $null [string]$EditorTheme = 'vs-dark' [bool]$EnsureVSCode = $true [string]$VSCodeBuildEdition = 'Stable-User' [bool]$DisableEditorSurface = $false [System.Management.Automation.ScriptBlock]$OnRequestShow = $null [System.Management.Automation.ScriptBlock]$OnDebugCommand = $null [System.Management.Automation.ScriptBlock]$OnEditorOpened = $null [System.Management.Automation.ScriptBlock]$OnEditorClosed = $null [System.Management.Automation.ScriptBlock]$OnDebugConsoleInput = $null hidden $_headerPanel = $null hidden $_titleLabel = $null hidden $_closeButton = $null hidden $_placeholder = $null hidden $_webView = $null hidden $_conPty = $null hidden $_startupTimer = $null hidden $_ioTimer = $null hidden $_pendingCommands = $null hidden [bool]$_started = $false hidden [bool]$_webViewReady = $false hidden [int]$_lastCols = 80 hidden [int]$_lastRows = 24 hidden $_contentPanel = $null hidden $_terminalPanel = $null hidden $_editorPanel = $null hidden $_editorWebView = $null hidden $_editorPlaceholder = $null hidden $_btnSave = $null hidden $_tabs = $null hidden [string]$_activeView = 'terminal' hidden [bool]$_editorStarted = $false hidden [bool]$_editorJsReady = $false hidden $_editorOutbox = $null hidden $_overlay = $null hidden $_overlayMsg = $null hidden $_overlaySave = $null hidden $_overlayDiscard = $null hidden $_overlayCancel = $null hidden $_promptEditor = $null hidden $_environment = $null hidden $_envTask = $null hidden [string]$_envError = '' hidden $_envWaiters = $null hidden [bool]$_vscodeChecked = $false hidden $_vscodeInstall = $null hidden $_vscodeTimer = $null hidden $_tabToolTip = $null hidden $_crossBitmap = $null hidden $_dbgBitmaps = $null hidden $_debugLabel = $null hidden $_debugPanel = $null hidden $_debugWebView = $null hidden $_debugPlaceholder = $null hidden $_debugOutbox = $null hidden [bool]$_debugStarted = $false hidden [bool]$_debugJsReady = $false hidden [bool]$_debugConsoleOn = $false hidden [int]$_debugCols = 120 SRxTerminalBoxControl() : base() { $this._pendingCommands = New-Object 'System.Collections.Generic.List[string]' $this._editorOutbox = New-Object 'System.Collections.Generic.List[string]' $this._envWaiters = New-Object 'System.Collections.Generic.List[string]' $this._debugOutbox = New-Object 'System.Collections.Generic.List[string]' $this._tabs = @{} $this.Editors = New-SRxMonacoEditorCollection -Owner $this $this.buildLayout() } hidden [void] buildLayout() { $this.Dock = [System.Windows.Forms.DockStyle]::Fill $this._headerPanel = [System.Windows.Forms.ToolStrip]::new() $this._headerPanel.Dock = [System.Windows.Forms.DockStyle]::Top $this._headerPanel.GripStyle = [System.Windows.Forms.ToolStripGripStyle]::Hidden $this._headerPanel.BackColor = [System.Drawing.SystemColors]::ControlLight $this._headerPanel.ImageScalingSize = [System.Drawing.Size]::new(20, 20) $this._headerPanel.AutoSize = $false $this._headerPanel.Height = 35 $this._btnSave = [System.Windows.Forms.ToolStripButton]::new() $this._btnSave.Name = '_btnSave' $this._btnSave.DisplayStyle = [System.Windows.Forms.ToolStripItemDisplayStyle]::Image $this._btnSave.Alignment = [System.Windows.Forms.ToolStripItemAlignment]::Left $this._btnSave.Size = [System.Drawing.Size]::new(29, 24) $this._btnSave.ImageScaling = [System.Windows.Forms.ToolStripItemImageScaling]::SizeToFit $this._btnSave.AutoToolTip = $false $this._btnSave.ToolTipText = '' $this._btnSave.Enabled = $false $this._btnSave.Visible = $false $this._btnSave.Tag = $this $this._btnSave.add_Click({ param($s, $e) $s.Tag.onSaveButtonClick() }) $this._btnSave.add_MouseEnter({ param($s, $e) $s.Tag.onSaveButtonMouseEnter() }) $this._btnSave.add_MouseLeave({ param($s, $e) $s.Tag.onSaveButtonMouseLeave() }) $this._titleLabel = [System.Windows.Forms.ToolStripLabel]::new('Terminal') $this._titleLabel.Tag = $this $this._titleLabel.add_Click({ param($s, $e) $s.Tag.ShowTerminal() }) $this._closeButton = [System.Windows.Forms.ToolStripButton]::new() $this._closeButton.Alignment = [System.Windows.Forms.ToolStripItemAlignment]::Right $this._closeButton.DisplayStyle = [System.Windows.Forms.ToolStripItemDisplayStyle]::Image $this._closeButton.Size = [System.Drawing.Size]::new(29, 24) $this._closeButton.ImageScaling = [System.Windows.Forms.ToolStripItemImageScaling]::SizeToFit $this._closeButton.AutoToolTip = $false $this._closeButton.Tag = $this $this._closeButton.add_Click({ param($source, $e) $_this = $source.Tag if ($_this.OnClose) { $_this.OnClose.Invoke() } }) [void]$this._headerPanel.Items.Add($this._btnSave) [void]$this._headerPanel.Items.Add($this._titleLabel) $this._debugLabel = [System.Windows.Forms.ToolStripLabel]::new('Debug Console') $this._debugLabel.Visible = $false $this._debugLabel.Tag = $this $this._debugLabel.add_Click({ param($s, $e) $s.Tag.SelectDebugConsole() }) [void]$this._headerPanel.Items.Add($this._debugLabel) [void]$this._headerPanel.Items.Add($this._closeButton) $this._placeholder = [System.Windows.Forms.Label]::new() $this._placeholder.Dock = [System.Windows.Forms.DockStyle]::Fill $this._placeholder.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter $this._placeholder.Text = "Terminal is starting..." $this._placeholder.Visible = $false $this._contentPanel = [System.Windows.Forms.Panel]::new() $this._contentPanel.Dock = [System.Windows.Forms.DockStyle]::Fill $this._terminalPanel = [System.Windows.Forms.Panel]::new() $this._terminalPanel.Dock = [System.Windows.Forms.DockStyle]::Fill [void]$this._terminalPanel.Controls.Add($this._placeholder) $this._editorPanel = [System.Windows.Forms.Panel]::new() $this._editorPanel.Dock = [System.Windows.Forms.DockStyle]::Fill $this._editorPanel.BackColor = [System.Drawing.Color]::FromArgb(30, 30, 30) $this._editorPanel.Visible = $false $this._editorPlaceholder = [System.Windows.Forms.Label]::new() $this._editorPlaceholder.Dock = [System.Windows.Forms.DockStyle]::Fill $this._editorPlaceholder.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter $this._editorPlaceholder.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 200) $this._editorPlaceholder.Text = "Editor is starting..." $this._editorPlaceholder.Visible = $false [void]$this._editorPanel.Controls.Add($this._editorPlaceholder) $this.buildOverlay() $this._debugPanel = [System.Windows.Forms.Panel]::new() $this._debugPanel.Dock = [System.Windows.Forms.DockStyle]::Fill $this._debugPanel.BackColor = [System.Drawing.Color]::FromArgb(30, 30, 30) $this._debugPanel.Visible = $false $this._debugPlaceholder = [System.Windows.Forms.Label]::new() $this._debugPlaceholder.Dock = [System.Windows.Forms.DockStyle]::Fill $this._debugPlaceholder.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter $this._debugPlaceholder.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 200) $this._debugPlaceholder.Text = "Debug Console is starting..." $this._debugPlaceholder.Visible = $false [void]$this._debugPanel.Controls.Add($this._debugPlaceholder) [void]$this._contentPanel.Controls.Add($this._debugPanel) [void]$this._contentPanel.Controls.Add($this._editorPanel) [void]$this._contentPanel.Controls.Add($this._terminalPanel) [void]$this.Controls.Add($this._contentPanel) [void]$this.Controls.Add($this._headerPanel) } [void] ApplyIcons() { try { if ($this.Icons.ContainsKey('Close') -and $this.Icons['Close']) { $this._closeButton.Image = $this.Icons['Close'] } if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._btnSave.Image = $this.Icons['Save'] $this._overlaySave.Image = $this.Icons['Save'] } foreach ($t in @($this._tabs.Values)) { $this.applyTabIcon($t) } $this.updateSaveButton() } catch { Write-Host "SRxTerminalBoxControl.ApplyIcons - $($_.Exception.Message)" } } hidden [void] status([string]$text) { try { if ($this.OnStatus) { $this.OnStatus.Invoke($text) } } catch { } } [void] Activate() { if ($this._started) { try { if ($this._webView) { $this._webView.Focus() } } catch { } return } $this._started = $true $this.status("Starting terminal...") if (-not (Test-SRxConPtySupported)) { $this.showPlaceholder("Terminal unavailable - this Windows build does not support ConPTY (needs 1809+).") $this.status("Terminal unavailable (ConPTY unsupported).") return } if (-not (Import-SRxWebView2Assemblies)) { $this.showPlaceholder("Terminal unavailable - WebView2 SDK assemblies are not deployed. See Terminal\Lib\WebView2.") $this.status("Terminal unavailable (WebView2 SDK missing).") return } try { $this._webView = New-Object -TypeName 'Microsoft.Web.WebView2.WinForms.WebView2' $this._webView.Dock = [System.Windows.Forms.DockStyle]::Fill [void]$this._terminalPanel.Controls.Add($this._webView) $this._webView.BringToFront() $this.acquireEnvironment('terminal') } catch { $this.showPlaceholder("Terminal failed to start: $($_.Exception.Message)") $this.status("Terminal start failed: $($_.Exception.Message)") } } hidden [void] clearStartupTimer() { $this._startupTimer = $null } hidden [void] acquireEnvironment([string]$consumer) { if ($this._environment) { $this.dispatchEnvironment($consumer); return } if ($this._envError) { $this.dispatchEnvironmentFailure($consumer, $this._envError); return } if (-not $this._envWaiters.Contains($consumer)) { $this._envWaiters.Add($consumer) } if ($this._envTask) { return } try { $userDataFolder = Join-Path $(if ($this.SourcePath) { $this.SourcePath } else { $env:TEMP }) 'WebView2UserData' if (-not (Test-Path $userDataFolder)) { New-Item -ItemType Directory -Path $userDataFolder -Force | Out-Null } $envType = Get-SRxWebView2Type 'Core.CoreWebView2Environment' if (-not $envType) { $this.failEnvironment("CoreWebView2Environment type not found after loading WebView2 SDK.") return } $this._envTask = $envType::CreateAsync($null, $userDataFolder, $null) $this._startupTimer = New-Object System.Windows.Forms.Timer $this._startupTimer.Interval = 50 $this._startupTimer.Tag = $this $this._startupTimer.add_Tick({ param($source, $e) $ctrl = $source.Tag $t = $ctrl._envTask if ($null -eq $t -or -not $t.IsCompleted) { return } $source.Stop() $source.Dispose() $ctrl.clearStartupTimer() if ($t.IsFaulted -or -not $t.Result) { $detail = "" try { if ($t.Exception) { $detail = " ($($t.Exception.GetBaseException().Message))" } } catch { } $ctrl.failEnvironment("WebView2 runtime not found or failed to initialize$detail. Install the Evergreen WebView2 Runtime.") return } $ctrl.environmentReady($t.Result) }) $this._startupTimer.Start() } catch { $this.failEnvironment("WebView2 environment creation failed: $($_.Exception.Message)") } } hidden [void] environmentReady($environment) { $this._environment = $environment $waiters = @($this._envWaiters.ToArray()) $this._envWaiters.Clear() foreach ($w in $waiters) { $this.dispatchEnvironment($w) } } hidden [void] failEnvironment([string]$reason) { $this._envError = $reason $waiters = @($this._envWaiters.ToArray()) $this._envWaiters.Clear() foreach ($w in $waiters) { $this.dispatchEnvironmentFailure($w, $reason) } } hidden [void] dispatchEnvironment([string]$consumer) { if ($consumer -eq 'terminal') { $this.onEnvironmentReady($this._environment) } elseif ($consumer -eq 'editor') { $this.onEditorEnvironmentReady($this._environment) } elseif ($consumer -eq 'debug') { $this.onDebugEnvironmentReady($this._environment) } } hidden [void] dispatchEnvironmentFailure([string]$consumer, [string]$reason) { if ($consumer -eq 'terminal') { $this.showPlaceholder("Terminal unavailable - $reason") $this.status("Terminal unavailable.") } elseif ($consumer -eq 'editor') { $this.showEditorPlaceholder("Editor unavailable - $reason") $this.status("Editor unavailable.") } elseif ($consumer -eq 'debug') { $this.showDebugPlaceholder("Debug Console unavailable - $reason") } } hidden [void] onEnvironmentReady($environment) { try { $this._webView.add_CoreWebView2InitializationCompleted({ param($source, $e) $ctrl = $source.Tag if (-not $e.IsSuccess) { $ctrl.showPlaceholder("Terminal unavailable - CoreWebView2 initialization failed: $($e.InitializationException.Message)") return } $ctrl.wireCoreWebView2() }) $this._webView.Tag = $this $this._webView.EnsureCoreWebView2Async($environment) | Out-Null } catch { $this.showPlaceholder("Terminal failed to initialize: $($_.Exception.Message)") } } hidden [void] wireCoreWebView2() { try { $core = $this._webView.CoreWebView2 $wwwPath = Join-Path $PSScriptRoot 'www' $kindType = Get-SRxWebView2Type 'Core.CoreWebView2HostResourceAccessKind' $allowKind = if ($kindType) { [Enum]::Parse($kindType, 'Allow') } else { 1 } $core.SetVirtualHostNameToFolderMapping('srx-terminal.local', $wwwPath, $allowKind) $control = $this $onWebMessage = { param($source, $e) try { $control.onWebMessage($e.TryGetWebMessageAsString()) } catch { Write-Host "onWebMessage - $($_.Exception.Message)" } }.GetNewClosure() $core.add_WebMessageReceived($onWebMessage) $this._webViewReady = $true $core.Navigate('https://srx-terminal.local/terminal.html') $this.status("Terminal ready.") } catch { $this.showPlaceholder("Terminal failed to wire CoreWebView2: $($_.Exception.Message)") } } hidden [void] onWebMessage([string]$json) { if ([string]::IsNullOrEmpty($json)) { return } $msg = $null try { $msg = $json | ConvertFrom-Json -ErrorAction Stop } catch { return } switch ([string]$msg.t) { 'ready' { $this.startConPty($this._lastCols, $this._lastRows) } 'in' { if ($this._conPty -and $msg.d) { $this._conPty.Write([string]$msg.d) } } 'resize' { $cols = [int]$msg.cols; $rows = [int]$msg.rows if ($cols -gt 0 -and $rows -gt 0) { $this._lastCols = $cols; $this._lastRows = $rows if ($this._conPty) { $this._conPty.Resize($cols, $rows) } } } default { } } } hidden [void] startConPty([int]$cols, [int]$rows) { if ($this._conPty) { return } try { $workDir = if ($this.SourcePath) { $this.SourcePath } else { $null } $this._conPty = Start-SRxConPtySession -CommandLine $this.StartupCommandLine -WorkingDirectory $workDir -Columns $cols -Rows $rows if (-not $this._conPty) { $this.status("Terminal: failed to start ConPTY session.") return } $this.status("Terminal session started (PID $($this._conPty.ProcessId)).") $this.startIoPump() if ($this._pendingCommands -and $this._pendingCommands.Count -gt 0) { foreach ($pending in $this._pendingCommands) { $this._conPty.Write($pending + "`r") } $this._pendingCommands.Clear() } } catch { $this.status("Terminal: ConPTY start failed: $($_.Exception.Message)") } } [void] RunCommand([string]$text) { if ([string]::IsNullOrEmpty($text)) { return } if ($this._activeView -ne 'terminal') { $this.switchView('terminal') } $this.Activate() if ($this._conPty) { $this._conPty.Write($text + "`r") } else { if (-not $this._pendingCommands) { $this._pendingCommands = New-Object 'System.Collections.Generic.List[string]' } $this._pendingCommands.Add($text) } } [int] GetProcessId() { if ($this._conPty) { return $this._conPty.ProcessId } return 0 } [void] RestartSession() { if (-not $this._started) { return } $hadExistingSession = ($null -ne $this._conPty) try { if ($this._ioTimer) { $this._ioTimer.Stop(); $this._ioTimer.Dispose() } } catch { } $this._ioTimer = $null try { if ($this._conPty) { $this._conPty.Stop(1500) } } catch { } $this._conPty = $null try { if ($this._pendingCommands) { $this._pendingCommands.Clear() } } catch { } if (-not $hadExistingSession) { $this.status("Terminal settings updated - will apply once the session starts.") return } $this.status("Restarting terminal session...") try { if ($this._webView -and $this._webView.CoreWebView2) { $payload = @{ t = 'clear' } | ConvertTo-Json -Compress $this._webView.CoreWebView2.PostWebMessageAsJson($payload) } } catch { } $this.startConPty($this._lastCols, $this._lastRows) } hidden [void] pumpConPtyIo() { if (-not $this._conPty) { return } try { $chunk = $null $budget = 64 while ($budget -gt 0 -and $this._conPty.TryDequeueOutput([ref]$chunk)) { $budget-- if ($chunk -and $this._webView -and $this._webView.CoreWebView2) { $ints = New-Object 'System.Collections.Generic.List[int]' foreach ($b in $chunk) { [void]$ints.Add([int]$b) } $payload = @{ t = 'out'; d = $ints } | ConvertTo-Json -Compress -Depth 4 $this._webView.CoreWebView2.PostWebMessageAsJson($payload) } } if ($this._conPty.HasExited) { $code = $this._conPty.LastExitCode $this.status("Terminal session ended (exit code $code).") try { if ($this._webView -and $this._webView.CoreWebView2) { $payload = @{ t = 'exit'; code = $code } | ConvertTo-Json -Compress $this._webView.CoreWebView2.PostWebMessageAsJson($payload) } } catch { } $this._conPty = $null if ($this._ioTimer) { $this._ioTimer.Stop(); $this._ioTimer.Dispose(); $this._ioTimer = $null } } } catch { Write-Host "pumpConPtyIo - $($_.Exception.Message)" } } hidden [void] startIoPump() { if ($this._ioTimer) { return } $this._ioTimer = New-Object System.Windows.Forms.Timer $this._ioTimer.Interval = 30 $this._ioTimer.Tag = $this $this._ioTimer.add_Tick({ param($source, $e) $ctrl = $source.Tag $ctrl.pumpConPtyIo() }) $this._ioTimer.Start() } hidden [void] showPlaceholder([string]$text) { try { if ($this._webView) { $this._webView.Visible = $false } $this._placeholder.Text = $text $this._placeholder.Visible = $true $this._placeholder.BringToFront() } catch { } } [object] OpenEditor([string]$path) { return $this.Editors.Open($path) } [object] OpenEditor([string]$path, [bool]$readOnly) { return $this.Editors.Open($path, $readOnly) } [void] ShowTerminal() { $this.switchView('terminal') $this.Activate() } [bool] HasUnsavedEditors() { return $this.Editors.HasUnsavedChanges() } [string] ActiveView() { return $this._activeView } hidden [void] editorOpened($editor, [string]$text) { $this.ensureEditorSurface() $this.addTab($editor) $this.postEditor(@{ t = 'open' id = [string]$editor.Id path = [string]$editor.Path name = [string]$editor.FileName lang = [string]$editor.Language text = $text eol = [string]$editor.NewLine ro = [bool]$editor.IsReadOnly }) $this.raiseEditorEvent($this.OnEditorOpened, $editor) } hidden [void] raiseEditorEvent([System.Management.Automation.ScriptBlock]$handler, $editor) { if ($null -eq $handler) { return } try { $handler.Invoke($editor) } catch { Write-Host "SRxTerminalBoxControl editor event - $($_.Exception.Message)" } } hidden [void] editorReadOnlyChanged($editor) { $this.postEditor(@{ t = 'readonly'; id = [string]$editor.Id; ro = [bool]$editor.IsReadOnly }) $this.updateTab($editor) $this.updateSaveButton() } hidden [void] editorSelected($editor, [bool]$show) { if ($null -eq $editor) { if ($show) { $hv = $this.homeView() $this.switchView($hv) if ($hv -eq 'terminal' -and $this._started) { $this.Activate() } } $this.refreshTabs() return } if ($show) { $this.requestShow() $this.postEditor(@{ t = 'select'; id = [string]$editor.Id }) $this.switchView([string]$editor.Id) } else { $this.refreshTabs() } } hidden [bool] editorClosed($editor) { $wasShown = ($this._activeView -eq [string]$editor.Id) if ($null -ne $this._promptEditor -and $this._promptEditor -eq $editor) { $this.hideOverlay() } $this.removeTab($editor) $this.postEditor(@{ t = 'close'; id = [string]$editor.Id }) if ($wasShown) { $this._activeView = '' } $this.updateSaveButton() $this.raiseEditorEvent($this.OnEditorClosed, $editor) return $wasShown } hidden [void] editorStateChanged($editor) { $this.updateTab($editor) $this.updateSaveButton() } hidden [void] promptUnsaved($editor) { $this.showOverlay($editor) } hidden [void] requestContent($editor, [string]$requestId) { $this.postEditor(@{ t = 'get'; id = [string]$editor.Id; req = $requestId }) } hidden [void] markSaved($editor, [int]$version) { $this.postEditor(@{ t = 'saved'; id = [string]$editor.Id; ver = $version }) } hidden [void] ensureEditorSurface() { if ($this._editorStarted) { return } $this._editorStarted = $true $this.checkVSCode() if ($this.DisableEditorSurface) { return } if (-not (Import-SRxWebView2Assemblies)) { $this.showEditorPlaceholder("Editor unavailable - WebView2 SDK assemblies are not deployed. See Terminal\Lib\WebView2.") $this.status("Editor unavailable (WebView2 SDK missing).") return } try { $this._editorWebView = New-Object -TypeName 'Microsoft.Web.WebView2.WinForms.WebView2' $this._editorWebView.Dock = [System.Windows.Forms.DockStyle]::Fill [void]$this._editorPanel.Controls.Add($this._editorWebView) $this._editorWebView.BringToFront() $this.showEditorPlaceholder("Editor is starting...") $this.acquireEnvironment('editor') } catch { $this.showEditorPlaceholder("Editor failed to start: $($_.Exception.Message)") $this.status("Editor start failed: $($_.Exception.Message)") } } hidden [void] onEditorEnvironmentReady($environment) { if ($null -eq $this._editorWebView) { return } try { $this._editorWebView.add_CoreWebView2InitializationCompleted({ param($source, $e) $ctrl = $source.Tag if (-not $e.IsSuccess) { $ctrl.showEditorPlaceholder("Editor unavailable - CoreWebView2 initialization failed: $($e.InitializationException.Message)") return } $ctrl.wireEditorCoreWebView2() }) $this._editorWebView.Tag = $this $this._editorWebView.EnsureCoreWebView2Async($environment) | Out-Null } catch { $this.showEditorPlaceholder("Editor failed to initialize: $($_.Exception.Message)") } } hidden [void] wireEditorCoreWebView2() { try { $core = $this._editorWebView.CoreWebView2 $wwwPath = Join-Path $PSScriptRoot 'www' $kindType = Get-SRxWebView2Type 'Core.CoreWebView2HostResourceAccessKind' $allowKind = if ($kindType) { [Enum]::Parse($kindType, 'Allow') } else { 1 } $core.SetVirtualHostNameToFolderMapping('srx-terminal.local', $wwwPath, $allowKind) $control = $this $onMessage = { param($source, $e) try { $control.onEditorWebMessage($e.TryGetWebMessageAsString()) } catch { Write-Host "onEditorWebMessage - $($_.Exception.Message)" } }.GetNewClosure() $core.add_WebMessageReceived($onMessage) try { $core.Settings.AreBrowserAcceleratorKeysEnabled = $false } catch { } $theme = [System.Uri]::EscapeDataString($this.EditorTheme) $core.Navigate("https://srx-terminal.local/editor.html?theme=$theme") } catch { $this.showEditorPlaceholder("Editor failed to wire CoreWebView2: $($_.Exception.Message)") } } hidden [void] postEditor([hashtable]$message) { $json = $null try { $json = ConvertTo-SRxJsonMessage -Message $message } catch { Write-Host "postEditor - serialize failed: $($_.Exception.Message)"; return } if ($this._editorJsReady -and $this._editorWebView -and $this._editorWebView.CoreWebView2) { try { $this._editorWebView.CoreWebView2.PostWebMessageAsJson($json) } catch { Write-Host "postEditor - $($_.Exception.Message)" } } else { $this._editorOutbox.Add($json) } } hidden [void] flushEditorOutbox() { if (-not ($this._editorWebView -and $this._editorWebView.CoreWebView2)) { return } $queued = $this._editorOutbox.ToArray() $this._editorOutbox.Clear() foreach ($json in $queued) { try { $this._editorWebView.CoreWebView2.PostWebMessageAsJson($json) } catch { Write-Host "flushEditorOutbox - $($_.Exception.Message)" } } } hidden [object] msgValue($message, [string]$key) { if ($null -ne $message -and $message.ContainsKey($key)) { return $message[$key] } return $null } hidden [void] onEditorWebMessage([string]$json) { $msg = ConvertFrom-SRxJsonMessage -Json $json if ($null -eq $msg) { return } $t = [string]$this.msgValue($msg, 't') $id = [string]$this.msgValue($msg, 'id') switch ($t) { 'ready' { $this._editorJsReady = $true $this._editorPlaceholder.Visible = $false if ($this._editorWebView) { $this._editorWebView.Visible = $true } $this.flushEditorOutbox() $this.status("Editor ready (Monaco: $([string]$this.msgValue($msg, 'source'))).") } 'dirty' { $this.Editors.SetDirty($id, [bool]$this.msgValue($msg, 'dirty')) } 'content' { $ver = 0 try { $ver = [int]$this.msgValue($msg, 'ver') } catch { $ver = 0 } $this.Editors.CompleteContentRequest([string]$this.msgValue($msg, 'req'), [string]$this.msgValue($msg, 'd'), $ver) } 'contentError' { $this.Editors.FailContentRequest([string]$this.msgValue($msg, 'req'), [string]$this.msgValue($msg, 'msg')) } 'save' { $ed = $this.Editors.GetById($id) if ($null -ne $ed -and -not $ed.IsReadOnly) { [void]$ed.Save() } } 'dbg' { $ed = $this.Editors.GetById($id) $cmd = [string]$this.msgValue($msg, 'cmd') $arg = $this.msgValue($msg, 'line') if ($cmd -eq 'bpsync') { $arg = @($this.msgValue($msg, 'lines')) } if ($null -ne $ed) { $this.raiseDebugCommand($ed, $cmd, $arg) } } 'error' { $text = [string]$this.msgValue($msg, 'msg') $this.showEditorPlaceholder("Editor unavailable - $text") $this.status("Editor error: $text") } default { } } } hidden [void] showEditorPlaceholder([string]$text) { try { if ($this._editorWebView) { $this._editorWebView.Visible = $false } $this._editorPlaceholder.Text = $text $this._editorPlaceholder.Visible = $true $this._editorPlaceholder.BringToFront() if ($this._overlay -and $this._overlay.Visible) { $this._overlay.BringToFront() } } catch { } } hidden [void] switchView([string]$view) { if ([string]::IsNullOrEmpty($view)) { $view = 'terminal' } $this._activeView = $view try { if ($view -eq 'terminal') { $this._editorPanel.Visible = $false $this._debugPanel.Visible = $false $this._terminalPanel.Visible = $true $this._terminalPanel.BringToFront() } elseif ($view -eq 'debug') { $this._editorPanel.Visible = $false $this._terminalPanel.Visible = $false $this._debugPanel.Visible = $true $this._debugPanel.BringToFront() if ($this._debugWebView -and $this._debugWebView.Visible) { try { $this._debugWebView.Focus() | Out-Null } catch { } } $this.postDebug(@{ t = 'focus' }) } else { $this._terminalPanel.Visible = $false $this._debugPanel.Visible = $false $this._editorPanel.Visible = $true $this._editorPanel.BringToFront() if ($this._editorWebView -and $this._editorWebView.Visible) { try { $this._editorWebView.Focus() | Out-Null } catch { } } } } catch { Write-Host "switchView - $($_.Exception.Message)" } $this.refreshTabs() $this.updateSaveButton() } hidden [void] requestShow() { try { if ($this.OnRequestShow) { $this.OnRequestShow.Invoke() } } catch { } } hidden [object] displayedEditor() { if ($this._activeView -eq 'terminal' -or $this._activeView -eq 'debug' -or [string]::IsNullOrEmpty($this._activeView)) { return $null } return $this.Editors.GetById($this._activeView) } hidden [void] addTab($editor) { try { $id = [string]$editor.Id if ($this._tabs.ContainsKey($id)) { return } $tagObj = [pscustomobject]@{ Control = $this; Editor = $editor } $font = $this.tabFont() $labelMargin = $this._titleLabel.Margin $tabHeight = $this.tabHeight() $panel = [System.Windows.Forms.Panel]::new() $panel.Height = $tabHeight $panel.Font = $font $panel.Margin = [System.Windows.Forms.Padding]::new(0) $panel.BackColor = [System.Drawing.SystemColors]::ControlLight $panel.Cursor = [System.Windows.Forms.Cursors]::Hand $panel.Tag = $tagObj $close = [System.Windows.Forms.PictureBox]::new() $close.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom $close.Size = [System.Drawing.Size]::new(14, 14) $close.Location = [System.Drawing.Point]::new(6, [int][Math]::Floor(($tabHeight - 14) / 2)) $close.BackColor = [System.Drawing.Color]::Transparent $close.Cursor = [System.Windows.Forms.Cursors]::Hand $close.Tag = $tagObj $label = [System.Windows.Forms.Label]::new() $label.AutoSize = $false $label.Location = [System.Drawing.Point]::new(24, 0) $label.Height = $tabHeight $label.Padding = [System.Windows.Forms.Padding]::new(0) $label.Margin = [System.Windows.Forms.Padding]::new(0) $label.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $label.UseCompatibleTextRendering = $false $label.UseMnemonic = $false $label.Font = $font $label.BackColor = [System.Drawing.Color]::Transparent $label.Cursor = [System.Windows.Forms.Cursors]::Hand $label.Text = $editor.DisplayTitle() $label.Tag = $tagObj [void]$panel.Controls.Add($close) [void]$panel.Controls.Add($label) $close.add_Click({ param($s, $e) $s.Tag.Control.onTabCloseClick($s.Tag.Editor) }) $close.add_MouseEnter({ param($s, $e) $s.BackColor = [System.Drawing.Color]::FromArgb(205, 220, 235) }) $close.add_MouseLeave({ param($s, $e) $s.BackColor = [System.Drawing.Color]::Transparent }) $label.add_Click({ param($s, $e) $s.Tag.Control.onTabClick($s.Tag.Editor) }) $panel.add_Click({ param($s, $e) $s.Tag.Control.onTabClick($s.Tag.Editor) }) $label.add_MouseUp({ param($s, $e) if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Middle) { $s.Tag.Control.onTabCloseClick($s.Tag.Editor) } }) $panel.add_MouseUp({ param($s, $e) if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Middle) { $s.Tag.Control.onTabCloseClick($s.Tag.Editor) } }) $tabHost = [System.Windows.Forms.ToolStripControlHost]::new($panel) $tabHost.AutoSize = $false $tabHost.Margin = [System.Windows.Forms.Padding]::new(2, $labelMargin.Top, 0, $labelMargin.Bottom) $tabHost.Name = "tab_$id" if ($null -eq $this._tabToolTip) { $this._tabToolTip = [System.Windows.Forms.ToolTip]::new() } $this._tabToolTip.SetToolTip($label, $this.tabToolTipText($editor)) $this._tabToolTip.SetToolTip($panel, $this.tabToolTipText($editor)) $this._tabToolTip.SetToolTip($close, 'Close') $tab = @{ Host = $tabHost; Panel = $panel; Close = $close; Label = $label; Editor = $editor } $this._tabs[$id] = $tab $this.applyTabIcon($tab) $this.sizeTab($tab) $idx = $this._headerPanel.Items.IndexOf($this._closeButton) if ($idx -lt 0) { [void]$this._headerPanel.Items.Add($tabHost) } else { $this._headerPanel.Items.Insert($idx, $tabHost) } } catch { Write-Host "addTab - $($_.Exception.Message)" } } hidden [void] sizeTab($tab) { $flags = [System.Windows.Forms.TextFormatFlags]::NoPrefix -bor [System.Windows.Forms.TextFormatFlags]::SingleLine $w = [System.Windows.Forms.TextRenderer]::MeasureText([string]$tab.Label.Text, $tab.Label.Font, [System.Drawing.Size]::new([int]::MaxValue, [int]::MaxValue), $flags).Width $h = $this.tabHeight() $tab.Label.Size = [System.Drawing.Size]::new($w + 2, $h) $extra = 0 $dbg = $tab['Dbg'] if ($null -ne $dbg -and $dbg.Count -gt 0) { $x = 24 + $w + 2 + 4 $y = [int][Math]::Floor(($h - 16) / 2) foreach ($b in $dbg) { $b.Location = [System.Drawing.Point]::new($x, $y) $x += 19 } $extra = 4 + 19 * $dbg.Count } $tab.Panel.Size = [System.Drawing.Size]::new(24 + $w + 2 + 6 + $extra, $h) $tab.Close.Top = [int][Math]::Floor(($h - $tab.Close.Height) / 2) $tab.Host.Size = $tab.Panel.Size } hidden [object[]] debugButtonDefs() { return @( @{ Cmd = 'continue'; Icon = 'DbgContinue'; Tip = 'Continue (F5)'; Step = $true } @{ Cmd = 'stepOver'; Icon = 'DbgStepOver'; Tip = 'Step Over (F10)'; Step = $true } @{ Cmd = 'stepInto'; Icon = 'DbgStepInto'; Tip = 'Step Into (F11)'; Step = $true } @{ Cmd = 'stepOut'; Icon = 'DbgStepOut'; Tip = 'Step Out (Shift+F11)'; Step = $true } @{ Cmd = 'stop'; Icon = 'DbgStop'; Tip = 'Stop Debugger (Shift+F5)'; Step = $false } ) } [void] SetEditorDebugControls([string]$editorId, [bool]$visible, [bool]$stopped) { try { if (-not $this._tabs.ContainsKey($editorId)) { return } $tab = $this._tabs[$editorId] $dbg = $tab['Dbg'] if (-not $visible) { if ($null -ne $dbg) { foreach ($b in @($dbg)) { try { [void]$tab.Panel.Controls.Remove($b); $b.Dispose() } catch { } } $tab.Remove('Dbg') $this.sizeTab($tab) } return } if ($null -eq $dbg) { $dbg = New-Object System.Collections.ArrayList if ($null -eq $this._tabToolTip) { $this._tabToolTip = [System.Windows.Forms.ToolTip]::new() } foreach ($def in $this.debugButtonDefs()) { $pb = [System.Windows.Forms.PictureBox]::new() $pb.Name = 'dbg_' + [string]$def.Cmd $pb.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom $pb.Size = [System.Drawing.Size]::new(16, 16) $pb.BackColor = [System.Drawing.Color]::Transparent $pb.Cursor = [System.Windows.Forms.Cursors]::Hand $pb.Tag = [pscustomobject]@{ Control = $this; Editor = $tab.Editor; Cmd = [string]$def.Cmd; Def = $def } $pb.add_Click({ param($s, $e) if ($s.Enabled) { $s.Tag.Control.raiseDebugCommand($s.Tag.Editor, [string]$s.Tag.Cmd, $null) } }) $pb.add_MouseEnter({ param($s, $e) if ($s.Enabled) { $s.BackColor = [System.Drawing.Color]::FromArgb(205, 220, 235) } }) $pb.add_MouseLeave({ param($s, $e) $s.BackColor = [System.Drawing.Color]::Transparent }) $this._tabToolTip.SetToolTip($pb, [string]$def.Tip) [void]$tab.Panel.Controls.Add($pb) [void]$dbg.Add($pb) } $tab['Dbg'] = $dbg } foreach ($b in @($dbg)) { $def = $b.Tag.Def $on = (-not [bool]$def.Step) -or $stopped $b.Enabled = $on $b.Image = $this.debugBitmap([string]$def.Cmd, [string]$def.Icon, $on) if (-not $on) { $b.BackColor = [System.Drawing.Color]::Transparent } } $tab['DbgStopped'] = $stopped $this.sizeTab($tab) } catch { Write-Host "SetEditorDebugControls - $($_.Exception.Message)" } } [void] SetEditorDebugLine([string]$editorId, [int]$line) { $this.postEditor(@{ t = 'dbgline'; id = $editorId; line = $line }) } [void] SetEditorBreakpoints([string]$editorId, [int[]]$lines) { $list = New-Object System.Collections.ArrayList foreach ($l in @($lines)) { if ([int]$l -gt 0) { [void]$list.Add([int]$l) } } $this.postEditor(@{ t = 'bps'; id = $editorId; lines = [object[]]$list.ToArray() }) } [string[]] TabDebugButtons([string]$editorId) { $out = New-Object 'System.Collections.Generic.List[string]' if (-not $this._tabs.ContainsKey($editorId)) { return $out.ToArray() } $dbg = $this._tabs[$editorId]['Dbg'] if ($null -eq $dbg) { return $out.ToArray() } foreach ($b in @($dbg)) { $out.Add(('{0}|{1}|{2}' -f $b.Tag.Cmd, $b.Enabled, $b.Tag.Def.Tip)) } return $out.ToArray() } hidden [void] raiseDebugCommand($editor, [string]$command, $argument) { if ($null -eq $this.OnDebugCommand -or $null -eq $editor -or [string]::IsNullOrEmpty($command)) { return } try { $this.OnDebugCommand.Invoke($editor, $command, $argument) } catch { Write-Host "SRxTerminalBoxControl.OnDebugCommand - $($_.Exception.Message)" } } hidden [object] debugBitmap([string]$cmd, [string]$iconKey, [bool]$enabled) { if ($null -eq $this._dbgBitmaps) { $this._dbgBitmaps = @{} } $key = $cmd + '|' + [string]$enabled if ($this._dbgBitmaps.ContainsKey($key)) { return $this._dbgBitmaps[$key] } $img = $null if ($this.Icons.ContainsKey($iconKey) -and $this.Icons[$iconKey]) { $img = $this.Icons[$iconKey] } else { $img = $this.drawDebugBitmap($cmd) } if (-not $enabled) { try { $img = [System.Windows.Forms.ToolStripRenderer]::CreateDisabledImage($img) } catch { } } $this._dbgBitmaps[$key] = $img return $img } hidden [object] drawDebugBitmap([string]$cmd) { $bmp = [System.Drawing.Bitmap]::new(32, 32) $g = [System.Drawing.Graphics]::FromImage($bmp) $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias $green = [System.Drawing.Color]::FromArgb(56, 138, 52) $blue = [System.Drawing.Color]::FromArgb(30, 111, 200) $red = [System.Drawing.Color]::FromArgb(209, 52, 56) $dark = [System.Drawing.Color]::FromArgb(70, 70, 70) $brush = $null $pen = $null try { switch ($cmd) { 'continue' { $brush = [System.Drawing.SolidBrush]::new($green) $g.FillPolygon($brush, [System.Drawing.Point[]]@([System.Drawing.Point]::new(8, 4), [System.Drawing.Point]::new(28, 16), [System.Drawing.Point]::new(8, 28))) } 'stop' { $brush = [System.Drawing.SolidBrush]::new($red) $g.FillRectangle($brush, 6, 6, 20, 20) } 'stepOver' { $pen = [System.Drawing.Pen]::new($blue, 3.0) $brush = [System.Drawing.SolidBrush]::new($blue) $g.DrawArc($pen, 5, 5, 22, 18, 200, 140) $g.FillPolygon($brush, [System.Drawing.Point[]]@([System.Drawing.Point]::new(21, 10), [System.Drawing.Point]::new(31, 10), [System.Drawing.Point]::new(26, 17))) $brush.Dispose() $brush = [System.Drawing.SolidBrush]::new($dark) $g.FillEllipse($brush, 12, 21, 9, 9) } 'stepInto' { $pen = [System.Drawing.Pen]::new($blue, 3.0) $brush = [System.Drawing.SolidBrush]::new($blue) $g.DrawLine($pen, 16, 2, 16, 13) $g.FillPolygon($brush, [System.Drawing.Point[]]@([System.Drawing.Point]::new(9, 11), [System.Drawing.Point]::new(23, 11), [System.Drawing.Point]::new(16, 19))) $brush.Dispose() $brush = [System.Drawing.SolidBrush]::new($dark) $g.FillEllipse($brush, 12, 21, 9, 9) } 'stepOut' { $pen = [System.Drawing.Pen]::new($blue, 3.0) $brush = [System.Drawing.SolidBrush]::new($blue) $g.DrawLine($pen, 16, 19, 16, 9) $g.FillPolygon($brush, [System.Drawing.Point[]]@([System.Drawing.Point]::new(9, 10), [System.Drawing.Point]::new(23, 10), [System.Drawing.Point]::new(16, 2))) $brush.Dispose() $brush = [System.Drawing.SolidBrush]::new($dark) $g.FillEllipse($brush, 12, 21, 9, 9) } } } finally { if ($null -ne $brush) { $brush.Dispose() } if ($null -ne $pen) { $pen.Dispose() } $g.Dispose() } return $bmp } [void] ShowDebugConsole([string]$title) { try { $this._debugConsoleOn = $true $tip = 'Debug Console' if (-not [string]::IsNullOrEmpty($title)) { $tip = "Debug Console - $title" } $this._debugLabel.ToolTipText = $tip $this._titleLabel.Visible = $false $this._debugLabel.Visible = $true $this.ensureDebugSurface() $this.postDebug(@{ t = 'clear' }) $this.requestShow() $this.switchView('debug') $this._headerPanel.PerformLayout() } catch { Write-Host "ShowDebugConsole - $($_.Exception.Message)" } } [void] SelectDebugConsole() { if (-not $this._debugConsoleOn) { return } $this.requestShow() $this.switchView('debug') } [void] HideDebugConsole() { try { if (-not $this._debugConsoleOn) { return } $this._debugConsoleOn = $false $this._debugLabel.Visible = $false $this._titleLabel.Visible = $true if ($this._activeView -eq 'debug') { $this.switchView('terminal') if ($this._started) { $this.Activate() } } else { $this.refreshTabs() } $this._headerPanel.PerformLayout() } catch { Write-Host "HideDebugConsole - $($_.Exception.Message)" } } [void] DebugConsoleWrite([string]$stream, [string]$text) { $this.DebugConsoleWriteLines(@(@{ s = $stream; d = $text })) } [void] DebugConsoleWriteLines([object[]]$items) { $list = New-Object System.Collections.ArrayList foreach ($it in @($items)) { if ($null -eq $it) { continue } [void]$list.Add(@{ s = [string]$it['s']; d = [string]$it['d'] }) } if ($list.Count -eq 0) { return } $this.postDebug(@{ t = 'lines'; items = [object[]]$list.ToArray() }) } [void] DebugConsoleState([string]$mode, [string]$prompt) { $this.postDebug(@{ t = 'state'; mode = $mode; prompt = $prompt }) } [bool] IsDebugConsoleVisible() { return $this._debugConsoleOn } [int] DebugConsoleColumns() { return $this._debugCols } [string[]] DebugConsoleOutbox() { return $this._debugOutbox.ToArray() } hidden [string] homeView() { if ($this._debugConsoleOn) { return 'debug' } return 'terminal' } hidden [void] ensureDebugSurface() { if ($this._debugStarted) { return } $this._debugStarted = $true if ($this.DisableEditorSurface) { return } if (-not (Import-SRxWebView2Assemblies)) { $this.showDebugPlaceholder("Debug Console unavailable - WebView2 SDK assemblies are not deployed. See Terminal\Lib\WebView2.") return } try { $this._debugWebView = New-Object -TypeName 'Microsoft.Web.WebView2.WinForms.WebView2' $this._debugWebView.Dock = [System.Windows.Forms.DockStyle]::Fill [void]$this._debugPanel.Controls.Add($this._debugWebView) $this._debugWebView.BringToFront() $this.showDebugPlaceholder("Debug Console is starting...") $this.acquireEnvironment('debug') } catch { $this.showDebugPlaceholder("Debug Console failed to start: $($_.Exception.Message)") } } hidden [void] onDebugEnvironmentReady($environment) { if ($null -eq $this._debugWebView) { return } try { $this._debugWebView.add_CoreWebView2InitializationCompleted({ param($source, $e) $ctrl = $source.Tag if (-not $e.IsSuccess) { $ctrl.showDebugPlaceholder("Debug Console unavailable - CoreWebView2 initialization failed: $($e.InitializationException.Message)") return } $ctrl.wireDebugCoreWebView2() }) $this._debugWebView.Tag = $this $this._debugWebView.EnsureCoreWebView2Async($environment) | Out-Null } catch { $this.showDebugPlaceholder("Debug Console failed to initialize: $($_.Exception.Message)") } } hidden [void] wireDebugCoreWebView2() { try { $core = $this._debugWebView.CoreWebView2 $wwwPath = Join-Path $PSScriptRoot 'www' $kindType = Get-SRxWebView2Type 'Core.CoreWebView2HostResourceAccessKind' $allowKind = if ($kindType) { [Enum]::Parse($kindType, 'Allow') } else { 1 } $core.SetVirtualHostNameToFolderMapping('srx-terminal.local', $wwwPath, $allowKind) $control = $this $onMessage = { param($source, $e) try { $control.onDebugWebMessage($e.TryGetWebMessageAsString()) } catch { Write-Host "onDebugWebMessage - $($_.Exception.Message)" } }.GetNewClosure() $core.add_WebMessageReceived($onMessage) try { $core.Settings.AreBrowserAcceleratorKeysEnabled = $false } catch { } $core.Navigate('https://srx-terminal.local/debugconsole.html') } catch { $this.showDebugPlaceholder("Debug Console failed to wire CoreWebView2: $($_.Exception.Message)") } } hidden [void] postDebug([hashtable]$message) { $json = $null try { $json = ConvertTo-SRxJsonMessage -Message $message } catch { Write-Host "postDebug - serialize failed: $($_.Exception.Message)"; return } if ($this._debugJsReady -and $this._debugWebView -and $this._debugWebView.CoreWebView2) { try { $this._debugWebView.CoreWebView2.PostWebMessageAsJson($json) } catch { Write-Host "postDebug - $($_.Exception.Message)" } } else { $this._debugOutbox.Add($json) while ($this._debugOutbox.Count -gt 5000) { $this._debugOutbox.RemoveAt(0) } } } hidden [void] flushDebugOutbox() { if (-not ($this._debugWebView -and $this._debugWebView.CoreWebView2)) { return } $queued = $this._debugOutbox.ToArray() $this._debugOutbox.Clear() foreach ($json in $queued) { try { $this._debugWebView.CoreWebView2.PostWebMessageAsJson($json) } catch { Write-Host "flushDebugOutbox - $($_.Exception.Message)" } } } hidden [void] onDebugWebMessage([string]$json) { $msg = ConvertFrom-SRxJsonMessage -Json $json if ($null -eq $msg) { return } $t = [string]$this.msgValue($msg, 't') switch ($t) { 'ready' { $this._debugJsReady = $true $this._debugPlaceholder.Visible = $false if ($this._debugWebView) { $this._debugWebView.Visible = $true } $this.setDebugCols($this.msgValue($msg, 'cols')) $this.flushDebugOutbox() } 'resize' { $this.setDebugCols($this.msgValue($msg, 'cols')) } 'input' { $this.raiseDebugConsole([string]$this.msgValue($msg, 'd'), [string]$this.msgValue($msg, 'mode')) } 'cancel' { $this.raiseDebugConsole('', 'cancel') } 'key' { $this.raiseDebugConsole([string]$this.msgValue($msg, 'cmd'), 'key') } default { } } } hidden [void] setDebugCols($cols) { $n = 0 try { $n = [int]$cols } catch { $n = 0 } if ($n -ge 20) { $this._debugCols = $n } } hidden [void] raiseDebugConsole([string]$text, [string]$mode) { if ($null -eq $this.OnDebugConsoleInput) { return } try { $this.OnDebugConsoleInput.Invoke($text, $mode) } catch { Write-Host "SRxTerminalBoxControl.OnDebugConsoleInput - $($_.Exception.Message)" } } hidden [void] showDebugPlaceholder([string]$text) { try { if ($this._debugWebView) { $this._debugWebView.Visible = $false } $this._debugPlaceholder.Text = $text $this._debugPlaceholder.Visible = $true $this._debugPlaceholder.BringToFront() } catch { } } hidden [object] tabFont() { $f = $this._titleLabel.Font if ($null -eq $f) { $f = $this._headerPanel.Font } return $f } hidden [int] tabHeight() { $m = $this._titleLabel.Margin $h = $this._headerPanel.DisplayRectangle.Height - $m.Top - $m.Bottom if ($h -lt 16) { $h = $this._headerPanel.Height - $m.Top - $m.Bottom } if ($h -lt 16) { $h = 26 } return $h } hidden [string] tabToolTipText($editor) { if ($editor.IsReadOnly) { return ([string]$editor.Path + ' (read-only)') } return [string]$editor.Path } [object] TabLayoutInfo([string]$editorId) { if (-not $this._tabs.ContainsKey($editorId)) { return $null } $tab = $this._tabs[$editorId] $hostB = $tab.Host.Bounds return [pscustomobject]@{ TitleFont = $this._titleLabel.Font TabFont = $tab.Label.Font SameFont = $this._titleLabel.Font.Equals($tab.Label.Font) TitleBounds = $this._titleLabel.Bounds TabBounds = [System.Drawing.Rectangle]::new($hostB.X + $tab.Label.Left, $hostB.Y + $tab.Label.Top, $tab.Label.Width, $tab.Label.Height) TitleTextAlign = $this._titleLabel.TextAlign TabTextAlign = $tab.Label.TextAlign } } hidden [void] applyTabIcon($tab) { try { if ($this.Icons.ContainsKey('Close') -and $this.Icons['Close']) { $tab.Close.Image = $this.Icons['Close'] } elseif ($null -eq $tab.Close.Image) { $tab.Close.Image = $this.crossBitmap() } } catch { } } hidden [object] crossBitmap() { if ($null -eq $this._crossBitmap) { $bmp = [System.Drawing.Bitmap]::new(14, 14) $g = [System.Drawing.Graphics]::FromImage($bmp) $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias $pen = [System.Drawing.Pen]::new([System.Drawing.Color]::FromArgb(60, 60, 60), 1.6) $g.DrawLine($pen, 3, 3, 10, 10) $g.DrawLine($pen, 10, 3, 3, 10) $pen.Dispose(); $g.Dispose() $this._crossBitmap = $bmp } return $this._crossBitmap } hidden [void] updateTab($editor) { try { $id = [string]$editor.Id if (-not $this._tabs.ContainsKey($id)) { return } $tab = $this._tabs[$id] $tab.Label.Text = $editor.DisplayTitle() if ($this._tabToolTip) { $this._tabToolTip.SetToolTip($tab.Label, $this.tabToolTipText($editor)) $this._tabToolTip.SetToolTip($tab.Panel, $this.tabToolTipText($editor)) } $this.sizeTab($tab) } catch { Write-Host "updateTab - $($_.Exception.Message)" } } hidden [void] removeTab($editor) { try { $id = [string]$editor.Id if (-not $this._tabs.ContainsKey($id)) { return } $tab = $this._tabs[$id] $this._tabs.Remove($id) [void]$this._headerPanel.Items.Remove($tab.Host) try { $tab.Host.Dispose() } catch { } } catch { Write-Host "removeTab - $($_.Exception.Message)" } } hidden [void] refreshTabs() { try { $activeFore = [System.Drawing.Color]::Black $inactiveFore = [System.Drawing.Color]::DimGray $this._titleLabel.ForeColor = $(if ($this._activeView -eq 'terminal') { $activeFore } else { $inactiveFore }) $this._debugLabel.ForeColor = $(if ($this._activeView -eq 'debug') { $activeFore } else { $inactiveFore }) foreach ($tab in @($this._tabs.Values)) { $isActive = ($this._activeView -eq [string]$tab.Editor.Id) $tab.Panel.BackColor = $(if ($isActive) { [System.Drawing.Color]::White } else { [System.Drawing.SystemColors]::ControlLight }) $tab.Label.ForeColor = $(if ($isActive) { $activeFore } else { $inactiveFore }) } } catch { Write-Host "refreshTabs - $($_.Exception.Message)" } } [string[]] TabTitles() { $titles = New-Object 'System.Collections.Generic.List[string]' foreach ($item in $this._headerPanel.Items) { if ($item -is [System.Windows.Forms.ToolStripControlHost] -and $item.Name -like 'tab_*') { $titles.Add([string]$item.Control.Controls[1].Text) } } return $titles.ToArray() } hidden [void] onTabClick($editor) { if ($null -eq $editor -or $editor.IsClosed) { return } $editor.Select() } hidden [void] onTabCloseClick($editor) { if ($null -eq $editor -or $editor.IsClosed) { return } [void]$editor.Close() } hidden [void] updateSaveButton() { try { $ed = $this.displayedEditor() $show = $false $saving = $false if ($null -ne $ed) { $show = ([bool]$ed.IsDirty -and -not [bool]$ed.IsReadOnly) $saving = [bool]$ed.IsSaving } $this._btnSave.Visible = $show $this._btnSave.Enabled = ($show -and -not $saving) if ($show) { if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._btnSave.Image = $this.Icons['Save'] } $this._btnSave.ToolTipText = "Save $($ed.FileName) (Ctrl+S)" } } catch { Write-Host "updateSaveButton - $($_.Exception.Message)" } } [bool] IsSaveButtonVisible() { return [bool]$this._btnSave.Available } hidden [void] onSaveButtonClick() { try { $ed = $this.displayedEditor() if ($null -ne $ed -and -not $ed.IsReadOnly) { [void]$ed.Save() } } catch { Write-Host "onSaveButtonClick - $($_.Exception.Message)" } } hidden [void] onSaveButtonMouseEnter() { try { if (-not $this._btnSave.Enabled) { return } $this._btnSave.BackColor = [System.Drawing.Color]::FromArgb(225, 235, 245) $this._headerPanel.Cursor = [System.Windows.Forms.Cursors]::Hand } catch { } } hidden [void] onSaveButtonMouseLeave() { try { $this._btnSave.BackColor = [System.Drawing.Color]::Transparent $this._headerPanel.Cursor = [System.Windows.Forms.Cursors]::Default } catch { } } hidden [void] buildOverlay() { $this._overlay = [System.Windows.Forms.Panel]::new() $this._overlay.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250) $this._overlay.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle $this._overlay.Visible = $false $this._overlay.Dock = [System.Windows.Forms.DockStyle]::Fill $this._overlayMsg = [System.Windows.Forms.Label]::new() $this._overlayMsg.AutoSize = $false $this._overlayMsg.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._overlayMsg.ForeColor = [System.Drawing.Color]::FromArgb(30, 30, 30) $this._overlayMsg.Location = [System.Drawing.Point]::new(16, 16) $this._overlayMsg.Size = [System.Drawing.Size]::new(360, 48) $this._overlayMsg.Text = 'You have unsaved changes.' [void]$this._overlay.Controls.Add($this._overlayMsg) $this._overlaySave = [System.Windows.Forms.Button]::new() $this._overlaySave.Text = 'Save changes' $this._overlaySave.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._overlaySave.Size = [System.Drawing.Size]::new(160, 30) $this._overlaySave.Location = [System.Drawing.Point]::new(16, 76) $this._overlaySave.FlatStyle = [System.Windows.Forms.FlatStyle]::System $this._overlaySave.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $this._overlaySave.ImageAlign = [System.Drawing.ContentAlignment]::MiddleLeft $this._overlaySave.TextImageRelation = [System.Windows.Forms.TextImageRelation]::ImageBeforeText $this._overlaySave.Padding = [System.Windows.Forms.Padding]::new(6, 0, 0, 0) $this._overlaySave.Tag = $this $this._overlaySave.add_Click({ param($s, $e) $s.Tag.onOverlaySave() }) [void]$this._overlay.Controls.Add($this._overlaySave) $this._overlayDiscard = [System.Windows.Forms.Button]::new() $this._overlayDiscard.Text = 'Discard changes' $this._overlayDiscard.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._overlayDiscard.Size = [System.Drawing.Size]::new(160, 30) $this._overlayDiscard.Location = [System.Drawing.Point]::new(16, 112) $this._overlayDiscard.FlatStyle = [System.Windows.Forms.FlatStyle]::System $this._overlayDiscard.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $this._overlayDiscard.Tag = $this $this._overlayDiscard.add_Click({ param($s, $e) $s.Tag.onOverlayDiscard() }) [void]$this._overlay.Controls.Add($this._overlayDiscard) $this._overlayCancel = [System.Windows.Forms.Button]::new() $this._overlayCancel.Text = 'Cancel' $this._overlayCancel.Font = [System.Drawing.Font]::new('Segoe UI', 9) $this._overlayCancel.Size = [System.Drawing.Size]::new(160, 30) $this._overlayCancel.Location = [System.Drawing.Point]::new(16, 148) $this._overlayCancel.FlatStyle = [System.Windows.Forms.FlatStyle]::System $this._overlayCancel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $this._overlayCancel.Tag = $this $this._overlayCancel.add_Click({ param($s, $e) $s.Tag.onOverlayCancel() }) [void]$this._overlay.Controls.Add($this._overlayCancel) [void]$this._editorPanel.Controls.Add($this._overlay) } hidden [void] showOverlay($editor) { try { $this._promptEditor = $editor if ($this._activeView -ne [string]$editor.Id) { $this.switchView([string]$editor.Id) } $this._overlayMsg.Text = "You have unsaved changes in '$($editor.FileName)'.`r`nWhat would you like to do?" if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._overlaySave.Image = $this.Icons['Save'] } $this._overlay.Dock = [System.Windows.Forms.DockStyle]::Fill $this._overlay.Visible = $true $this._overlay.BringToFront() } catch { Write-Host "showOverlay - $($_.Exception.Message)" } } hidden [void] hideOverlay() { try { $this._overlay.Visible = $false } catch { } $this._promptEditor = $null } [bool] IsPromptVisible() { return ($null -ne $this._promptEditor) } hidden [void] onOverlaySave() { $ed = $this._promptEditor $this.hideOverlay() if ($null -ne $ed) { $this.Editors.ResolvePrompt($ed, 'save') } } hidden [void] onOverlayDiscard() { $ed = $this._promptEditor $this.hideOverlay() if ($null -ne $ed) { $this.Editors.ResolvePrompt($ed, 'discard') } } hidden [void] onOverlayCancel() { $ed = $this._promptEditor $this.hideOverlay() if ($null -ne $ed) { $this.Editors.ResolvePrompt($ed, 'cancel') } } hidden [void] checkVSCode() { if ($this._vscodeChecked -or -not $this.EnsureVSCode) { return } $this._vscodeChecked = $true try { $det = Test-SRxVSCodeInstalled if ($det.Installed) { return } $this._vscodeInstall = Start-SRxVSCodeUserInstall -Detection $det -BuildEdition $this.VSCodeBuildEdition if ($null -eq $this._vscodeInstall) { return } $this.status("VS Code not found - installing it for the current user in the background (log: $($this._vscodeInstall.LogPath)).") $this._vscodeTimer = New-Object System.Windows.Forms.Timer $this._vscodeTimer.Interval = 2000 $this._vscodeTimer.Tag = $this $this._vscodeTimer.add_Tick({ param($s, $e) $s.Tag.pollVSCodeInstall() }) $this._vscodeTimer.Start() } catch { Write-Host "checkVSCode - $($_.Exception.Message)" } } hidden [void] pollVSCodeInstall() { try { $p = $null if ($this._vscodeInstall) { $p = $this._vscodeInstall.Process } if ($null -ne $p -and -not $p.HasExited) { return } if ($this._vscodeTimer) { $this._vscodeTimer.Stop(); $this._vscodeTimer.Dispose(); $this._vscodeTimer = $null } $code = $null try { if ($p) { $code = $p.ExitCode } } catch { } $det = Test-SRxVSCodeInstalled if ($det.Installed) { $this.status("VS Code installed for the current user ($($det.Path)).") } else { $this.status("VS Code installation did not complete (exit code $code) - see $($this._vscodeInstall.LogPath).") } } catch { Write-Host "pollVSCodeInstall - $($_.Exception.Message)" } } [void] Shutdown() { try { if ($this._startupTimer) { $this._startupTimer.Stop(); $this._startupTimer.Dispose() } } catch { } $this._startupTimer = $null try { if ($this._ioTimer) { $this._ioTimer.Stop(); $this._ioTimer.Dispose() } } catch { } $this._ioTimer = $null try { if ($this._conPty) { $this._conPty.Stop(1500) } } catch { } $this._conPty = $null try { if ($this._webView) { $this._webView.Dispose() } } catch { } $this._webView = $null try { if ($this._vscodeTimer) { $this._vscodeTimer.Stop(); $this._vscodeTimer.Dispose() } } catch { } $this._vscodeTimer = $null try { if ($this.Editors) { $this.Editors.Clear() } } catch { } try { if ($this._editorWebView) { $this._editorWebView.Dispose() } } catch { } $this._editorWebView = $null $this._editorJsReady = $false try { if ($this._tabToolTip) { $this._tabToolTip.Dispose() } } catch { } $this._tabToolTip = $null $this._dbgBitmaps = $null try { if ($this._debugWebView) { $this._debugWebView.Dispose() } } catch { } $this._debugWebView = $null $this._debugJsReady = $false $this._debugConsoleOn = $false try { $this._debugOutbox.Clear() } catch { } } } function New-SRxTerminalBoxControl { [OutputType([object])] param() return [SRxTerminalBoxControl]::new() } Export-ModuleMember -Function New-SRxTerminalBoxControl, Import-SRxWebView2Assemblies, Get-SRxWebView2Type, Get-SRxLoadedAssembly |