Streamlining your workflow around the PowerShell terminal
Motivation
What do you first do when you want to start something on your PC? I used to use PowerToys Run to run installed apps, open a Visual Studio solution file or open a folder. On the other hand, when I wanted to get the latest code for a project I was working on, I opened Windows Terminal to run some git commands. So, when I launch an app, I hit Alt+Space
to open PowerToys Run. When I get the latest code, I hit Ctrl+Shift+Space
to open Windows Terminal. It is not consistent, is it? What if they are unified into a single interface?
I thought about adding everything to PowerToys Run first by creating bat files that have a command with parameters or a sequence of commands. For example to pull the latest code for project A, I create this bat file:
cd Path/To/ProjectA
git pull --rebase --prune
This is not great as you'll end up making a bat file for every project. Also, if any error occurs after the pull, you would want to run some other commands in the same terminal. For this reason, the other way around felt right to me, "making the terminal a launcher for everything". That is the reason why I made PowerShellRun, a launcher app on the PowerShell terminal and this post is an introduction to it.
PowerShellRun Module
PowerShellRun is a PowerShell module so you can install it as usual:
Install-Module -Name PowerShellRun -Scope CurrentUser
Before running the launcher, you have to enable some categories that PowerShellRun can launch. This setting exists for when you want to disable some features so let's enable all for now.
Enable-PSRunEntry -Category All
By calling Invoke-PSRun
, you get the TUI launcher.
Invoke-PSRun
You can fuzzy search entries by typing a query. Enter
is the primary key which launches the selected item. There are some more keys assigned depending on the entry type but don't worry about memorizing all of them. Hit Ctrl+k
to open the Action Window to see what actions are available.
Lastly, let's set a shortcut key with Set-PSRunPSReadlineKeyHandler
to call Invoke-PSRun
. The minimum profile script looks like this:
# profile.ps1
Enable-PSRunEntry -Category All
Set-PSRunPSReadLineKeyHandler -Chord 'Ctrl+Spacebar'
Now, this is the procedure I always take when I start something on my PC:
Ctrl+Shift+Space
- Move focus to Windows Terminal with the global summonCtrl+Space
- Launch PowerShellRunType what I need and hit
Enter
The step 2 might feel a bit redundant but let me prioritize the consistency this time. In the following sections, we'll see how PowerShellRun works for each scenario.
Launching Applications
This is the simplest one. Just type an application name and hit Enter
to launch the application. On Windows, you can also launch it as admin with Shift+Enter
.
Running Executables
Executable files under $env:PATH
are also listed as entries. CLI applications are executed in the same terminal where PowerShellRun is running. It behaves just like the filename is typed in the terminal but with a fuzzy search.
Opening Folders
The utility entry, File Manager (PSRun)
, navigates the folder hierarchy using PowerShellRun's TUI. You can go inside the folder, Set-Location
to the directory, or open it with the Explorer.
By setting folders that you frequently visit as favorites, you can list them in the top menu for quick access.
Add-PSRunFavoriteFolder -Path 'D:/PowerShellRun' -Icon '✨'
Opening Files
Files are basically the same as folders except that they can be opened or executed by the default apps with Enter
key. With Shift+Enter
you can edit the file by a custom editor script you specify.
Set-PSRunDefaultEditorScript -ScriptBlock {
param ($path)
& nvim $path
}
Calling PowerShell Functions
This is the reason why we are making the launcher on the terminal and what makes PowerShellRun special. You can add your functions just by defining them between Start-PSRunFunctionRegistration
and Stop-PSRunFunctionRegistration
. SYNOPSIS
or DESCRIPTION
in the comment based help is used as a description of the entry.
Start-PSRunFunctionRegistration
#.SYNOPSIS
# git pull with rebase and prune options.
function global:GitPullRebase() {
git pull --rebase --prune
}
# Define functions here as many as you want...
Stop-PSRunFunctionRegistration
It works just like typing the function name on the terminal but the TUI helps you find it even if you don't remember the exact name.
Sub Menus
As an advanced function entry, it's also possible to open a sub menu inside a function.
Invoke-PSRunSelectorCustom
takes an array of SelectorEntry
objects, opens the selector and returns the selected entry and the pressed key.
'a', 'b', 'c' | ForEach-Object {
$entry = [PowerShellRun.SelectorEntry]::new()
$entry.Name = $_
$entry
} | Invoke-PSRunSelectorCustom
To make it look like a sub menu, you can enable QuitWithBackspaceOnEmptyQuery
through options and restore the parent menu by calling Restore-PSRunFunctionParentSelector
on backspace.
Start-PSRunFunctionRegistration
function global:SubMenu() {
$option = Get-PSRunDefaultSelectorOption
$option.QuitWithBackspaceOnEmptyQuery = $true
$option.Prompt = 'Backspace to go back > '
$result = 'a', 'b', 'c' | ForEach-Object {
$entry = [PowerShellRun.SelectorEntry]::new()
$entry.Name = $_
$entry
} | Invoke-PSRunSelectorCustom -Option $option
if ($result.KeyCombination -eq 'Backspace') {
Restore-PSRunFunctionParentSelector
return
}
$result.FocusedEntry.Name
}
Stop-PSRunFunctionRegistration
As a practical example of a sub menu, let's make a git branch switcher.
This function lists available git branches and lets you select a branch and switch to it. You can build the UI by setting properties of SelectorEntry
, such as ActionKeys
and PreviewAsyncScript
. It looks a bit complex but the points are passing an array of SelectorEntry
to Invoke-PSRunSelectorCustom
and handling the returned object.
Start-PSRunFunctionRegistration
<#
.SYNOPSIS
Git switch to branch
.COMPONENT
PSRun(Icon = 🥏)
#>
function global:GitSwitch() {
$option = Get-PSRunDefaultSelectorOption
$option.QuitWithBackspaceOnEmptyQuery = $true
$option.Prompt = 'Filter branch name > '
$currentDir = (Get-Location).Path
$result = git branch --all | ForEach-Object {
if ($_.Contains('HEAD detached')) {
return
}
$branchName = $_.Replace('*', '').Trim().Split(' ')[0]
$isRemote = $branchName -cmatch 'remotes/[^/]*/(.*)'
$localBranchName = $null
if ($isRemote) {
$localBranchName = $Matches[1]
}
$isHead = $localBranchName -eq 'HEAD'
$entry = [PowerShellRun.SelectorEntry]::new()
$entry.UserData = $branchName, $localBranchName
$entry.Name = $_
$entry.ActionKeys = if ($isHead) {
[PowerShellRun.ActionKey]::new('Shift+Enter', 'Detach HEAD and Switch')
} elseif ($isRemote) {
@(
[PowerShellRun.ActionKey]::new('Enter', 'Create branch and Switch')
[PowerShellRun.ActionKey]::new('Shift+Enter', 'Detach HEAD and Switch')
)
} else {
[PowerShellRun.ActionKey]::new('Enter', 'Switch to this branch')
}
$entry.PreviewAsyncScript = {
param ($dir, $branchName)
git -C $dir log $branchName -20 --graph --oneline --decorate=short --color=always
}
$entry.PreviewAsyncScriptArgumentList = $currentDir, $branchName
$entry
} | Invoke-PSRunSelectorCustom -Option $option
if ($result.KeyCombination -eq 'Backspace') {
Restore-PSRunFunctionParentSelector
return
}
if ($result.FocusedEntry.UserData) {
$branchName, $localBranchName = $result.FocusedEntry.UserData
if ($result.KeyCombination -eq 'Enter') {
if ($localBranchName) {
git switch $localBranchName
} else {
git switch $branchName
}
} else {
git switch $branchName --detach
}
}
}
Stop-PSRunFunctionRegistration
If you are familiar with fzf, you should be able to imagine what else you can make. You can do customization similar to fzf but in a more object-based way. Please visit the project's Discussions page to see more examples.
Conclusion
With a help of PowerShellRun module, you can launch anything from the terminal and streamline your workflow around it. I switched from a desktop launcher app to this one and I like it so far. I would be happy if this post could be a hint for you to explore your terminal based workflows.
References
PowerShellRun Module
Windows Terminal Global summon
https://learn.microsoft.com/en-us/windows/terminal/customize-settings/actions#global-summon
Subscribe to my newsletter
Read articles from mdgrs directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
mdgrs
mdgrs
Hobby Coder who loves PowerShell 💻 GameDev at work 🕹️