GUI/Start-VSCodeUpdaterGUI.ps1
|
<#
SPDX-License-Identifier: MIT Copyright (c) 2026 Leon McClatchey, Linktech Engineering LLC Package: VSCode-Updater Author: Leon McClatchey Company: Linktech Engineering LLC Created: 2026-07-02 Modified: 2026-07-05 File: Start-VSCodeUpdaterGUI Version: 1.1.0 Description: WPF GUI host for VSCode-Updater. Uses unified output helpers, Safe Mode, and async command execution with a simple view-model. #> # Import module + helpers Remove-Module VSCode-Updater -ErrorAction SilentlyContinue Import-Module "$PSScriptRoot\..\VSCode-Updater.psd1" -Force . "$PSScriptRoot\..\Private\OutputHelpers.ps1" . "$PSScriptRoot\..\Private\Write-VSCodeUpdaterLog.ps1" . "$PSScriptRoot\..\Private\Get-SelectedVersionName.ps1" # Simple view-model for GUI state $script:vm = [pscustomobject]@{ SafeMode = $true AutoScrollEnabled = $true GuiInitialized = $false } # Lightweight Task Viewer writer function Write-TaskViewerLine { param( [string]$Text, [string]$Color = "White" ) $window.Dispatcher.Invoke({ # Cap at 200 lines $lines = $txtTaskViewer.Document.Blocks.Count if ($lines -gt 200) { $txtTaskViewer.Document.Blocks.Clear() } $run = New-Object System.Windows.Documents.Run $run.Text = $Text + "`n" $run.Foreground = $Color $para = New-Object System.Windows.Documents.Paragraph $para.Inlines.Add($run) $txtTaskViewer.Document.Blocks.Add($para) $txtTaskViewer.ScrollToEnd() }) } function Show-DashboardInTerminal { $info = Get-VSCodeDashboard Write-TerminalLine "=== Dashboard ===" "LightGray" if ($info) { Write-TerminalLine "Installed Versions: $($info.InstalledVersions)" "White" Write-TerminalLine "Version Count: $($info.VersionCount)" "White" Write-TerminalLine "Active Version: $($info.ActiveVersion)" "Green" Write-TerminalLine "Cache Path: $($info.CachePath)" "Yellow" Write-TerminalLine "LastUpdateLog: $($info.LastUpdateLog)" "White" } else { Write-TerminalLine "Dashboard: No information available." "Red" } Write-TerminalLine "=================" "LightGray" } function Show-VSCodeSymlinkInfo { $info = Get-VSCodeSymlinkInfo Write-TerminalLine "=== VSCode Symlink Info ===" "LightGray" if ($info) { Write-TerminalLine "Active Version: $($info.ActiveVersion)" "LightGreen" Write-TerminalLine "Target Path: $($info.TargetPath)" "White" Write-TerminalLine "Valid: $($info.IsValid)" "LightBlue" Write-TerminalLine "Last Update: $($info.LastUpdateResult)" "Yellow" Write-TerminalLine "Fallback Reason:$($info.LastFallbackReason)" "Red" } else { Write-TerminalLine "No symlink information found." "Red" } Write-TerminalLine "=============================" "LightGray" } function Confirm-SetupAppRunning { $proc = Get-Process -Name "SetupApp" -ErrorAction SilentlyContinue if (-not $proc) { Write-TaskViewerLine "SetupApp missing — restarting..." "Yellow" try { Start-Process "C:\Path\To\SetupApp.exe" Write-TaskViewerLine "SetupApp restarted successfully." "LightGreen" } catch { Write-TaskViewerLine "Failed to restart SetupApp: $_" "Red" } } } function Update-TaskViewerStatus { $proc = Get-Process -Name "SetupApp" -ErrorAction SilentlyContinue $window.Dispatcher.Invoke({ $txtTaskViewer.Document.Blocks.Clear() }) if ($proc) { Write-TaskViewerLine "SetupApp: Running (PID $($proc.Id))" "LightGreen" Write-TaskViewerLine ("CPU Time: {0:N2} sec" -f $proc.CPU) "White" Write-TaskViewerLine ("Memory: {0:N2} MB" -f ($proc.WorkingSet64 / 1MB)) "White" if ($proc.WorkingSet64 -gt 50MB) { Write-TaskViewerLine "Status: Busy" "Yellow" } else { Write-TaskViewerLine "Status: Idle" "LightBlue" } } else { Write-TaskViewerLine "SetupApp: Not running" "Red" } Write-TaskViewerLine ("Last check: {0}" -f (Get-Date -Format "HH:mm:ss")) "Gray" } function Update-SymlinkInfo { $info = Get-VSCodeSymlinkInfo $txtActiveVersion.Text = "Active Version: $($info.ActiveVersion)" $txtSymlinkTarget.Text = "Symlink Target: $($info.TargetPath)" $txtSymlinkValid.Text = "Symlink Status: " + ($info.IsValid ? "Valid" : "Broken") $txtLastUpdate.Text = "Last Update: $($info.LastUpdateResult)" $txtFallbackReason.Text = "Fallback Reason: $($info.LastFallbackReason)" } function Start-VSCodeUpdaterGUI { Add-Type -AssemblyName PresentationFramework Add-Type -AssemblyName PresentationCore Add-Type -AssemblyName WindowsBase $script:SafeMode = $true # default ON $xamlPath = Join-Path $PSScriptRoot "VSCodeUpdaterGUI.xaml" $xamlPath = (Resolve-Path $xamlPath).Path if (-not (Test-Path $xamlPath)) { throw "GUI XAML file not found: $xamlPath" } $xaml = Get-Content $xamlPath -Raw $utf8 = New-Object System.Text.UTF8Encoding $bytes = $utf8.GetBytes($xaml) $stream = New-Object System.IO.MemoryStream $stream.Write($bytes, 0, $bytes.Length) $stream.Position = 0 $reader = [System.Xml.XmlReader]::Create($stream) $global:window = [Windows.Markup.XamlReader]::Load($reader) $dgVersions = $window.FindName("DgVersions") $script:btnUpdate = $window.FindName("BtnUpdate") $script:btnRollback = $window.FindName("BtnRollback") $script:btnSwitch = $window.FindName("BtnSwitch") $script:txtStatus = $window.FindName("TxtStatus") $script:txtLog = $window.FindName("TxtLog") $global:txtCommandOutput = $window.FindName("TxtCommandOutput") $script:txtActiveVersion = $window.FindName("TxtActiveVersion") $script:txtSymlinkTarget = $window.FindName("TxtSymlinkTarget") $script:txtSymlinkValid = $window.FindName("TxtSymlinkValid") $script:txtLastUpdate = $window.FindName("TxtLastUpdate") $script:txtFallbackReason = $window.FindName("TxtFallbackReason") $script:btnValidateSymlink = $window.FindName("BtnValidateSymlink") $script:btnSafeMode = $window.FindName("BtnSafeMode") $script:btnDashboard = $window.FindName("BtnDashboard") $script:btnPauseLog = $window.FindName("BtnPauseLog") $script:btnSearchLog = $window.FindName("BtnSearchLog") $script:chkAutoScroll = $window.FindName("ChkAutoScroll") $script:txtSearchLog = $window.FindName("TxtSearchLog") $script:txtTaskViewer = $window.FindName("TxtTaskViewer") $script:btnRestartSetup = $window.FindName("BtnRestartSetup") $script:chkSafeMode = $window.FindName("ChkSafeMode") $chkSafeMode.IsChecked = $true $chkAutoScroll.IsChecked = $true $script:vm.AutoScrollEnabled = $true $txtTaskViewer.Document = New-Object System.Windows.Documents.FlowDocument $info = Get-VSCodeSymlinkInfo $window.Resources["ActiveVersionName"] = $info.ActiveVersion Update-SymlinkInfo # Log tailing $logRoot = "C:\Logs" $logFile = Join-Path $logRoot "Update-Code.log" $timer = New-Object Windows.Threading.DispatcherTimer $timer.Interval = [TimeSpan]::FromSeconds(2) $timer.Add_Tick({ if (Test-Path $logFile) { try { $txtLog.Text = (Get-Content $logFile -Tail 200) -join "`r`n" if ($chkAutoScroll.IsChecked) { $txtLog.ScrollToEnd() } } catch { } } }) $timer.Start() $taskTimer = New-Object Windows.Threading.DispatcherTimer $taskTimer.Interval = [TimeSpan]::FromSeconds(5) $taskTimer.Add_Tick({ Update-TaskViewerStatus }) $taskTimer.Start() # Version list $dgVersions.ItemsSource = Get-VSCodeVersions $window.Add_Loaded({ $script:vm.GuiInitialized = $true $active = $info.ActiveVersion foreach ($row in $dgVersions.Items) { if ($row.Name -eq $active) { $dgVersions.ScrollIntoView($row) $dgVersions.Dispatcher.Invoke({ $container = $dgVersions.ItemContainerGenerator.ContainerFromItem($row) if ($null -ne $container) { $container.Background = 'LightGreen' } }, 'Render') } } }) # Async helpers function Start-UpdateJob { $job = Start-Job -ScriptBlock { Update-VSCode } Register-ObjectEvent -InputObject $job -EventName StateChanged -Action { if ($job.State -eq 'Completed') { $result = Receive-Job $job $window.Dispatcher.Invoke({ $txtStatus.Text = "Update completed: $result" Show-VSCodeSymlinkInfo }) Unregister-Event -SourceIdentifier $event.SourceIdentifier Remove-Job $job } } | Out-Null } function Start-RollbackJob { $job = Start-Job -ScriptBlock { Invoke-VSCodeRollback } Register-ObjectEvent -InputObject $job -EventName StateChanged -Action { if ($job.State -eq 'Completed') { $result = Receive-Job $job $window.Dispatcher.Invoke({ $txtStatus.Text = "Rollback completed: $result" Show-VSCodeSymlinkInfo $dgVersions.ItemsSource = Get-VSCodeVersions }) Unregister-Event -SourceIdentifier $event.SourceIdentifier Remove-Job $job } } | Out-Null } function Start-SwitchJob { param($selected) $job = Start-Job -ScriptBlock { Switch-VSCodeVersion -VersionName $using:selected.Name } Register-ObjectEvent -InputObject $job -EventName StateChanged -Action { if ($job.State -eq 'Completed') { $result = Receive-Job $job $window.Dispatcher.Invoke({ $txtStatus.Text = "Switched to: $result" Show-VSCodeSymlinkInfo $dgVersions.ItemsSource = Get-VSCodeVersions }) Unregister-Event -SourceIdentifier $event.SourceIdentifier Remove-Job $job } } | Out-Null } # Events $btnUpdate.Add_Click({ if ($script:SafeMode) { _out "SAFE MODE — Update blocked." "Yellow" $txtStatus.Text = "Update blocked (Safe Mode)." return } Start-UpdateJob }) $btnRollback.Add_Click({ if ($script:SafeMode) { _out "SAFE MODE — Rollback blocked." "Yellow" $txtStatus.Text = "Rollback blocked (Safe Mode)." return } Start-RollbackJob }) $btnSwitch.Add_Click({ if ($script:SafeMode) { _out "SAFE MODE — Switch blocked." "Yellow" $txtStatus.Text = "Switch blocked (Safe Mode)." return } $selected = $dgVersions.SelectedItem if ($selected) { Start-SwitchJob -selected $selected } else { $txtStatus.Text = "Select a version before switching." } }) $btnValidateSymlink.Add_Click({ try { $result = Test-VSCodeSymlink $txtStatus.Text = "Symlink validation: $result" Update-SymlinkInfo Show-VSCodeSymlinkInfo } catch { $txtStatus.Text = "Symlink validation failed: $_" } }) $btnSafeMode.Add_Click({ Start-VSCodeSafeMode }) $chkSafeMode.Add_Checked({ Set-VSCodeSafeMode -Enabled $true $txtStatus.Text = "Safe Mode enabled." }) $chkSafeMode.Add_Unchecked({ Set-VSCodeSafeMode -Enabled $false $txtStatus.Text = "Safe Mode disabled." }) $btnDashboard.Add_Click({ Show-DashboardInTerminal }) $btnPauseLog.Add_Click({ if ($timer.IsEnabled) { $timer.Stop() $btnPauseLog.Content = "Resume Log" $txtStatus.Text = "Log paused." } else { $timer.Start() $btnPauseLog.Content = "Pause Log" $txtStatus.Text = "Log resumed." } }) $btnSearchLog.Add_Click({ $query = $txtSearchLog.Text if ([string]::IsNullOrWhiteSpace($query)) { $txtStatus.Text = "Enter text to search." return } $text = $txtLog.Text $index = $text.IndexOf($query, [System.StringComparison]::OrdinalIgnoreCase) if ($index -ge 0) { $txtLog.Select($index, $query.Length) $txtLog.ScrollToLine($txtLog.GetLineIndexFromCharacterIndex($index)) $txtStatus.Text = "Found '$query'." } else { $txtStatus.Text = "Search text not found." } }) $btnRestartSetup.Add_Click({ Confirm-SetupAppRunning }) $dgVersions.Add_SelectionChanged({ $selected = $dgVersions.SelectedItem if (-not $selected) { return } if ($selected.IsValid) { $script:btnSwitch.Content = "Switch Version" } else { $script:btnSwitch.Content = "Delete Version" } Write-TerminalLine "Selected Version: $($selected.Version)" "LightBlue" Write-VSCodeUpdaterLog "Selected Version: $($selected.Version)" }) $chkAutoScroll.Add_Checked({ $script:vm.AutoScrollEnabled = $true $txtStatus.Text = "Auto-scroll enabled." }) $chkAutoScroll.Add_Unchecked({ $script:vm.AutoScrollEnabled = $false $txtStatus.Text = "Auto-scroll disabled." }) $window.Add_Closed({ $timer.Stop() $taskTimer.Stop() }) $window.ShowDialog() | Out-Null } Start-VSCodeUpdaterGUI |