Managing Windows Explorer context menu with Powershell

Abstract

Managing Windows Exporer context menu has always been somewhat of a challenge. At least since retirement of Win95 Power Toys with their ‘cmd prompt here’ back in 1990x, Windows instrmentation has always been lacking standard tools for doing this kind of tasks. While there currently are a number of 3rd party tools available, mostly compiled programs, the need to download and install a 3rd part binary executable may appear an unfeasible overkill and unnecessary risk, and thus inappropriate in this situation. Manually editing Windows Registry via regedit is tricky and prone to typos and other kinds of errors, let alone not reproducible.

On the other hand, Poweshell scripting is a very good candidate. A script is easily customizable; it can be replayed on any number of workstations, or on the same worktation to restore or alter customized menus. The only requrement is Powershell itself, which has become an integral part of Win10 OS.

Description

The script creates the cascading context submenu Shells containing a number of popular shells: cmd, powershell, Windows Subsystem for Linux shell, Windows Terminal, plus some development shell environments. All shells except cmd are added conditionally (once found).

The cascading menu is attached to folders (context #2), folder backgrounds (context #3) and Library folder backgrounds (context #4). A shell is launched in the directory the menu was invoked upon. The submenu is registered in the HKCU registry tree (thus no admin rights are required and the submenu is user-specific).

  • cmd is added unconditionally unless Windows Terminal is found.
  • powershell and wsl are added if found.
  • If Windows Terminal is found, it is added, and adding separate items for cmd, powershell and wsl is suppressed.
  • If MS Visual Studio 2019 (or just its Build Tools) is found, an additional entry is added that launches cmd with x64 build environment activated.
  • The script looks up a conda environment named dev (may be overriden by a command line argument). If found, an additional entry is added that launches cmd with this env activated. If MS VS/Build Tools is also present, the x64 environment is also activated in that shell.

Requirements

Windows 10 with all recent updates and powershell 5.1 or later.

How to run

  • Save code to a file (e.g. directory-menu.ps1)
  • Digitally sign if necessary
  • Run it from powershell

TODO

  • Add ‘remove menu’

Code

<#
.SYNOPSIS

Creates cascading submenus to Explorer directory context menu with commonly used shells.

.DESCRIPTION

This script creates a submenu "Shells", containing cmd, powershell and wsl entries. 
Shells are launched in the directory the context menu is invoked upon.
#>
#To print out this help in PS: get-help .\directory-menu.ps1 -detailed
param(
    [string]
    #Conda environment name
    $CondaEnv = "dev",
    [Int[]]
    #Context numbers, array of numbers from 1 to 4
    $Contexts = (2,3,4)
)

#should be followed by Create-Menu-Item calls
function Create-Cascading-Menu(
  [Int]$Context, 
  [String]$Key, 
  [String]$Name, 
  [String]$Icon
  ) {
    switch($Context) {
        1 { $BasePath = "HKCU:\Software\Classes\Folder\shell"}
        2 { $BasePath = "HKCU:\Software\Classes\Directory\shell"}
        3 { $BasePath = "HKCU:\Software\Classes\Directory\Background\shell" }
        4 { $BasePath = "HKCU:\Software\Classes\LibraryFolder\background\shell" }
        default {  
            Write-Error "Invalid context value: $Context"
            return
        }
    }

    Set-Location $BasePath
    New-Item -Name $Key -Force | Out-Null
    Set-Location $Key
    New-ItemProperty -Path . -Name "MUIVerb" -Value $Name -PropertyType STRING `
      | Out-Null
    New-ItemProperty -Path . -Name "subcommands" -Value "" -PropertyType STRING `
      | Out-Null
    New-ItemProperty -Path . -Name "Icon" -Value $Icon -PropertyType STRING `
      | Out-Null
    New-Item -Name "shell" `
      | Out-Null
    Set-Location "shell"
}


#Must be called in the context of 'shell' registry key
function Create-Menu-Item(
  [String]$Key, 
  [String]$Name, 
  [String]$Icon, 
  [String]$Command
  ) {
    if (-not (@(Get-Location).Path.StartsWith("HKCU:"))) {
        Write-Error "Current location is not in HKCU:"
        return
    }    
    New-Item -Name $Key | Out-Null
    Push-Location $Key
    Set-Item -Path . -Value $Name
    New-ItemProperty -Path . -Name "Icon" -Value $Icon | Out-Null
    New-Item -Name "Command" | Out-Null
    Push-Location "Command"
    Set-Item -Path . -Value $Command
    Pop-Location
    Pop-Location
}

$CmdPath = (Get-Command cmd.exe).Path
$GotoAndRunPfx = "$CmdPath /k pushd `"%V`" && "
$WTPath = (Get-Command -ErrorAction Ignore wt.exe).Path
if ($WTPath) { $GotoAndRunPfx = "$WTPath -d `"%V`" -p `"cmd`" cmd /k " }
$WSLPath = (Get-Command -ErrorAction Ignore wsl.exe).Path
$PSPath = (Get-Command -ErrorAction Ignore powershell.exe).Path
$VSActivate = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019" + `
  "\BuildTools\VC\Auxiliary\Build\vcvarsall.bat"
$HasVS = (Test-Path $VSActivate)
if (-not ($CondaEnv)) { $CondaEnv = "dev" }
$CondaRoot = "$env:USERPROFILE\Miniconda3"
$CondaActivate = "$CondaRoot\condabin\activate.bat"
$CondaEnvRoot = "$CondaRoot\envs\$CondaEnv"
$HasCondaEnv = (Test-Path $CondaEnvRoot)

function Create-Menu([Int]$Context) {
    Create-Cascading-Menu -Context $Context -Key Shells -Name Shells -Icon "$CmdPath,0"

    #If Windows Terminal is present, we add it alone instead of adding cmd + ps + wsl 
    #(as those can be launched from WT)
    if ($WTPath) {
        Create-Menu-Item -Key "wt" -Name "Terminal" -Icon "$CmdPath,0" `
          -Command "$WTPath -d `"%V`""
    } else {
        Create-Menu-Item -Key "cmd" -Name "cmd" -Icon "$CmdPath,0" `
          -Command "$CmdPath /s /k pushd `"%V`""
        if ($PSPath) {
            Create-Menu-Item -Key "ps" -Name "PowerShell" -Icon "$PSPath,0" `
              -Command "$PSPath -NoExit -Command `"cd %V `""
        }
        if ($WSLPath) {
            Create-Menu-Item -Key "wsl" -Name "WSL" -Icon "$WSLPath,0" `
              -Command "$WSLPath --cd `"%V`""
        }
    }

    if ($HasCondaEnv) {
        if ($HasVS) {
            $c = "$GotoAndRunPfx `"$CondaActivate`" $CondaEnv && `"$VSActivate`" x64"
        } else {
            $c = "$GotoAndRunPfx `"$CondaActivate`" $CondaEnv"
        }
        Create-Menu-Item -Key $CondaEnv -Name $CondaEnv `
          -Icon "$CondaRoot\Menu\Iconleak-Atrous-Console.ico" -Command $c
    }
    if ($HasVS) {
        Create-Menu-Item -Key "vscmd" -Name "VStudioTools" -Icon "$CmdPath,0" `
          -Command "$GotoAndRunPfx `"$VSActivate`" x64"
    }
}

Push-Location
foreach ($cx in $Contexts) {
    Write-Host "Creating menu for context $cx"
    Create-Menu -Context $cx
}
Pop-Location