In my previous blog post, I introduced PowerShellRun module as a terminal based launcher application. That is the main purpose of the module but the underlying fuzzy selector can also be used as a generic selector. In this post, we’ll see some use cases by building small tools using the selector.
What is an Interactive Selector?
An interactive selector is a tool that takes objects as inputs, shows you a UI to select the objects and returns the selected objects. In some of the popular modules, Out-ConsoleGridView
and Out-GridView -PassThru
fall into this category, but we use PowerShellRun as it gives you more customizability.
In PowerShellRun, Invoke-PSRunSelector
works as an interactive selector. The following command lists processes and lets you select a process through a TUI:
Get-Process | Invoke-PSRunSelector
It has a search bar where you can fuzzy-search processes with their name. By pressing Enter
it returns the selected process object.
The powerful part is that you can perform various actions to the selected objects by piping them to other commands. An interactive process killer for example can be made by this one-linear:
Get-Process | Invoke-PSRunSelector | Stop-Process
This is pretty much the way how the interactive selector works. In the following sections, we’ll see how we can make it more useful by customizing the UI and behavior.
Customizing the UI
The objects that are passed to the selector can have two main UI parts, Name and Description. By default, the Name
and Description
properties of the input object are used but you can specify which properties to show.
Let's make a Script Launcher as an example. The command below lists ps1 script files and pipe them to the selector. The FileInfo
object does not have a Description
property, so we specify the Directory
property instead:
Get-ChildItem *.ps1 -Recurse | Invoke-PSRunSelector -DescriptionProperty Directory
Another important UI element is the Preview window. The Preview window is a place where you can show detailed information about the focused object. Same as the -DescriptionProperty
, you can specify which property is shown in the Preview with the -PreviewProperty
parameter:
Get-ChildItem *.ps1 -Recurse | Invoke-PSRunSelector -PreviewProperty LastWriteTime -DescriptionProperty Directory
Since the Preview window is scrollable (with Shift+Up
and Shift+Down
keys), you would want to add more information than just a property. To build a custom string for the preview or other UI components, you can use -Expression
parameter. It takes a ScriptBlock that returns a hashtable. The hashtable should include string values with Name
, Description
and Preview
keys:
$originalOutputRendering = $PSStyle.OutputRendering
$PSStyle.OutputRendering = 'Ansi'
Get-ChildItem *.ps1 -Recurse | Invoke-PSRunSelector -Expression {@{
Name = $_.Name
Description = $_.Directory
Preview = $_ | Format-Table | Out-String
}}
$PSStyle.OutputRendering = $originalOutputRendering
The above code sets the Preview string to the output of Format-Table
. Out-String
is necessary to get the formatted output as a string that was supposed to be rendered on the console. By setting $PSStyle.OutputRendering
to Ansi
, the code ensures that Out-String
always outputs color escape sequences to the string.
Probably, it should be also useful if we can preview the script content in the Preview window. This is possible by just calling Get-Content
but we use bat this time for a nice syntax highlighting:
Get-ChildItem *.ps1 -Recurse | Invoke-PSRunSelector -Expression {@{
Name = $_.Name
Description = $_.Directory
# Preview is an array of strings.
Preview = @($_ | Format-Table | Out-String) + (& bat --color=always $_.FullName)
}}
Although we got a nice preview, there is a performance issue because we read all script files for the preview before opening up the selector. There is a way to generate the preview string asynchronously, but you need a more advanced command to access that feature. Now, it’s time to use Invoke-PSRunSelectorCustom
.
PreviewAsyncScript
Invoke-PSRunSelectorCustom
takes SelectorEntry
objects as input rather than objects themselves. It returns SelectorResult
object that contains the selected entry. The previous code can be re-written like this:
$result = Get-ChildItem *.ps1 -Recurse | ForEach-Object {
$entry = [PowerShellRun.SelectorEntry]::new()
$entry.UserData = $_
$entry.Name = $_.Name
$entry.Description = $_.Directory
$entry.Preview = @($_ | Format-Table | Out-String) + (& bat --color=always $_.FullName)
$entry
} | Invoke-PSRunSelectorCustom
# The selected FileInfo is stored here.
$result.FocusedEntry.UserData
The SelectorEntry
object has all the low level properties that can be used for customization. Instead of generating all the preview strings beforehand with the Preview
property, you can use the PreviewAsyncScript
and PreviewAsyncScriptArgumentList
to asynchronously generate the preview strings:
$result = Get-ChildItem *.ps1 -Recurse | ForEach-Object {
$entry = [PowerShellRun.SelectorEntry]::new()
$entry.UserData = $_
$entry.Name = $_.Name
$entry.Description = $_.Directory
$entry.PreviewAsyncScript = {
param($fileInfo)
@($fileInfo | Format-Table | Out-String) + (& bat --color=always $fileInfo.FullName)
}
$entry.PreviewAsyncScriptArgumentList = $_
$entry
} | Invoke-PSRunSelectorCustom
Now the preview string is generated in a background thread when the entry is focused which improves the boot speed of the selector.
Custom Actions
We are making a script launcher, so after selecting a script we want to run the script:
$fileInfo = $result.FocusedEntry.UserData
if ($fileInfo) {
& $fileInfo.FullName
}
SelectorResult
object contains the selected SelectorEntry
object in the FocusedEntry
property. We can get the FileInfo
object from the UserData
property as we stored it there in the previous code. The null check is for the case when the user canceled the selector with the Escape
key.
The Enter
key is the default accept key, but you can add multiple keys as accept keys and perform different actions depending on the pressed key. Let’s assign Shift+Enter
key to edit the script file. To specify additional keys per entry, you can set the ActionKeys
property:
$result = Get-ChildItem *.ps1 -Recurse | ForEach-Object {
$entry = [PowerShellRun.SelectorEntry]::new()
$entry.UserData = $_
$entry.Name = $_.Name
$entry.Description = $_.Directory
$entry.PreviewAsyncScript = {
param($fileInfo)
@($fileInfo | Format-Table | Out-String) + (& bat --color=always $fileInfo.FullName)
}
$entry.PreviewAsyncScriptArgumentList = $_
$entry.ActionKeys = @('Enter:Run script', 'Shift+Enter:Edit script')
$entry
} | Invoke-PSRunSelectorCustom
By pressing Ctrl+k
, users can see what actions are available in the Action Window.
Now that Enter
and Shift+Enter
are both treated as an accept key, you need to branch the actions depending on the pressed key:
$fileInfo = $result.FocusedEntry.UserData
if ($fileInfo) {
switch ($result.KeyCombination) {
'Enter' {
# Run the script.
& $fileInfo.FullName
}
'Shift+Enter' {
# Open the file in VSCode.
code $fileInfo.FullName
}
}
}
Full Code
This is the full code for the script launcher we made in this post:
$originalOutputRendering = $PSStyle.OutputRendering
$PSStyle.OutputRendering = 'Ansi'
$result = Get-ChildItem *.ps1 -Recurse | ForEach-Object {
$entry = [PowerShellRun.SelectorEntry]::new()
$entry.UserData = $_
$entry.Name = $_.Name
$entry.Description = $_.Directory
$entry.PreviewAsyncScript = {
param($fileInfo)
@($fileInfo | Format-Table | Out-String) + (& bat --color=always $fileInfo.FullName)
}
$entry.PreviewAsyncScriptArgumentList = $_
$entry.ActionKeys = @('Enter:Run script', 'Shift+Enter:Edit script')
$entry
} | Invoke-PSRunSelectorCustom
$PSStyle.OutputRendering = $originalOutputRendering
$fileInfo = $result.FocusedEntry.UserData
if ($fileInfo) {
switch ($result.KeyCombination) {
'Enter' {
# Run the script.
& $fileInfo.FullName
}
'Shift+Enter' {
# Open the file in VSCode.
code $fileInfo.FullName
}
}
}
Here is how it works. I typed “dock“ as a search word and hit Shift+Enter
to edit the script:
Conclusion
Interactive Selectors are powerful tools that let you select objects interactively through their UI. PowerShellRun is one of those tools and gives you many customization options.
If you find something that is suitable for the selector interface, try making your ideal tool using PowerShellRun. It would make your workflow more efficient and fun to use.
References
PowerShellRun Module
https://github.com/mdgrs-mei/PowerShellRunbat: A cat(1) clone with wings
https://github.com/sharkdp/bat