Instantly share code, notes, and snippets.
        
          
            Forked from jcwillox/toolbox-context-menu.ps1
          
        
    
          Created
          July 20, 2022 16:30 
        
      - 
            
      
        
      
    Star
      
          0
          (0)
      
  
You must be signed in to star a gist  - 
              
      
        
      
    Fork
      
          0
          (0)
      
  
You must be signed in to fork a gist  
- 
        
Save rivman/b3adc0e0f942c46e3abb27b283f792c6 to your computer and use it in GitHub Desktop.  
    PowerShell script to automatically add context menu entries for Jetbrains IDEs
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | <# | |
| .SYNOPSIS | |
| Automatically add context menu entries for Jetbrains IDEs. | |
| .PARAMETER Name | |
| The name or names of the IDEs to add context menus for, use -List to see available IDEs. | |
| .PARAMETER BasePath | |
| The path to the Toolbox apps directory, defaults to "$env:LOCALAPPDATA\JetBrains\Toolbox\apps". | |
| .PARAMETER Global | |
| Install context menu entries in HKLM registry (machine wide), requires running as administrator. | |
| .PARAMETER Force | |
| Overwrite current registry entries, useful when updating existing entries. | |
| .PARAMETER Remove | |
| Will remove the context menu entries for specified IDEs | |
| .PARAMETER List | |
| List available IDEs installed via Toolbox | |
| .PARAMETER UseNircmd | |
| Will use nircmd (if installed) to invisibly run the IDE's batch script, this will avoid you having to | |
| re-run this script each time an IDE is updated via Toolbox. | |
| .PARAMETER NirCmdPath | |
| Specify the location of the nircmd executable to use, will attempt to locate it from $PATH if not specified. | |
| .PARAMETER AppDir | |
| If you are not using Toolbox you can specify the IDE's installation directory. | |
| .PARAMETER DefaultChannel | |
| Specify the default channel to use when there are multiple channels found, the user will be prompted to | |
| choose a channel if the default channel is not found. | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [array]$Name, | |
| [string]$BasePath, | |
| [switch]$Global, | |
| [switch]$Force, | |
| [switch]$Remove, | |
| [switch]$List, | |
| [switch]$UseNircmd, | |
| [string]$NirCmdPath, | |
| [string]$AppDir, | |
| [ArgumentCompleter({ return @("'Release'", "'Early Access Program'") })] | |
| [string]$DefaultChannel | |
| ) | |
| $ErrorActionPreference = "Stop" | |
| trap { throw $Error[0] } | |
| $toolbox = if ($BasePath) { $BasePath } else { Join-Path $env:LOCALAPPDATA "JetBrains\Toolbox\apps" } | |
| $regRoot = if ($Global) { "HKLM" } else { "HKCU" } | |
| Write-Verbose "Toolbox: '$toolbox'" | |
| if ($UseNircmd -and -not (Get-Command "nircmd.exe" -ErrorAction Ignore)) { | |
| Write-Error "nircmd.exe is not installed or missing from the path" | |
| Write-Error "either install nircmd or specify its location using '-NirCmdPath'" | |
| return | |
| } | |
| function Get-Channel([string]$Path) { | |
| $channels = Get-ChildItem (Join-Path $Path "ch-*") | |
| if (-not $channels) { | |
| Write-Error "no channels found in '$Path'" | |
| } | |
| # immediately return if only 1 channel | |
| if ($channels.Length -eq 1) { | |
| return $channels[0] | |
| } | |
| $channelsOutput = "" | |
| for ($i = 0; $i -lt $channels.Length; $i++) { | |
| $channel = $channels[$i] | |
| # extract channel type | |
| $channelSettings = (Get-Content (Join-Path $channel.FullName ".channel.settings.json") | ConvertFrom-Json) | |
| $channelName = $channelSettings.filter.quality_filter.name | |
| # skip the prompt if we match the default channel | |
| if ($DefaultChannel -eq $channelName) { | |
| return $channel | |
| } | |
| $channelsOutput += "[$i]: '$($channel.BaseName)' ($channelName)" | |
| if ($i -lt $channels.Length - 1) { | |
| $channelsOutput += "`n" | |
| } | |
| } | |
| # prompt user to select which channel to use | |
| Write-Host (Split-Path -Leaf $Path) -ForegroundColor Green | |
| Write-Host $channelsOutput | |
| $i = $null | |
| while (-not ($i -ge 0 -and $i -lt $channels.Length)) { | |
| if ($null -ne $i) { | |
| Write-Host "'$i' is not a valid index." -ForegroundColor Red | |
| } | |
| $i = Read-Host "Enter the number of the desired channel" | |
| } | |
| return $channels[$i] | |
| } | |
| function Get-IDEs { | |
| $IDEs = @{} | |
| foreach ($item in (Get-ChildItem $toolbox -Exclude Toolbox)) { | |
| $channel = Get-Channel -Path $item.FullName | |
| $versionPath = (Get-ChildItem -Path $channel -Directory -Exclude "*.plugins" | Sort-Object BaseName -Descending | Select-Object -First 1 | Select-Object -ExpandProperty FullName) | |
| Write-Verbose "Version Path: '$versionPath'" | |
| $binPath = Join-Path $versionPath "bin" | |
| if (Test-Path $binPath) { | |
| $IDEs[$item.BaseName] = $versionPath | |
| } | |
| } | |
| return $IDEs | |
| } | |
| if ($List) { | |
| return Get-IDEs | |
| } | |
| function Add-ShellKeys { | |
| param ( | |
| [string]$Name, | |
| [string]$ExePath, | |
| [string]$Path, | |
| [string]$Action, | |
| [string]$LaunchArgs | |
| ) | |
| $Path = Join-Path "${regRoot}:" $Path | |
| if ($UseNircmd) { | |
| # extract shell link path | |
| $scriptPath = Get-Content (Join-Path (Split-Path $ExePath) "../.." ".shellLink") | |
| # move icon outside of version directory so it will survive updates | |
| $baseName = (Split-Path $ExePath -LeafBase).Replace("64", "") | |
| $iconPath = Join-Path (Split-Path $ExePath) "$baseName.ico" | |
| $destPath = Join-Path (Split-Path (Split-Path (Split-Path $ExePath))) "$baseName.ico" | |
| Move-Item -Path $iconPath -Destination $destPath -ErrorAction Ignore | |
| $iconPath = "`"$destPath`",0" | |
| # contruct args | |
| $ExePath = if ($NirCmdPath) { $NirCmdPath } else { (Get-Command "nircmd.exe").Path } | |
| $LaunchArgs = "execmd $scriptPath $LaunchArgs" | |
| } else { | |
| $iconPath = $ExePath | |
| } | |
| if (-not $Force -and (Test-Path -LiteralPath "$Path\$Name")) { | |
| Write-Host "EXISTS: $Path\$Name" -f Yellow | |
| } else { | |
| New-Item ` | |
| -Path "$Path\$Name" ` | |
| -Value "$Action with $Name" ` | |
| -Force:$Force ` | |
| -Confirm:$false | Out-Null | |
| Write-Host "ADDED: $Path\$Name" -f DarkGreen | |
| } | |
| if (-not $Force -and (Get-ItemProperty -LiteralPath "$Path\$Name" -Name "Icon" -ErrorAction Ignore)) { | |
| Write-Host "EXISTS: $Path\$Name [Icon]" -f Yellow | |
| } else { | |
| New-ItemProperty ` | |
| -LiteralPath "$Path\$Name" ` | |
| -PropertyType ExpandString ` | |
| -Name "Icon" ` | |
| -Value $iconPath ` | |
| -Force:$Force ` | |
| -Confirm:$false | Out-Null | |
| Write-Host "ADDED: $Path\$Name [Icon]" -f DarkGreen | |
| } | |
| if (-not $Force -and (Get-Item -LiteralPath "$Path\$Name\command" -ErrorAction Ignore)) { | |
| Write-Host "EXISTS: $Path\$Name\command" -f Yellow | |
| } else { | |
| New-Item ` | |
| -Path "$Path\$Name\command" ` | |
| -Value "`"$exePath`" $LaunchArgs" ` | |
| -Force:$Force ` | |
| -Confirm:$false | Out-Null | |
| Write-Host "ADDED: $Path\$Name\command" -f DarkGreen | |
| } | |
| } | |
| function Add-RegKey([string]$Path) { | |
| $Path = Join-Path "${regRoot}:" $Path | |
| if (-not (Test-Path -LiteralPath $Path)) { | |
| New-Item -Path $Path -Force -ErrorAction Ignore -Confirm:$false | Out-Null | |
| } | |
| } | |
| function Add-ContextMenu { | |
| param ( | |
| [string]$Name, | |
| [string]$ExePath | |
| ) | |
| # ensure base folders exist | |
| Add-RegKey "SOFTWARE\Classes\`*\shell" | |
| Add-RegKey "SOFTWARE\Classes\Directory\shell" | |
| Add-RegKey "SOFTWARE\Classes\Directory\Background\shell" | |
| # add to file context menu | |
| Write-Output "ACTION: Edit with $Name (Files)" | |
| Add-ShellKeys $Name $ExePath "SOFTWARE\Classes\*\shell" "Edit" "`"%1`"" | |
| # add to directory context menu | |
| Write-Output "ACTION: Open with $Name (Directory)" | |
| Add-ShellKeys $Name $ExePath "SOFTWARE\Classes\Directory\shell" "Open" "`"%1`"" | |
| # add to directory background context menu | |
| Write-Output "ACTION: Open with $Name (Directory Background)" | |
| Add-ShellKeys $Name $ExePath "SOFTWARE\Classes\Directory\Background\shell" "Open" "`"%V`"" | |
| } | |
| function Remove-Reg([string]$Path) { | |
| $Path = Join-Path "${regRoot}:" $Path | |
| if (-not (Test-Path -LiteralPath $Path)) { | |
| Write-Host "MISSING: $Path" -f Yellow | |
| } else { | |
| Remove-Item -LiteralPath $Path -Recurse | |
| Write-Host "REMOVED: $Path" -f Red | |
| } | |
| } | |
| function Remove-ContextMenu([string]$Name) { | |
| Remove-Reg "SOFTWARE\Classes\*\shell\$Name" | |
| Remove-Reg "SOFTWARE\Classes\Directory\shell\$Name" | |
| Remove-Reg "SOFTWARE\Classes\Directory\Background\shell\$Name" | |
| } | |
| function Start-ContextMenu([string]$AppDir) { | |
| $info = (Get-Content (Join-Path $AppDir "product-info.json") | ConvertFrom-Json) | |
| $friendlyName = $info.Name | |
| $exePath = Join-Path $AppDir $info.launch[0].launcherPath | |
| if ($Remove) { | |
| Write-Host "Removing Context Menu for '$friendlyName'" -ForegroundColor Cyan | |
| Remove-ContextMenu -Name $friendlyName | |
| } else { | |
| Write-Host "Adding Context Menu for '$friendlyName'" -ForegroundColor Cyan | |
| Add-ContextMenu -Name $friendlyName -ExePath $exePath | |
| } | |
| } | |
| if ($AppDir) { | |
| Start-ContextMenu $AppDir | |
| } else { | |
| $ides = (Get-IDEs) | |
| foreach ($ide in $ides.Keys) { | |
| foreach ($match in $Name) { | |
| if (($match -eq "*") -or ($ide.ToLower().StartsWith($match.ToLower()))) { | |
| Start-ContextMenu $ides[$ide] | |
| break | |
| } | |
| } | |
| } | |
| } | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment
  
            
Download Script
iwr https://gist.github.com/jcwillox/a9b480f8d180d44368e7df2eb7009207/raw/toolbox-context-menu.ps1 -o toolbox-context-menu.ps1
You may also need to run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser.
Toolbox Users
You can list installed IDEs with -List and then specify the IDEs you want to add context menus for, note that you don't have to specify the full name pyc will be enough to match with PyCharm-P. You can remove the added context menus by using the -Remove option.
.\toolbox-context-menu.ps1 -List
.\toolbox-context-menu.ps1 pyc,idea
.\toolbox-context-menu.ps1 pyc,idea -Remove
update existing context menu entries
.\toolbox-context-menu.ps1 pyc,idea -Force
NirCmd
With the default method you will have to re-run this script using the -Force option each time toolbox updates an IDE as the executable path will change. You can avoid this by using nircmd, it will be used to silently run the batch script (e.g. idea.cmd). Make sure you've enabled the "Generate shell scripts" option in Toolbox, it doesn't matter where you place these scripts as long as the path won't change when you update an IDE, ~/.local/bin or $env:LOCALAPPDATA\JetBrains\Toolbox\scripts are good options.
.\toolbox-context-menu.ps1 pyc,idea -UseNircmd -Force
optionally specify the path for nircmd if its not on your system $PATH
.\toolbox-context-menu.ps1 pyc,idea -UseNircmd -NirCmdPath "C:...path"
Direct Install
If you installed your IDE with the direct download you can still use this script by specifying the installation directory with -AppDir
.\toolbox-context-menu.ps1 -AppDir "C:\Program Files\JetBrains\WebStorm 2021.3.1"