Streamlining your workflow around the PowerShell terminal

mdgrsmdgrs
6 min read

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.

💡
In this post, we use PowerShellRun v0.3.0 on Windows 11.

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:

  1. Ctrl+Shift+Space - Move focus to Windows Terminal with the global summon

  2. Ctrl+Space - Launch PowerShellRun

  3. Type 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

3
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 🕹️