src/GUI/ContextMenuAdder.ps1

<#
.SYNOPSIS
启用/禁用/重置 ps12exe 的右键菜单
.DESCRIPTION
启用/禁用/重置 ps12exe 的右键菜单
.PARAMETER action
enable、disable 或 reset
.PARAMETER Locale
用于服务器端日志记录的语言代码
.EXAMPLE
Set-ps12exeContextMenu
.EXAMPLE
Set-ps12exeContextMenu -action 'enable' -Locale 'en-UK'
#>

[CmdletBinding()]
param (
    [ValidateScript({
        . $PSScriptRoot\..\predicate.ps1
        (IsEnable $_) -or (IsDisable $_) -or ($_ -eq 'reset')
    })]
    [ArgumentCompleter({
        param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)
        . $PSScriptRoot\..\predicate.ps1
        if (-not $WordToComplete) {
            @('enable', 'disable', 'reset')
        }
        else {
            @($DisablePredicates; $EnablePredicates; 'reset') | Where-Object { $_ -like "$WordToComplete*" }
        }
    })]
    $action = 'on',
    [ArgumentCompleter({
        Param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
        . "$PSScriptRoot\..\LocaleArgCompleter.ps1" @PSBoundParameters
    })]
    [string]$Locale,
    [switch]$SkipEditorExtension,
    [switch]$help
)

$LocalizeData = . $PSScriptRoot\..\LocaleLoader.ps1 -Locale $Locale

if ($help) {
    $MyHelp = $LocalizeData.SetContextMenuHelpData
    . $PSScriptRoot\..\HelpShower.ps1 -HelpData $MyHelp | Write-Host
    return
}

Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
 
public class ExplorerRefresher {
    [DllImport("user32.dll", SetLastError = true)]
    private static extern IntPtr SendMessageTimeout(IntPtr hWnd, int Msg, IntPtr wParam, string lParam, uint fuFlags, uint uTimeout, IntPtr lpdwResult);
 
    private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff);
    private const int WM_SETTINGCHANGE = 0x1a;
    private const int SMTO_ABORTIFHUNG = 0x0002;
    public static void RefreshSettings() {
        SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, null, SMTO_ABORTIFHUNG, 100, IntPtr.Zero);
    }
    [DllImport("shell32.dll")]
    private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);
    public static void RefreshDesktop() {
        SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero);
    }
}
'@


