Skip to content

Instantly share code, notes, and snippets.

@rivman
Forked from jcwillox/toolbox-context-menu.ps1
Created July 20, 2022 16:30
Show Gist options
  • Save rivman/b3adc0e0f942c46e3abb27b283f792c6 to your computer and use it in GitHub Desktop.
Save rivman/b3adc0e0f942c46e3abb27b283f792c6 to your computer and use it in GitHub Desktop.
PowerShell script to automatically add context menu entries for Jetbrains IDEs
<#
.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
}
}
}
}
@rivman
Copy link
Author

rivman commented Jul 20, 2022

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"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment