Last active
October 15, 2024 21:50
-
-
Save yumura/8df37c22ae1b7942dec7 to your computer and use it in GitHub Desktop.
powershell peco
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Load | |
Split-Path $MyInvocation.MyCommand.Path -Parent | Push-Location | |
Get-ChildItem poco_*.ps1 | %{. $_} | |
Pop-Location | |
function Select-Poco | |
{ | |
Param | |
( | |
[Object[]]$Property = $null, | |
[string]$Query = '', | |
[ValidateSet('match', 'like', 'eq')] | |
[string]$Filter = 'match', | |
[switch]$CaseSensitive = $false, | |
[switch]$InvertFilter = $false, | |
[string]$Prompt = 'Query', | |
[ValidateSet('TopDown', 'BottomUp')] | |
[string]$Layout = 'TopDown', | |
[HashTable]$Keymaps = (New-PocoKeymaps) | |
) | |
$Items = $input | %{,$_} | |
$config = New-Config $Items $Property $Prompt $Layout $Keymaps # immutable | |
$state = New-State $Query $Filter $CaseSensitive $InvertFilter $config # mutable | |
Backup-ScrBuf | |
Clear-Host | |
$action = 'None' | |
while ($action -ne 'Cancel' -and $action -ne 'Finish') | |
{ | |
Write-Screen $state $config | |
$key, $keystr = Get-PocoKey | |
$action = Get-Action $config $keystr | |
$state = Update-State $state $config $action $key | |
} | |
Restore-ScrBuf | |
if ($action -eq 'Finish') {$state.Entry} | |
trap | |
{ | |
Restore-ScrBuf | |
break | |
} | |
} | |
Set-Alias poco Select-Poco | |
Export-ModuleMember -Function "*-Poco*" -Alias "poco" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function New-Config ($Items, $Property, $Prompt, $Layout, $Keymaps) | |
{ | |
@{ | |
'Input' = $Items | |
'Property' = $Property | |
'Prompt' = $Prompt | |
'Layout' = $Layout | |
'Keymaps' = $Keymaps | |
} | |
} | |
function New-State ($Query, $Filter, $CaseSensitive, $InvertFilter, $Config) | |
{ | |
$state = @{ | |
'Query' = $Query | |
'Filter' = $Filter | |
'CaseSensitive' = $CaseSensitive | |
'InvertFilter' = $InvertFilter | |
'Acrion' = 'Identity' | |
'Entry' = @() | |
'PrevLength' = 0 | |
'Screen' = @{ | |
'Prompt' = '' | |
'FilterType' = '' | |
'QueryX' = 0 | |
'X' = 0 | |
# 'Y' = 1 # 選択機能があれば... | |
} | |
} | |
$state.Screen.Prompt = Get-Prompt $state $config | |
$state.Screen.FilterType = Get-FilterType $state | |
$state.Screen.QueryX = $Query.Length | |
$state.Screen.X = $state.Screen.Prompt.Length | |
$state.Entry = Get-Entry $state $config | |
$state | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function New-PocoKeymaps | |
{ | |
@{ | |
'Escape' = 'Cancel' | |
'Control+C' = 'Cancel' | |
'Enter' = 'Finish' | |
'Alt+B' = 'BackwardChar' | |
'Alt+F' = 'ForwardChar' | |
'Alt+A' = 'BeginningOfLine' | |
'Alt+E' = 'EndOfLine' | |
'Alt+D8' = 'DeleteBackwardChar' | |
'Backspace' = 'DeleteBackwardChar' | |
'Alt+D' = 'DeleteForwardChar' | |
'Delete' = 'DeleteForwardChar' | |
'Alt+U' = 'KillBeginningOfLine' | |
'Alt+K' = 'KillEndOfLine' | |
'Alt+R' = 'RotateMatcher' | |
'Alt+C' = 'ToggleCaseSensitive' | |
'Alt+I' = 'ToggleInvertFilter' | |
'Alt+W' = 'DeleteBackwardWord' | |
'Alt+N' = 'SelectUp' | |
'Alt+P' = 'SelectDown' | |
'Control+Spacebar' = 'ToggleSelectionAndSelectNext' | |
'UpArrow' = 'SelectUp' | |
'DownArrow' = 'SelectDown' | |
'RightArrow' = 'ScrollPageUp' | |
'LeftArrow' = 'ScrollPageDown' | |
'Tab' = 'TabExpansion' | |
} | |
} | |
function Get-PocoKey | |
{ | |
$flag = [console]::TreatControlCAsInput | |
[console]::TreatControlCAsInput = $true | |
$Key = [console]::ReadKey($true) | |
[console]::TreatControlCAsInput = $flag | |
$KeyString = $Key.Key.ToString() | |
if ($Key.Modifiers -ne 0) | |
{ | |
$m = $Key.Modifiers.ToString() -replace ', ', '+' | |
$KeyString = "${m}+${KeyString}" | |
} | |
return $Key, $KeyString | |
} | |
function Get-Action ($config, $keystr) | |
{ | |
if ($config.Keymaps.Contains($keystr)) | |
{ | |
return $config.Keymaps[$keystr] | |
} | |
if ($keystr -notmatch 'Alt|Control') | |
{ | |
'AddChar' | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Where-Query | |
{ | |
Param | |
( | |
$state, | |
[Parameter(ValueFromPipeline=$True)] | |
$obj | |
) | |
begin {$hash = Convert-QueryHash $state} | |
process | |
{ | |
if ($hash.Contains('')) | |
{ | |
foreach ($value in $hash['']) | |
{ | |
$test = Test-Matching $state.Screen.FilterType $obj $value | |
if ($test -eq $false) {return} | |
} | |
} | |
foreach ($property in $hash.Keys) | |
{ | |
if ($property -eq '') {continue} | |
$l = ($obj | Get-Member $property).length | |
if ($l -eq 0) {continue} | |
foreach ($value in $hash[$property]) | |
{ | |
$test = Test-Matching $state.Screen.FilterType $obj.$property $value | |
if ($test -eq $false) {return} | |
} | |
} | |
,$obj | |
} | |
} | |
function Convert-QueryHash ($state) | |
{ | |
$property = '' | |
$hash = @{$property = @()} | |
$state.Query -split ' ' | ?{$_ -ne ''} | %{ | |
$token = $_ | |
if ($token.StartsWith(':')) | |
{ | |
$property = $token.Remove(0, 1) | |
if (-not $hash.Contains($property)) | |
{ | |
$hash[$property] = @() | |
} | |
} | |
else | |
{ | |
$hash[$property] += $token | |
} | |
} | |
$hash | |
} | |
function Test-Matching | |
{ | |
Param | |
( | |
[string] $FilterType, | |
[string] $p, | |
[string] $value | |
) | |
try | |
{ | |
switch ($FilterType) | |
{ | |
'match' {$p -match $value} | |
'like' {$p -like $value} | |
'eq' {$p -eq $value} | |
'notmatch' {$p -notmatch $value} | |
'notlike' {$p -notlike $value} | |
'neq' {$p -ne $value} | |
'cmatch' {$p -cmatch $value} | |
'clike' {$p -clike $value} | |
'ceq' {$p -ceq $value} | |
'cnotmatch' {$p -cnotmatch $value} | |
'cnotlike' {$p -cnotlike $value} | |
'cneq' {$p -cne $value} | |
} | |
} | |
catch | |
{ | |
$true | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# http://d.hatena.ne.jp/newpops/20080514 | |
# スクリーンバッファのバックアップ | |
function Backup-ScrBuf | |
{ | |
$rui = Get-RawUI | |
$rect = New-Object System.Management.Automation.Host.Rectangle | |
$rect.Left = 0 | |
$rect.Top = 0 | |
$rect.Right = $rui.WindowSize.Width | |
$rect.Bottom = $rui.CursorPosition.Y | |
$script:screen = $rui.GetBufferContents($rect) | |
} | |
# http://d.hatena.ne.jp/newpops/20080515 | |
# スクリーンバッファのリストア | |
function Restore-ScrBuf | |
{ | |
Clear-Host | |
$rui = Get-RawUI | |
if (-not (Test-Path 'variable:screen')) {return} | |
$origin = New-Object System.Management.Automation.Host.Coordinates(0, 0) | |
$rui.SetBufferContents($origin, $script:screen) | |
$pos = New-Object System.Management.Automation.Host.Coordinates(0, $script:screen.GetUpperBound(0)) | |
$rui.CursorPosition = $pos | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Write-Screen ($state, $config) | |
{ | |
switch ($config.Layout) | |
{ | |
'TopDown' {Write-TopDown $state $config} | |
'BottomUp' {Write-BottomUp $state $config} | |
} | |
} | |
function Write-TopDown ($state, $config) | |
{ | |
Write-ScreenLine 0 $state.Screen.Prompt | |
Write-RightInfo 0 $state | |
if ($state.Entry.length -ne $state.PrevLength) | |
{ | |
$h = (Get-RawUI).WindowSize.Height | |
$entries = $state.Entry | Format-Table | Out-String -Stream | Select-Object -First ($h - 1) | |
if ($entries -is [string]) {$entries = ,@($entries)} | |
foreach ($i in 0..($h - 2)) | |
{ | |
$line = if ($i -lt $entries.length) {$entries[$i]} else {''} | |
Write-ScreenLine ($i + 1) $line | |
} | |
$state.PrevLength = $state.Entry.length | |
} | |
$x = Convert-CursorPositionX $state | |
Set-CursorPosition $x 0 | |
} | |
function Write-BottomUp ($state, $config, $entries) | |
{ | |
if ($state.Entry.length -ne $state.PrevLength) | |
{ | |
if ($state.Screen.Page -gt $m) {$state.Screen.Page = $m} | |
$h = (Get-RawUI).WindowSize.Height | |
$entries = $state.Entry | Format-Table | Out-String -Stream | Select-Object -First ($h - 1) | |
if ($entries -is [string]) {$entries = ,@($entries)} | |
foreach ($i in 0..($h - 2)) | |
{ | |
$line = if ($i -lt $entries.length) {$entries[$i]} else {''} | |
Write-ScreenLine $i $line | |
} | |
$state.PrevLength = $state.Entry.length | |
} | |
Write-ScreenLine ($h - 1) $state.Screen.Prompt | |
Write-RightInfo ($h - 1) $state | |
$x = Convert-CursorPositionX $state | |
$y = (Get-RawUI).CursorPosition.Y | |
Set-CursorPosition $x $y | |
} | |
function Write-ScreenLine ($i, $line) | |
{ | |
$w = (Get-RawUI).BufferSize.Width | |
Set-CursorPosition 0 $i | |
([string]$line).PadRight($w) | Write-Host -NoNewline | |
} | |
function Write-RightInfo ($i, $state) | |
{ | |
$f = $state.Screen.FilterType | |
$n = $state.Entry.Length | |
$h = (Get-RawUI).WindowSize.Height | |
$info = "${f} [${n}]" | |
$w = (Get-RawUI).WindowSize.Width | |
Set-CursorPosition ($w - $info.length) $i | |
$info | Write-Host -NoNewline | |
} | |
function Convert-CursorPositionX ($state) | |
{ | |
$str = $state.Screen.Prompt.Substring(0, $state.Screen.X) | |
(Get-RawUI).LengthInBufferCells($str) | |
} | |
function Set-CursorPosition ($x, $y) | |
{ | |
$pos = New-Object System.Management.Automation.Host.Coordinates($x, $y) | |
(Get-RawUI).CursorPosition = $pos | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Update-State ($state, $config, $action, $key) | |
{ | |
switch ($action) | |
{ | |
'AddChar' {Add-Char $state $config $key.KeyChar} | |
'ForwardChar' {Move-ForwardChar $state} | |
'BackwardChar' {Move-BackwardChar $state} | |
'BeginningOfLine' {Move-BeginningOfLine $state} | |
'EndOfLine' {Move-EndOfLine $state} | |
'DeleteBackwardChar' {Remove-BackwardChar $state} | |
'DeleteForwardChar' {Remove-ForwardChar $state} | |
'KillBeginningOfLine' {Remove-HeadLine $state} | |
'KillEndOfLine' {Remove-TailLine $state} | |
'RotateMatcher' {Select-Matcher $state} | |
'ToggleCaseSensitive' {Switch-CaseSensitive $state} | |
'ToggleInvertFilter' {Switch-InvertFilter $state} | |
default {} # None, Cancel, Finish = identity | |
} | |
$state | |
} | |
function Add-Char ($state, $config, $char) | |
{ | |
$x = $state.Screen.QueryX | |
$q = $state.Query | |
$state.Query = $q.Insert($x, $char) | |
$state.Screen.QueryX++ | |
$state.Screen.X++ | |
$state.Screen.Prompt = Get-Prompt $state $config | |
$state.Entry = Get-Entry $state $config | |
} | |
function Move-BackwardChar ($state) | |
{ | |
$x = $state.Screen.QueryX | |
if ($x - 1 -ge 0) | |
{ | |
$state.Screen.QueryX-- | |
$state.Screen.X-- | |
} | |
} | |
function Move-ForwardChar ($state) | |
{ | |
$x = $state.Screen.X | |
$l = $state.Screen.Prompt.length | |
if ($x + 1 -le $l) | |
{ | |
$state.Screen.QueryX++ | |
$state.Screen.X++ | |
} | |
} | |
function Move-BeginningOfLine ($state) | |
{ | |
$state.Screen.X -= $state.Screen.QueryX | |
$state.Screen.QueryX = 0 | |
} | |
function Move-EndOfLine ($state) | |
{ | |
$state.Screen.QueryX = $state.Query.length | |
$state.Screen.X = $state.Screen.Prompt.length | |
} | |
function Remove-BackwardChar ($state) | |
{ | |
$x = $state.Screen.QueryX | |
$q = $state.Query | |
if ($x - 1 -ge 0) { | |
$state.Query = $q.Remove($x - 1, 1) | |
$state.Screen.QueryX-- | |
$state.Screen.X-- | |
$state.Screen.Prompt = Get-Prompt $state $config | |
$state.Entry = Get-Entry $state $config | |
} | |
} | |
function Remove-ForwardChar ($state) | |
{ | |
$x = $state.Screen.X | |
$l = $state.Screen.Prompt.length | |
$qx = $state.Screen.QueryX | |
$q = $state.Query | |
if ($x + 1 -le $l) | |
{ | |
$state.Query = $q.Remove($qx, 1) | |
$state.Screen.Prompt = Get-Prompt $state $config | |
$state.Entry = Get-Entry $state $config | |
} | |
} | |
function Remove-HeadLine ($state) | |
{ | |
while ($state.Screen.QueryX -gt 0) | |
{ | |
Remove-BackwardChar ($state) | |
} | |
} | |
function Remove-TailLine ($state) | |
{ | |
while ($state.Screen.QueryX -lt $state.Query.length) | |
{ | |
Remove-ForwardChar ($state) | |
} | |
} | |
function Select-Matcher ($state) | |
{ | |
$arr = @('match', 'like', 'eq') | |
$n = $arr.length | |
$i = $arr.IndexOf($state.Filter) + 1 | |
$state.Filter = $arr[$i % $n] | |
$state.Screen.FilterType = Get-FilterType $state | |
$state.Entry = Get-Entry $state $config | |
} | |
function Switch-CaseSensitive ($state) | |
{ | |
$state.CaseSensitive = -not $state.CaseSensitive | |
$state.Screen.FilterType = Get-FilterType $state | |
$state.Entry = Get-Entry $state $config | |
} | |
function Switch-InvertFilter ($state) | |
{ | |
$state.InvertFilter = -not $state.InvertFilter | |
$state.Screen.FilterType = Get-FilterType $state | |
$state.Entry = Get-Entry $state $config | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Get-RawUI {(Get-Host).UI.RawUI} | |
function Get-Prompt ($state, $config) | |
{ | |
$config.Prompt + '> ' + $state.Query | |
} | |
function Get-FilterType ($state) | |
{ | |
$type = '' | |
if ($state.CaseSensitive) {$type += 'c'} | |
if ($state.InvertFilter) {$type += 'not'} | |
$type += $state.Filter | |
$type -replace 'noteq', 'neq' | |
} | |
function Get-Entry ($state, $config) | |
{ | |
$config.Input | | |
Select-Object -Property $config.Property | | |
Where-Query $state | |
} |
Hi Yamura. Thank-you for sharing this code. Could you give me a bit of guidance on how to run this code locally please? I am new to powershell module development but not new to programming in general. I am really impressed with the way I can interactively filter in powershell with this module, so I was hoping to understand more and possibly extend in some other scenarios like command history search.
You have clone this repository:
git clone https://gist.github.com/yumura/8df37c22ae1b7942dec7
cd 8df37c22ae1b7942dec7
with Admin privileges
create folder Poco at modules for PowerShell
mkdir $PSHome/Modules/Poco
cd $PSHome/Modules/Poco
copy all content to modules folder:
copy * $PSHome/Modules/Poco
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Yamura. Thank-you for sharing this code. Could you give me a bit of guidance on how to run this code locally please? I am new to powershell module development but not new to programming in general. I am really impressed with the way I can interactively filter in powershell with this module, so I was hoping to understand more and possibly extend in some other scenarios like command history search.