function PwshCodeAsCommand($command) {
    "powershell.exe -NoProfile -Command `"if(-Not (Get-Module -ListAvailable -Name ps12exe)){ Install-Module ps12exe -Force -Scope CurrentUser -ErrorAction Ignore }; Import-Module ps12exe -ErrorAction Stop; $command`""
}

function AddCommandToContextMenu {
    param (
        $className,
        $fileType,
        $title,
        $command,
        $Icon = "$PSScriptRoot\..\..\img\icon.ico"
    )
    $key = "Registry::HKEY_CURRENT_USER\Software\Classes\*\shell\$className"

    if (-not (Test-Path -LiteralPath $key)) {
        New-Item -Path $key -Force | Out-Null
    }

    Set-ItemProperty -LiteralPath $key -Name "(Default)" -Value $title
    New-ItemProperty -LiteralPath $key -Name "AppliesTo" -Value "System.ItemName:$fileType" -PropertyType String -Force | Out-Null

    if ($Icon) {
        New-ItemProperty -LiteralPath $key -Name "Icon" -Value $Icon -PropertyType String -Force | Out-Null
    }

    New-Item -Path "$key\Command" -Force | Out-Null
    Set-ItemProperty -LiteralPath "$key\Command" -Name "(Default)" -Value $command
}

function RemoveCommandsFromContextMenu($className) {
    $key = "Registry::HKEY_CURRENT_USER\Software\Classes\*\shell\$className"
    if (Test-Path -LiteralPath $key) {
        Remove-Item -LiteralPath $key -Recurse
    }
}

function AddFileType($fileType, $DefaultProgram) {
    $key = "Registry::HKEY_CURRENT_USER\Software\Classes\$fileType"
    New-Item -Path $key -Force | Out-Null
    if ($DefaultProgram) {
        New-Item -Path "$key\OpenWithProgids" -Force | Out-Null
        New-ItemProperty -LiteralPath "$key\OpenWithProgids" -Name $DefaultProgram -Value "" -PropertyType String -Force | Out-Null
    }
}

function RemoveFileType($fileType) {
    $key = "Registry::HKEY_CURRENT_USER\Software\Classes\$fileType"
    if (Test-Path -LiteralPath $key) {
        Remove-Item -LiteralPath $key -Recurse
    }
}

function AddFileHandlerProgram {
    param (
        $className,
        $command,
        $FileDescription,
        $Icon = "$PSScriptRoot\..\..\img\icon.ico"
    )
    $key = "Registry::HKEY_CURRENT_USER\Software\Classes\$className"

    if (-not (Test-Path -LiteralPath $key)) {
        New-Item -Path $key -Force | Out-Null
    }

    if ($Icon) {
        New-Item -Path $key -Name "DefaultIcon" -Force | Out-Null
        New-ItemProperty -LiteralPath "$key\DefaultIcon" -Name "(Default)" -Value $Icon -PropertyType String -Force | Out-Null
    }
    New-ItemProperty -LiteralPath $key -Name "FriendlyTypeName" -Value $FileDescription -PropertyType String -Force | Out-Null

    New-Item -Path "$key\shell\open\command" -Force | Out-Null
    Set-ItemProperty -LiteralPath "$key\shell\open\command" -Name "(Default)" -Value $command
}

function RemoveFileHandlerProgram($className) {
    $key = "Registry::HKEY_CURRENT_USER\Software\Classes\$className"
    if (Test-Path -LiteralPath $key) {
        Remove-Item -LiteralPath $key -Recurse
    }
}

# 用 Get-Command 探测的基于 VS Code 的编辑器,以及要安装到其中的扩展。
$VSCodeBasedEditorNames = @(
    'code',                # Visual Studio Code
    'code-insiders',    # Visual Studio Code Insiders
    'codium',            # VSCodium
    'vscodium',            # VSCodium
    'cursor',            # Cursor
    'windsurf',            # Windsurf
    'trae',                # Trae
    'positron',            # Positron
    'void'                # Void
)
$VSCodeExtensionId = 'steve02081504.ps12exe'

# 用 Get-Command 探测上述编辑器,并对指向同一 CLI 的名称去重。
function Get-VSCodeBasedEditors {
    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($name in $VSCodeBasedEditorNames) {
        $command = Get-Command -Name $name -CommandType Application -ErrorAction Ignore | Select-Object -First 1
        if (-not $command) { continue }
        if (-not $seen.Add($command.Source)) { continue }
        [PSCustomObject]@{
            Name = $name
            Path = $command.Source
        }
    }
}

# 通过 CLI 将 ps12exe VS Code 扩展安装到每个检测到的编辑器中。该扩展尚未发布到应用市场,因此失败会被报告并忽略。
function Install-ps12exeVSCodeExtension {
    $InstallingMessage = if ($LocalizeData.VSCodeExtensionInstalling) { $LocalizeData.VSCodeExtensionInstalling }
    else { 'Installing the ps12exe extension for {0} ...' }
    $FailedMessage = if ($LocalizeData.VSCodeExtensionInstallFailed) { $LocalizeData.VSCodeExtensionInstallFailed }
    else { 'Failed to install the ps12exe extension for {0} (it may not be published yet): {1}' }

    $editors = try { @(Get-VSCodeBasedEditors) } catch { @() }
    foreach ($editor in $editors) {
        try {
            $installed = & $editor.Path --list-extensions 2>$null
            if ($installed | Where-Object { $_ -and ($_.Trim() -ieq $VSCodeExtensionId) }) {
                continue
            }
            Write-Host ($InstallingMessage -f $editor.Name) -ForegroundColor Gray
            $output = & $editor.Path --install-extension $VSCodeExtensionId --force 2>&1
            if ($LASTEXITCODE) {
                Write-Warning ($FailedMessage -f @($editor.Name, (($output | Out-String).Trim())))
            }
        }
        catch {
            Write-Warning ($FailedMessage -f @($editor.Name, $_.Exception.Message))
        }
    }
}

# 从每个检测到的编辑器中卸载 ps12exe VS Code 扩展(仅卸载确实已安装的)。
function Uninstall-ps12exeVSCodeExtension {
    $UninstallingMessage = if ($LocalizeData.VSCodeExtensionUninstalling) { $LocalizeData.VSCodeExtensionUninstalling }
    else { 'Uninstalling the ps12exe extension for {0} ...' }
    $FailedMessage = if ($LocalizeData.VSCodeExtensionUninstallFailed) { $LocalizeData.VSCodeExtensionUninstallFailed }
    else { 'Failed to uninstall the ps12exe extension for {0}: {1}' }

    $editors = try { @(Get-VSCodeBasedEditors) } catch { @() }
    foreach ($editor in $editors) {
        try {
            $installed = & $editor.Path --list-extensions 2>$null
            if (-not ($installed | Where-Object { $_ -and ($_.Trim() -ieq $VSCodeExtensionId) })) {
                continue
            }
            Write-Host ($UninstallingMessage -f $editor.Name) -ForegroundColor Gray
            $output = & $editor.Path --uninstall-extension $VSCodeExtensionId 2>&1
            if ($LASTEXITCODE) {
                Write-Warning ($FailedMessage -f @($editor.Name, (($output | Out-String).Trim())))
            }
        }
        catch {
            Write-Warning ($FailedMessage -f @($editor.Name, $_.Exception.Message))
        }
    }
}

. $PSScriptRoot\..\predicate.ps1
if ('reset' -eq $action -or (IsDisable $action)) {
    RemoveCommandsFromContextMenu "ps12exeCompile"
    RemoveCommandsFromContextMenu "ps12exeGUIOpen"
    RemoveFileHandlerProgram "ps12exeGUI.psccfg"
    RemoveFileType ".psccfg"
    if ((IsDisable $action) -and -not $SkipEditorExtension) {
        Uninstall-ps12exeVSCodeExtension
    }
}
if ('reset' -eq $action -or (IsEnable $action)) {
    AddCommandToContextMenu "ps12exeCompile" "ps1" $LocalizeData.CompileTitle (PwshCodeAsCommand "ps12exe '%1';pause")
    AddCommandToContextMenu "ps12exeGUIOpen" "ps1" $LocalizeData.OpenInGUI (PwshCodeAsCommand "ps12exeGUI -PS1File '%1'")
    AddFileHandlerProgram "ps12exeGUI.psccfg" (PwshCodeAsCommand "ps12exeGUI '%1'") $LocalizeData.GUICfgFileDesc
    AddFileType ".psccfg" "ps12exeGUI.psccfg"
    if (-not $SkipEditorExtension) {
        Install-ps12exeVSCodeExtension
    }
}
[ExplorerRefresher]::RefreshSettings()
[ExplorerRefresher]::RefreshDesktop()