diff --git a/src/samy.functions.ps1 b/src/samy.functions.ps1 index a0dd5d2..28a913b 100644 --- a/src/samy.functions.ps1 +++ b/src/samy.functions.ps1 @@ -1,16 +1,157 @@ +#region Web: Remote Fetch Helpers + +function Get-RemoteText { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Url + ) + + try { + $resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop + return $resp.Content + } + catch { + Write-LogHybrid "Get-RemoteText failed for ${Url}: $($_.Exception.Message)" Warning UI -LogToEvent + return "" + } +} + +#endregion Web: Remote Fetch Helpers + +#region Web: Router / Request Dispatch + +function Invoke-TasksCompleted { + param($Context) + Write-LogHybrid "All UI-selected tasks processed" Info UI -LogToEvent + Send-Text $Context "Tasks completion acknowledged." +} + +function Dispatch-Request { + param($Context) + + if ($null -eq $Context -or $null -eq $Context.Request -or $null -eq $Context.Request.Url) { + return + } + + try { + $path = $Context.Request.Url.AbsolutePath.TrimStart('/') + + switch -Regex ($path) { + + '^$' { + $html = Get-UIHtml -Page 'onboard' + Send-HTML $Context $html + return + } + + '^samy\.js$' { + Send-RemoteAsset -Context $Context -Url $Script:SamyJsUrl -ContentType 'application/javascript; charset=utf-8' + return + } + + '^samy\.css$' { + Send-RemoteAsset -Context $Context -Url $Script:SamyCssUrl -ContentType 'text/css; charset=utf-8' + return + } + + '^SVS_logo\.svg$' { + Send-RemoteAsset -Context $Context -Url $Script:SamyTopLogoUrl -ContentType 'image/svg+xml' + return + } + + '^SAMY\.png$' { + Send-RemoteAsset -Context $Context -Url $Script:SamyBgLogoUrl -ContentType 'image/png' + return + } + + '^SVS_Favicon\.ico$' { + Send-RemoteAsset -Context $Context -Url $Script:SamyFaviconUrl -ContentType 'image/x-icon' + return + } + } + + if ($path -eq 'quit') { + Write-LogHybrid "Shutdown requested" "Info" "Server" -LogToEvent + Send-Text $Context "Server shutting down." + $Global:Listener.Stop() + return + } + + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'tasksCompleted') { Invoke-TasksCompleted $Context; return } + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') { Invoke-FetchSites $Context; return } + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'renameComputer') { Invoke-RenameComputer $Context; return } + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getprinters') { Invoke-GetPrinters $Context; return } + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'installprinters'){ Invoke-InstallPrinters $Context; return } + + if ($path -in @('onboard', 'offboard', 'devices')) { + $html = Get-UIHtml -Page $path + Send-HTML $Context $html + return + } + + $task = $Global:SamyTasks | Where-Object Name -EQ $path | Select-Object -First 1 + if ($task) { + $fn = Get-TaskHandlerName -Task $task + if ([string]::IsNullOrWhiteSpace($fn)) { + $Context.Response.StatusCode = 500 + Send-Text $Context "Task '$($task.Label)' is missing a handler (HandlerFn/Handler/Fn/Name)." + return + } + + $cmd = Get-Command -Name $fn -ErrorAction SilentlyContinue + if (-not $cmd) { + $Context.Response.StatusCode = 500 + Send-Text $Context "Handler not found: $fn" + return + } + + if ($cmd.Parameters.ContainsKey('Context')) { & $fn -Context $Context } + else { & $fn } + return + } + + $Context.Response.StatusCode = 404 + Send-Text $Context '404 - Not Found' + } + catch { + # Always return something so clients don't hang + try { + $Context.Response.StatusCode = 500 + $pos = if ($_.InvocationInfo) { $_.InvocationInfo.PositionMessage } else { "" } + $stk = if ($_.ScriptStackTrace) { $_.ScriptStackTrace } else { "" } + $msg = "Dispatch error: $($_.Exception.Message)`n$pos`n$stk" + $bytes = [Text.Encoding]::UTF8.GetBytes($msg) + $Context.Response.ContentType = 'text/plain; charset=utf-8' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) + } catch { } + } + finally { + try { $Context.Response.OutputStream.Close() } catch { } + try { $Context.Response.Close() } catch { } + } +} + +#endregion Web: Router / Request Dispatch + +#region Bootstrap: NuGet + PowerShellGet + function Initialize-NuGetProvider { [CmdletBinding()] param() - # Silent defaults + #region Bootstrap: Defaults / Non-interactive behavior + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $ConfirmPreference = 'None' - - # Extra guardrails for "ShouldContinue" style prompts $PSDefaultParameterValues['*:Confirm'] = $false - # Ensure provider folders exist (CurrentUser and AllUsers locations) + #endregion Bootstrap: Defaults / Non-interactive behavior + + #region Bootstrap: Ensure provider folders exist + $userProvPath = Join-Path $env:LOCALAPPDATA 'PackageManagement\ProviderAssemblies' $allProvPath = Join-Path ${env:ProgramFiles} 'PackageManagement\ProviderAssemblies' @@ -20,34 +161,37 @@ function Initialize-NuGetProvider { New-Item -Path $p -ItemType Directory -Force -ErrorAction Stop | Out-Null Write-LogHybrid "Ensured provider folder exists: $p" Info Bootstrap -LogToEvent } - } catch { + } + catch { # AllUsers path can fail without admin. That's OK. Write-LogHybrid "Could not create provider folder: $p ($($_.Exception.Message))" Warning Bootstrap -LogToEvent } } - # 1) Install NuGet provider FIRST, silently, so Install-Module never prompts + #endregion Bootstrap: Ensure provider folders exist + + #region Bootstrap: NuGet provider install/import + try { $existing = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue | - Sort-Object Version -Descending | Select-Object -First 1 + Sort-Object Version -Descending | Select-Object -First 1 if (-not $existing -or $existing.Version -lt [Version]'2.8.5.201') { - Write-LogHybrid "Installing NuGet provider (min 2.8.5.201)..." Info Bootstrap -LogToEvent - # ForceBootstrap helps avoid interactive bootstrap prompts Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 ` -Force -ForceBootstrap -Confirm:$false ` -Scope CurrentUser -ErrorAction Stop | Out-Null $existing = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue | - Sort-Object Version -Descending | Select-Object -First 1 + Sort-Object Version -Descending | Select-Object -First 1 } if ($existing) { Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null Write-LogHybrid "NuGet provider ready (v$($existing.Version))" Success Bootstrap -LogToEvent - } else { + } + else { throw "NuGet provider still not available after install attempt." } } @@ -56,12 +200,17 @@ function Initialize-NuGetProvider { throw } - # 2) Now it is safe to update / install modules without NuGet prompts + #endregion Bootstrap: NuGet provider install/import + + #region Bootstrap: PackageManagement / PowerShellGet / PSGallery policy + try { Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null - $pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version + $pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | + Sort-Object Version -Descending | Select-Object -First 1).Version + if ($pkgMgmtVersion -and $pkgMgmtVersion -lt [Version]'1.3.1') { Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent @@ -72,7 +221,6 @@ function Initialize-NuGetProvider { Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent } - # Trust PSGallery (optional, but common) $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop @@ -81,48 +229,64 @@ function Initialize-NuGetProvider { } catch { Write-LogHybrid "PackageManagement/PowerShellGet setup failed: $($_.Exception.Message)" Warning Bootstrap -LogToEvent - # You can choose to throw here if you want hard-fail behavior + # Decide if you want to throw here or allow soft-fail. } + + #endregion Bootstrap: PackageManagement / PowerShellGet / PSGallery policy } -#region Re-usable functions +#endregion Bootstrap: NuGet + PowerShellGet + +#region Registry: Low-level helpers function Set-RegistryValueInHkuRoot { [CmdletBinding()] param( - [Parameter(Mandatory)] [string] $HkuRoot, # e.g. "Registry::HKEY_USERS\S-1-5-21-..." - [Parameter(Mandatory)] [string] $RelativeKeyPath, # e.g. "Software\...\Explorer\Advanced" + [Parameter(Mandatory)] [string] $HkuRoot, + [Parameter(Mandatory)] [string] $RelativeKeyPath, [Parameter(Mandatory)] [string] $Name, - [Parameter(Mandatory)] [ValidateSet('String','ExpandString','DWord','QWord','Binary','MultiString')] [string] $Type, + [Parameter(Mandatory)] + [ValidateSet('String','ExpandString','DWord','QWord','Binary','MultiString')] + [string] $Type, [Parameter(Mandatory)] $Value ) $k = Join-Path $HkuRoot $RelativeKeyPath if (-not (Test-Path $k)) { New-Item -Path $k -Force | Out-Null } - New-ItemProperty -Path $k -Name $Name -PropertyType $Type -Value $Value -Force -err - Stop | Out-Null + New-ItemProperty -Path $k -Name $Name -PropertyType $Type -Value $Value -Force -ErrorAction Stop | Out-Null } +#endregion Registry: Low-level helpers + +#region Registry: Apply settings to Current User + Default User + Existing Profiles + function Set-RegistryValueForCurrentAndAllUsers { [CmdletBinding()] param( [Parameter(Mandatory)] [string] $RelativeKeyPath, [Parameter(Mandatory)] [string] $Name, - [Parameter(Mandatory)] [ValidateSet('String','ExpandString','DWord','QWord','Binary','MultiString')] [string] $Type, + [Parameter(Mandatory)] + [ValidateSet('String','ExpandString','DWord','QWord','Binary','MultiString')] + [string] $Type, [Parameter(Mandatory)] $Value ) - # 1) Current user (HKCU) when meaningful + #region HKCU: current user (best-effort) + try { $hkcuKey = "HKCU:\$RelativeKeyPath" if (-not (Test-Path $hkcuKey)) { New-Item -Path $hkcuKey -Force | Out-Null } New-ItemProperty -Path $hkcuKey -Name $Name -PropertyType $Type -Value $Value -Force | Out-Null - } catch { + } + catch { # Common during SYSTEM runs; ignore } - # 2) Default User (future users) + #endregion HKCU: current user (best-effort) + + #region Default User: future profiles + $defaultDat = "C:\Users\Default\NTUSER.DAT" $mountName = "SVS_DefaultUser" $mount = "HKU\$mountName" @@ -135,10 +299,12 @@ function Set-RegistryValueForCurrentAndAllUsers { if ($LASTEXITCODE -eq 0) { $didLoad = $true Write-LogHybrid "Loaded Default User hive ($defaultDat) to HKEY_USERS\$mountName" Info Tweaks -LogToEvent - } else { + } + else { Write-LogHybrid "Failed to load Default User hive ($defaultDat) to HKEY_USERS\$mountName. reg.exe said: $loadOut" Warning Tweaks -LogToEvent } - } else { + } + else { Write-LogHybrid "Default User hive already loaded at HKEY_USERS\$mountName (skipping reg load)" Info Tweaks -LogToEvent } @@ -155,28 +321,32 @@ function Set-RegistryValueForCurrentAndAllUsers { $unloadOut = & reg.exe unload $mount 2>&1 if ($LASTEXITCODE -eq 0) { Write-LogHybrid "Unloaded Default User hive from HKEY_USERS\$mountName" Info Tweaks -LogToEvent - } else { + } + else { Write-LogHybrid "Failed to unload Default User hive from HKEY_USERS\$mountName. reg.exe said: $unloadOut" Warning Tweaks -LogToEvent } } } - } else { + } + else { Write-LogHybrid "Default User hive not found at $defaultDat (skipping future-user tweak)" Warning Tweaks -LogToEvent } + #endregion Default User: future profiles + + #region Existing Profiles: iterate ProfileList, mount NTUSER.DAT as needed - # 3) Existing profiles $profileList = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" Get-ChildItem $profileList -ErrorAction SilentlyContinue | ForEach-Object { $sid = $_.PSChildName if ($sid -notmatch '^S-1-5-21-\d+-\d+-\d+-\d+$') { return } - # If already loaded, write directly if (Test-Path "Registry::HKEY_USERS\$sid") { try { Set-RegistryValueInHkuRoot -HkuRoot "Registry::HKEY_USERS\$sid" ` -RelativeKeyPath $RelativeKeyPath -Name $Name -Type $Type -Value $Value - } catch {} + } + catch { } return } @@ -191,168 +361,171 @@ function Set-RegistryValueForCurrentAndAllUsers { try { Set-RegistryValueInHkuRoot -HkuRoot "Registry::HKEY_USERS\SVS_$sid" ` -RelativeKeyPath $RelativeKeyPath -Name $Name -Type $Type -Value $Value - } finally { + } + finally { & reg.exe unload $tempMount 2>$null | Out-Null } } + + #endregion Existing Profiles: iterate ProfileList, mount NTUSER.DAT as needed } +#endregion Registry: Apply settings to Current User + Default User + Existing Profiles + +#region Shell: Restart helpers + function Restart-ExplorerIfInteractive { [CmdletBinding()] param() - # Avoid during SYSTEM/unboxing contexts if ($env:USERNAME -ne 'SYSTEM') { Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue Start-Process explorer.exe } } -#endregion Re-usable functions +#endregion Shell: Restart helpers -#region App handlers +#region UI Handlers: Apps - function Invoke-Install1Password { - param($Context) +function Invoke-Install1Password { + param($Context) - try { - # Default if called without suboptions - $selected = @('desktop') + try { + $selected = @('desktop') - # If JS POSTs { checkedValues: [...] }, use that - if ($Context -and $Context.Request -and $Context.Request.HttpMethod -eq 'POST') { - $raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() - if (-not [string]::IsNullOrWhiteSpace($raw)) { - $data = $raw | ConvertFrom-Json - if ($data.checkedValues) { $selected = @($data.checkedValues) } - } + if ($Context -and $Context.Request -and $Context.Request.HttpMethod -eq 'POST') { + $raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() + if (-not [string]::IsNullOrWhiteSpace($raw)) { + $data = $raw | ConvertFrom-Json + if ($data.checkedValues) { $selected = @($data.checkedValues) } } + } - # Extension IDs from official store listings - $chromeExtId = 'aeblfdkhhhdcdjpifhhbdiojplfjncoa' # Chrome Web Store :contentReference[oaicite:1]{index=1} - $edgeExtId = 'dppgmdbiimibapkepcbdbmkaabgiofem' # Edge Add-ons :contentReference[oaicite:2]{index=2} + $chromeExtId = 'aeblfdkhhhdcdjpifhhbdiojplfjncoa' + $edgeExtId = 'dppgmdbiimibapkepcbdbmkaabgiofem' - if ($selected -contains 'desktop') { - winget install -e --id AgileBits.1Password --silent --accept-package-agreements --accept-source-agreements - Write-LogHybrid "Installed 1Password desktop app via winget" Success SVSApps -LogToEvent - } + if ($selected -contains 'desktop') { + winget install -e --id AgileBits.1Password --silent --accept-package-agreements --accept-source-agreements + Write-LogHybrid "Installed 1Password desktop app via winget" Success SVSApps -LogToEvent + } - if ($selected -contains 'chromeExt') { - $chromeKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist" - New-Item -Path $chromeKey -Force | Out-Null - New-ItemProperty -Path $chromeKey -Name "1" -PropertyType String -Force ` + if ($selected -contains 'chromeExt') { + $chromeKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist" + New-Item -Path $chromeKey -Force | Out-Null + New-ItemProperty -Path $chromeKey -Name "1" -PropertyType String -Force ` -Value "$chromeExtId;https://clients2.google.com/service/update2/crx" | Out-Null - Write-LogHybrid "Forced 1Password extension install for Chrome" Success SVSApps -LogToEvent - } + Write-LogHybrid "Forced 1Password extension install for Chrome" Success SVSApps -LogToEvent + } - if ($selected -contains 'edgeExt') { - $edgeKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallForcelist" - New-Item -Path $edgeKey -Force | Out-Null - New-ItemProperty -Path $edgeKey -Name "1" -PropertyType String -Force ` + if ($selected -contains 'edgeExt') { + $edgeKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallForcelist" + New-Item -Path $edgeKey -Force | Out-Null + New-ItemProperty -Path $edgeKey -Name "1" -PropertyType String -Force ` -Value "$edgeExtId;https://edge.microsoft.com/extensionwebstorebase/v1/crx" | Out-Null - Write-LogHybrid "Forced 1Password extension install for Edge" Success SVSApps -LogToEvent - } + Write-LogHybrid "Forced 1Password extension install for Edge" Success SVSApps -LogToEvent + } - Send-Text $Context "1Password processed: $($selected -join ', ')" - } - catch { - Write-LogHybrid "1Password install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent - Send-Text $Context "ERROR: $($_.Exception.Message)" - } + Send-Text $Context "1Password processed: $($selected -join ', ')" } - - function Invoke-DisableAnimations { - param($Context) - - try { - $selected = @() - - if ($Context -and $Context.Request -and $Context.Request.HttpMethod -eq 'POST') { - $raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() - if (-not [string]::IsNullOrWhiteSpace($raw)) { - $data = $raw | ConvertFrom-Json - if ($data.checkedValues) { $selected = @($data.checkedValues) } - } - } - - if ($selected.Count -eq 0) { - $selected = @('window','taskbar','menus') - } - - if ($selected -contains 'window') { - Set-RegistryValueForCurrentAndAllUsers ` - -RelativeKeyPath "Control Panel\Desktop\WindowMetrics" ` - -Name "MinAnimate" -Type String -Value "0" - } - - if ($selected -contains 'taskbar') { - Set-RegistryValueForCurrentAndAllUsers ` - -RelativeKeyPath "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" ` - -Name "TaskbarAnimations" -Type DWord -Value 0 - } - - if ($selected -contains 'menus') { - Set-RegistryValueForCurrentAndAllUsers ` - -RelativeKeyPath "Control Panel\Desktop" ` - -Name "MenuShowDelay" -Type String -Value "50" - } - - if ($selected -contains 'taskbar') { - Restart-ExplorerIfInteractive - } - - Write-LogHybrid "Disable Animations applied (Current + All Existing + Default User). Selected: $($selected -join ', ')" Success Tweaks -LogToEvent - Send-Text $Context "Disable Animations applied: $($selected -join ', ')" - } - catch { - Write-LogHybrid "Disable Animations failed: $($_.Exception.Message)" Error Tweaks -LogToEvent - Send-Text $Context "ERROR: $($_.Exception.Message)" - } + catch { + Write-LogHybrid "1Password install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent + Send-Text $Context "ERROR: $($_.Exception.Message)" } +} +#endregion UI Handlers: Apps - function Invoke-EnableNumLock { - param($Context) +#region UI Handlers: Tweaks - try { - # .DEFAULT affects the logon screen + default profile behavior - $path = "Registry::HKEY_USERS\.DEFAULT\Control Panel\Keyboard" - New-Item -Path $path -Force | Out-Null +function Invoke-DisableAnimations { + param($Context) - # Common value: "2" = NumLock on at startup (Windows uses string here) - Set-ItemProperty -Path $path -Name "InitialKeyboardIndicators" -Value "2" -Type String + try { + $selected = @() - Write-LogHybrid "NumLock default enabled (HKEY_USERS\.DEFAULT)" Success Tweaks -LogToEvent - Send-Text $Context "NumLock default enabled." + if ($Context -and $Context.Request -and $Context.Request.HttpMethod -eq 'POST') { + $raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() + if (-not [string]::IsNullOrWhiteSpace($raw)) { + $data = $raw | ConvertFrom-Json + if ($data.checkedValues) { $selected = @($data.checkedValues) } + } } - catch { - Write-LogHybrid "Enable NumLock failed: $($_.Exception.Message)" Error Tweaks -LogToEvent - Send-Text $Context "ERROR: $($_.Exception.Message)" + + if ($selected.Count -eq 0) { + $selected = @('window','taskbar','menus') } + + if ($selected -contains 'window') { + Set-RegistryValueForCurrentAndAllUsers ` + -RelativeKeyPath "Control Panel\Desktop\WindowMetrics" ` + -Name "MinAnimate" -Type String -Value "0" + } + + if ($selected -contains 'taskbar') { + Set-RegistryValueForCurrentAndAllUsers ` + -RelativeKeyPath "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" ` + -Name "TaskbarAnimations" -Type DWord -Value 0 + } + + if ($selected -contains 'menus') { + Set-RegistryValueForCurrentAndAllUsers ` + -RelativeKeyPath "Control Panel\Desktop" ` + -Name "MenuShowDelay" -Type String -Value "50" + } + + if ($selected -contains 'taskbar') { + Restart-ExplorerIfInteractive + } + + Write-LogHybrid "Disable Animations applied (Current + All Existing + Default User). Selected: $($selected -join ', ')" Success Tweaks -LogToEvent + Send-Text $Context "Disable Animations applied: $($selected -join ', ')" } - - function Invoke-ClassicContextMenu { - param($Context) - - try { - $key = "HKCU:\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" - New-Item -Path $key -Force | Out-Null - - # Default value must be blank - Set-ItemProperty -Path $key -Name "(default)" -Value "" -Force - - # Restart Explorer so it takes effect - Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue - Start-Process explorer.exe - - Write-LogHybrid "Classic context menu enabled (Win11)" Success Tweaks -LogToEvent - Send-Text $Context "Classic context menu enabled (Explorer restarted)." - } - catch { - Write-LogHybrid "Classic context menu tweak failed: $($_.Exception.Message)" Error Tweaks -LogToEvent - Send-Text $Context "ERROR: $($_.Exception.Message)" - } + catch { + Write-LogHybrid "Disable Animations failed: $($_.Exception.Message)" Error Tweaks -LogToEvent + Send-Text $Context "ERROR: $($_.Exception.Message)" } +} +function Invoke-EnableNumLock { + param($Context) + + try { + $path = "Registry::HKEY_USERS\.DEFAULT\Control Panel\Keyboard" + New-Item -Path $path -Force | Out-Null + + # Ensure it's a string value (Windows uses string here) + New-ItemProperty -Path $path -Name "InitialKeyboardIndicators" -PropertyType String -Value "2" -Force | Out-Null + + Write-LogHybrid "NumLock default enabled (HKEY_USERS\.DEFAULT)" Success Tweaks -LogToEvent + Send-Text $Context "NumLock default enabled." + } + catch { + Write-LogHybrid "Enable NumLock failed: $($_.Exception.Message)" Error Tweaks -LogToEvent + Send-Text $Context "ERROR: $($_.Exception.Message)" + } +} + +function Invoke-ClassicContextMenu { + param($Context) + + try { + $key = "HKCU:\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" + New-Item -Path $key -Force | Out-Null + + Set-ItemProperty -Path $key -Name "(default)" -Value "" -Force + + Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue + Start-Process explorer.exe + + Write-LogHybrid "Classic context menu enabled (Win11)" Success Tweaks -LogToEvent + Send-Text $Context "Classic context menu enabled (Explorer restarted)." + } + catch { + Write-LogHybrid "Classic context menu tweak failed: $($_.Exception.Message)" Error Tweaks -LogToEvent + Send-Text $Context "ERROR: $($_.Exception.Message)" + } +} + +#endregion UI Handlers: Tweaks -#endregion App handlers