├── .gitignore ├── README.md ├── docs ├── Select-Poco.md ├── about_poco.txt └── demos │ └── poco-ps.gif ├── psakefile.ps1 └── src └── ps1 ├── poco.psd1 ├── poco.psm1 ├── poco_init.ps1 ├── poco_key.ps1 ├── poco_query.ps1 ├── poco_scrbuf.ps1 ├── poco_screen.ps1 ├── poco_update_state.ps1 └── poco_util.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | build/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poco 2 | Interactive pipeline filtering in PowerShell (a port of [peco](https://github.com/peco/peco)). 3 | 4 | A fork of [poco by yumura](https://gist.github.com/yumura/8df37c22ae1b7942dec7). 5 | 6 | **Major Features:** 7 | 8 | - Interactively filter objects in the pipeline (interactive version of `Where-Object`) 9 | 10 | ![Example of poco in action](docs/demos/poco-ps.gif) 11 | 12 | ## Usage 13 | 14 | ### Syntax 15 | 16 | ```powershell 17 | Select-Poco [[-Property] ] [[-Query] ] [[-Filter] {match | like | eq}] [[-Prompt] ] 18 | [[-Layout] {TopDown | BottomUp}] [[-Keymaps] ] [-CaseSensitive] [-InvertFilter] 19 | ``` 20 | 21 | ## Install 22 | 23 | Inspect 24 | 25 | ```powershell 26 | Save-Module -Name poco -Path 27 | ``` 28 | 29 | Install 30 | 31 | ```powershell 32 | Install-Module -Name poco 33 | ``` 34 | 35 | ## Build 36 | 37 | ```powershell 38 | Invoke-PSake build 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/Select-Poco.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: poco-help.xml 3 | Module Name: poco 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Select-Poco 9 | 10 | ## SYNOPSIS 11 | Interactively filter objects from the pipeline. 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | Select-Poco [[-Property] ] [[-Query] ] [[-Filter] ] [-CaseSensitive] [-InvertFilter] 17 | [[-Prompt] ] [[-Layout] ] [[-Keymaps] ] 18 | ``` 19 | 20 | ## DESCRIPTION 21 | This command will interactively filter objects from the 22 | 23 | ## EXAMPLES 24 | 25 | ### Example 1 26 | ```powershell 27 | PS C:\> {{ Add example code here }} 28 | ``` 29 | 30 | {{ Add example description here }} 31 | 32 | ## PARAMETERS 33 | 34 | ### -CaseSensitive 35 | Force comparison to be case sensitive. 36 | 37 | ```yaml 38 | Type: SwitchParameter 39 | Parameter Sets: (All) 40 | Aliases: 41 | 42 | Required: False 43 | Position: Named 44 | Default value: None 45 | Accept pipeline input: False 46 | Accept wildcard characters: False 47 | ``` 48 | 49 | ### -Filter 50 | Comparison mode. 51 | 52 | ```yaml 53 | Type: String 54 | Parameter Sets: (All) 55 | Aliases: 56 | Accepted values: match, like, eq 57 | 58 | Required: False 59 | Position: 2 60 | Default value: None 61 | Accept pipeline input: False 62 | Accept wildcard characters: False 63 | ``` 64 | 65 | ### -InvertFilter 66 | Invert the boolean logic of every comparison. 67 | 68 | ```yaml 69 | Type: SwitchParameter 70 | Parameter Sets: (All) 71 | Aliases: 72 | 73 | Required: False 74 | Position: Named 75 | Default value: None 76 | Accept pipeline input: False 77 | Accept wildcard characters: False 78 | ``` 79 | 80 | ### -Keymaps 81 | {{Fill Keymaps Description}} 82 | 83 | ```yaml 84 | Type: Hashtable 85 | Parameter Sets: (All) 86 | Aliases: 87 | 88 | Required: False 89 | Position: 5 90 | Default value: None 91 | Accept pipeline input: False 92 | Accept wildcard characters: False 93 | ``` 94 | 95 | ### -Layout 96 | Layout poco from top or bottom. 97 | 98 | ```yaml 99 | Type: String 100 | Parameter Sets: (All) 101 | Aliases: 102 | Accepted values: TopDown, BottomUp 103 | 104 | Required: False 105 | Position: 4 106 | Default value: None 107 | Accept pipeline input: False 108 | Accept wildcard characters: False 109 | ``` 110 | 111 | ### -Prompt 112 | Prompt string to display next to query. 113 | 114 | ```yaml 115 | Type: String 116 | Parameter Sets: (All) 117 | Aliases: 118 | 119 | Required: False 120 | Position: 3 121 | Default value: None 122 | Accept pipeline input: False 123 | Accept wildcard characters: False 124 | ``` 125 | 126 | ### -Property 127 | {{Fill Property Description}} 128 | 129 | ```yaml 130 | Type: Object[] 131 | Parameter Sets: (All) 132 | Aliases: 133 | 134 | Required: False 135 | Position: 0 136 | Default value: None 137 | Accept pipeline input: False 138 | Accept wildcard characters: False 139 | ``` 140 | 141 | ### -Query 142 | Specify starting query. 143 | 144 | ```yaml 145 | Type: String 146 | Parameter Sets: (All) 147 | Aliases: 148 | 149 | Required: False 150 | Position: 1 151 | Default value: None 152 | Accept pipeline input: False 153 | Accept wildcard characters: False 154 | ``` 155 | 156 | ## INPUTS 157 | 158 | ### System.Object 159 | 160 | 161 | ## OUTPUTS 162 | 163 | ### System.Object 164 | 165 | ## NOTES 166 | 167 | ## RELATED LINKS 168 | 169 | [about_poco]() 170 | -------------------------------------------------------------------------------- /docs/about_poco.txt: -------------------------------------------------------------------------------- 1 | TOPIC 2 | about_poco 3 | 4 | SHORT DESCRIPTION 5 | poco is a PowerShell port of peco (https://github.com/peco/peco) and 6 | offers in-pipeline, interactive filtering of objects or text. 7 | 8 | LONG DESCRIPTION 9 | 10 | Select-Poco will collect all pipeline input and display a blocking, 11 | interactive UI. 12 | 13 | Queries 14 | The format for queries is simplistic but does support most of the 15 | basic comparison operators. 16 | 17 | The query line accepts a list of space separated patterns and 18 | commands. Each pattern is boolean AND joined to all other patterns 19 | entered. All patterns will use the same matching modes. 20 | 21 | Pattern modes are "regex", "like" or "eq". Additionally there is 22 | case sensitive mode and inverted mode ("not"). Modes are switched by 23 | their corresponding key binding or by parameters on Select-Poco. 24 | 25 | Commands between with ":" and change the following patterns to match 26 | against the object property that matches the name in the command until 27 | another command is entered. For example, ":name foo" will match "foo" 28 | against the values in the "Name" property of all input objects. 29 | 30 | FEEDBACK 31 | Please submit suggestions, bug reports or questions to the poco 32 | website. 33 | 34 | https://github.com/jasonmarcher/poco 35 | -------------------------------------------------------------------------------- /docs/demos/poco-ps.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmarcher/poco/0ba3b3d58a8c08435e1ffa2cd37e96ef17a38e61/docs/demos/poco-ps.gif -------------------------------------------------------------------------------- /psakefile.ps1: -------------------------------------------------------------------------------- 1 | Properties { 2 | $SrcDirectory = "$PSScriptRoot/src/ps1" 3 | $HelpDirectory = "$PSScriptRoot/docs" 4 | $OutputDirectory = "$PSScriptRoot/build" 5 | $ReportDirectory = "$PSSCriptRoot/reports" 6 | $DeployDirectory = "$HOME/Documents/WindowsPowerShell/Modules/poco" 7 | } 8 | 9 | Task 'default' -Depends build 10 | 11 | Task 'clean' { 12 | if (Test-Path $OutputDirectory) { 13 | Remove-Item $OutputDirectory -Recurse -Force 14 | } 15 | } 16 | 17 | Task 'docs' { 18 | New-Item $OutputDirectory -ItemType Directory -ErrorAction SilentlyContinue > $null 19 | 20 | New-ExternalHelp -Path $HelpDirectory -OutputPath $OutputDirectory 21 | 22 | ## Copy about topics 23 | Copy-Item "$HelpDirectory/*" -Include "about_*.txt" -Destination $OutputDirectory -Force 24 | } 25 | 26 | Task 'assemble' -Depends clean { 27 | New-Item $OutputDirectory -ItemType Directory -ErrorAction SilentlyContinue > $null 28 | 29 | ## Copy manifest 30 | Copy-Item "$SrcDirectory/poco.psd1" -Destination $OutputDirectory -Force 31 | 32 | ## Build module 33 | $ModuleContent = Get-Content "$SrcDirectory/poco.psm1" 34 | foreach ($script in (Get-ChildItem $SrcDirectory -Include *.ps1 -Recurse)) { 35 | $ModuleContent += Get-Content $script.FullName 36 | } 37 | Set-Content "$OutputDirectory/poco.psm1" -Value $ModuleContent -Encoding UTF8 -Force 38 | } 39 | 40 | Task 'build' -Depends assemble, docs 41 | 42 | Task 'deploy' -Depends build { 43 | if (Test-Path $DeployDirectory) { 44 | Remove-Item $DeployDirectory -Recurse -Force 45 | } 46 | 47 | New-Item $DeployDirectory -ItemType Directory -ErrorAction SilentlyContinue > $null 48 | 49 | Copy-Item "$OutputDirectory/*" -Destination $DeployDirectory -Force 50 | } 51 | 52 | Task 'checkStyle' -Depends build { 53 | New-Item $ReportDirectory -ItemType Directory -ErrorAction SilentlyContinue > $null 54 | 55 | $FilesToCheck = Get-ChildItem $SrcDirectory -Include *.ps1 -Recurse 56 | $FilesToCheck += Get-ChildItem $OutputDirectory -Include *.psd1 -Recurse 57 | 58 | $Results = $FilesToCheck | ForEach-Object {Invoke-ScriptAnalyzer $_.FullName} 59 | $Results | Where-Object Severity -eq "Error" | Format-Table | Out-String | Write-Host -ForegroundColor Red 60 | $Results | Select-Object RuleName,Severity,Line,Column,ScriptName,Message | ConvertTo-Csv -NoTypeInformation | Set-Content "$ReportDirectory/checkstyle.csv" -Force 61 | } 62 | 63 | # Task 'test' { 64 | # New-Item $ReportDirectory -ItemType Directory -ErrorAction SilentlyContinue > $null 65 | 66 | # Invoke-Pester ` 67 | # -OutputFile "$ReportDirectory/tests_unit.xml" -CodeCoverage "$SrcDirectory/*.ps1" -CodeCoverageOutputFile "$ReportDirectory/coverage.xml" 68 | # } -------------------------------------------------------------------------------- /src/ps1/poco.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ## Module Info 3 | ModuleVersion = '1.2.0' 4 | Description = "Interactive filtering command based on peco" 5 | GUID = 'fcdcab47-0505-4ba8-b845-538effc1d88e' 6 | # HelpInfoURI = '' 7 | 8 | ## Module Components 9 | RootModule = @("poco.psm1") 10 | ScriptsToProcess = @() 11 | TypesToProcess = @() 12 | FormatsToProcess = @() 13 | FileList = @() 14 | 15 | ## Public Interface 16 | CmdletsToExport = '' 17 | FunctionsToExport = @("Select-Poco") 18 | VariablesToExport = @() 19 | AliasesToExport = @("poco") 20 | # DscResourcesToExport = @() 21 | # DefaultCommandPrefix = '' 22 | 23 | ## Requirements 24 | # CompatiblePSEditions = @() 25 | PowerShellVersion = '2.0' 26 | # PowerShellHostName = '' 27 | # PowerShellHostVersion = '' 28 | RequiredModules = @() 29 | RequiredAssemblies = @() 30 | ProcessorArchitecture = 'None' 31 | DotNetFrameworkVersion = '3.5' 32 | CLRVersion = '3.5' 33 | 34 | ## Author 35 | Author = 'yumura' 36 | CompanyName = '' 37 | Copyright = '' 38 | 39 | ## Private Data 40 | PrivateData = @{ 41 | PSData = @{ 42 | # Tags applied to this module. These help with module discovery in online galleries. 43 | Tags = @("productivity","filter","pipeline") 44 | 45 | # A URL to the license for this module. 46 | # LicenseUri = '' 47 | 48 | # A URL to the main website for this project. 49 | ProjectUri = 'https://github.com/jasonmarcher/poco' 50 | 51 | # A URL to an icon representing this module. 52 | # IconUri = '' 53 | 54 | # ReleaseNotes of this module 55 | ReleaseNotes = @" 56 | ## 2019-07-19 - Version 1.2.0 - Jason Archer 57 | 58 | Features: 59 | 60 | - Added about_poco help topic 61 | 62 | ## 2018-07-19 - Version 1.1.0 - Jason Archer 63 | 64 | Features: 65 | 66 | - Improved performance and responsiveness 67 | - Faster code 68 | - Batch processing of key presses 69 | - Only filter object list when necessary 70 | - Filter object list with LINQ 71 | 72 | ## 2016-02-19 - Version 1.0.0 - yumura 73 | 74 | A peco implementation in PowerShell 75 | 76 | "@ 77 | } # End of PSData hashtable 78 | } # End of PrivateData hashtable 79 | } 80 | -------------------------------------------------------------------------------- /src/ps1/poco.psm1: -------------------------------------------------------------------------------- 1 | # Requirements 2 | if ($Host.Name -eq "Windows PowerShell ISE Host") { 3 | throw "poco is not compatible with Windows PowerShell ISE." 4 | 5 | ## TODO: Add a more specific test for the ability to modify the console output buffer which is the real compatibility issue 6 | } 7 | 8 | # .ExternalHelp poco-help.xml 9 | function Select-Poco { 10 | param( 11 | [Object[]]$Property = $null 12 | , 13 | [string]$Query = '' 14 | , 15 | [ValidateSet('match', 'like', 'eq')] 16 | [string]$Filter = 'match' 17 | , 18 | [switch]$CaseSensitive = $false 19 | , 20 | [switch]$InvertFilter = $false 21 | , 22 | [string]$Prompt = 'Query' 23 | , 24 | [ValidateSet('TopDown', 'BottomUp')] 25 | [string]$Layout = 'TopDown' 26 | , 27 | [HashTable]$Keymaps = (New-PocoKeymaps) 28 | ) 29 | 30 | begin { 31 | $Items = [System.Collections.ArrayList]::new() 32 | } 33 | 34 | process { 35 | $null = $Items.Add($_) 36 | } 37 | 38 | end { 39 | try { 40 | $config = New-Config $Items $Property $Prompt $Layout $Keymaps # immutable 41 | $state = New-State $Query $Filter $CaseSensitive $InvertFilter $config # mutable 42 | 43 | Backup-ScrBuf 44 | Clear-Host 45 | 46 | $action = 'None' 47 | while ($action -ne 'Cancel' -and $action -ne 'Finish') { 48 | Write-Screen $state $config 49 | 50 | $OldQuery = $State.Query -replace '(:\w+\s*)$|(\s+)$' 51 | 52 | do { 53 | $key, $keystr = Get-PocoKey 54 | $action = Get-Action $config $keystr 55 | $state = Update-State $state $config $action $key 56 | } while ([console]::KeyAvailable) 57 | 58 | if ($OldQuery -ne ($State.Query -replace '(:\w+\s*)$|(\s+)$')) { 59 | $state.Entry = Get-Entry $state $config 60 | } 61 | } 62 | 63 | Restore-ScrBuf 64 | if ($action -eq 'Finish') {$state.Entry} 65 | } catch { 66 | Restore-ScrBuf 67 | } 68 | } 69 | } 70 | 71 | Set-Alias poco Select-Poco 72 | 73 | Export-ModuleMember -Function "Select-Poco" -Alias "poco" 74 | -------------------------------------------------------------------------------- /src/ps1/poco_init.ps1: -------------------------------------------------------------------------------- 1 | function New-Config ($Items, $Property, $Prompt, $Layout, $Keymaps) { 2 | @{ 3 | 'Input' = $Items 4 | 'Property' = $Property 5 | 'Prompt' = $Prompt 6 | 'Layout' = $Layout 7 | 'Keymaps' = $Keymaps 8 | } 9 | } 10 | 11 | function New-State ($Query, $Filter, $CaseSensitive, $InvertFilter, $Config) { 12 | $state = @{ 13 | 'Query' = $Query 14 | 'Filter' = $Filter 15 | 'CaseSensitive' = $CaseSensitive 16 | 'InvertFilter' = $InvertFilter 17 | 'Acrion' = 'Identity' 18 | 'Entry' = @() 19 | 'Properties' = @() 20 | 'PrevLength' = 0 21 | 'Screen' = @{ 22 | 'Prompt' = '' 23 | 'FilterType' = '' 24 | 'QueryX' = 0 25 | 'X' = 0 26 | 'RawUI' = Get-RawUI 27 | # 'Y' = 1 # 選択機能があれば... 28 | } 29 | } 30 | 31 | $state.Screen.Prompt = Get-Prompt $state $config 32 | $state.Screen.FilterType = Get-FilterType $state 33 | $state.Screen.QueryX = $Query.Length 34 | $state.Screen.X = $state.Screen.Prompt.Length 35 | 36 | $state.Entry = Get-Entry $state $config 37 | 38 | $FirstItem = $config.Input[0] 39 | if ($FirstItem) { 40 | $state.Properties += $FirstItem.PSObject.Properties 41 | } 42 | 43 | $state 44 | } 45 | -------------------------------------------------------------------------------- /src/ps1/poco_key.ps1: -------------------------------------------------------------------------------- 1 | function New-PocoKeymaps { 2 | @{ 3 | 'Escape' = 'Cancel' 4 | 'Control+C' = 'Cancel' 5 | 'Enter' = 'Finish' 6 | 'Alt+B' = 'BackwardChar' 7 | 'Alt+F' = 'ForwardChar' 8 | 'Alt+A' = 'BeginningOfLine' 9 | 'Alt+E' = 'EndOfLine' 10 | 'Alt+D8' = 'DeleteBackwardChar' 11 | 'Backspace' = 'DeleteBackwardChar' 12 | 'Alt+D' = 'DeleteForwardChar' 13 | 'Delete' = 'DeleteForwardChar' 14 | 'Alt+U' = 'KillBeginningOfLine' 15 | 'Alt+K' = 'KillEndOfLine' 16 | 'Alt+R' = 'RotateMatcher' 17 | 'Alt+C' = 'ToggleCaseSensitive' 18 | 'Alt+I' = 'ToggleInvertFilter' 19 | 20 | 'Alt+W' = 'DeleteBackwardWord' 21 | 'Alt+N' = 'SelectUp' 22 | 'Alt+P' = 'SelectDown' 23 | 'Control+Spacebar' = 'ToggleSelectionAndSelectNext' 24 | 'UpArrow' = 'SelectUp' 25 | 'DownArrow' = 'SelectDown' 26 | 'RightArrow' = 'ScrollPageUp' 27 | 'LeftArrow' = 'ScrollPageDown' 28 | 'Tab' = 'TabExpansion' 29 | } 30 | } 31 | 32 | function Get-PocoKey { 33 | $flag = [console]::TreatControlCAsInput 34 | [console]::TreatControlCAsInput = $true 35 | 36 | $Key = [console]::ReadKey($true) 37 | 38 | [console]::TreatControlCAsInput = $flag 39 | 40 | $KeyString = $Key.Key.ToString() 41 | if ($Key.Modifiers -ne 0) { 42 | $m = $Key.Modifiers.ToString() -replace ', ','+' 43 | $KeyString = "${m}+${KeyString}" 44 | } 45 | 46 | return $Key, $KeyString 47 | } 48 | 49 | function Get-Action ($config, $keystr) { 50 | if ($config.Keymaps.Contains($keystr)) { 51 | return $config.Keymaps[$keystr] 52 | } 53 | 54 | if ($keystr -notmatch 'Alt|Control') { 55 | 'AddChar' 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ps1/poco_query.ps1: -------------------------------------------------------------------------------- 1 | function Convert-QueryHash ($state) { 2 | $property = '' 3 | $hash = @{$property = @()} 4 | 5 | $state.Query -split ' ' | Where-Object {$_ -ne ''} | ForEach-Object { 6 | $token = $_ 7 | 8 | if ($token.StartsWith(':')) { 9 | $property = Resolve-PropertyName $state $token.Remove(0, 1) 10 | if (-not $hash.Contains($property)) { 11 | $hash[$property] = @() 12 | } 13 | } else { 14 | $hash[$property] += $token 15 | } 16 | } 17 | 18 | $hash 19 | } 20 | 21 | function Select-ByQuery { 22 | param( 23 | $State 24 | , 25 | [object[]] $Objects 26 | ) 27 | 28 | begin { 29 | [Func[Object,bool]]$Delegate = New-QueryDelegate $State 30 | } 31 | 32 | end { 33 | [Linq.Enumerable]::Where($Objects, $Delegate) 34 | } 35 | } 36 | 37 | function New-QueryDelegate { 38 | param( 39 | $State 40 | ) 41 | 42 | $DelegateString = [System.Text.StringBuilder]::new() 43 | 44 | $DelegateString.Append('param($Object); ') > $null 45 | 46 | $MatchType = switch ($State.Screen.FilterType) { 47 | 'match' {"-match"; break} 48 | 'like' {"-like"; break} 49 | 'eq' {"-eq"; break} 50 | 51 | 'notmatch' {"-notmatch"; break} 52 | 'notlike' {"-notlike"; break} 53 | 'neq' {"-ne"; break} 54 | 55 | 'cmatch' {"-cmatch"; break} 56 | 'clike' {"-clike"; break} 57 | 'ceq' {"-ceq"; break} 58 | 59 | 'cnotmatch' {"-cnotmatch"; break} 60 | 'cnotlike' {"-cnotlike"; break} 61 | 'cneq' {"-cne"; break} 62 | } 63 | 64 | $HashQuery = Convert-QueryHash $State 65 | 66 | ## This search always treats multiple conditions as boolean AND joined 67 | 68 | ## String representation search 69 | if ($HashQuery.Contains('')) { 70 | foreach ($Value in $HashQuery['']) { 71 | $MatchLine = "`$Object $MatchType '$Value'" 72 | $DelegateString.Append("if (($MatchLine) -eq `$false) {return `$false}; ") > $null 73 | } 74 | } 75 | 76 | ## Property value search 77 | foreach ($Property in $HashQuery.Keys) { 78 | if ($Property -eq '') {continue} 79 | 80 | foreach ($Value in $HashQuery[$Property]) { 81 | $MatchLine = "`$Object.$Property $MatchType '$Value'" 82 | $DelegateString.Append("if (($MatchLine) -eq `$false) {return `$false}; ") > $null 83 | } 84 | } 85 | 86 | ## Object passed all tests, so it passes the filter 87 | $DelegateString.Append('return $true') > $null 88 | 89 | [Scriptblock]::Create($DelegateString.ToString()) 90 | } 91 | 92 | function Resolve-PropertyName { 93 | param ( 94 | $State 95 | , 96 | $Alias 97 | ) 98 | 99 | ## TODO: This algorithm needs to be optimized 100 | 101 | $Properties = $State.Properties | Select-Object -ExpandProperty Name | Sort-Object 102 | foreach ($Property in $Properties) { 103 | if ($Property -like "$Alias*") { 104 | return $Property 105 | } 106 | } 107 | 108 | return "" 109 | } -------------------------------------------------------------------------------- /src/ps1/poco_scrbuf.ps1: -------------------------------------------------------------------------------- 1 | # http://d.hatena.ne.jp/newpops/20080514 2 | # スクリーンバッファのバックアップ 3 | function Backup-ScrBuf { 4 | $rui = Get-RawUI 5 | 6 | $rect = New-Object System.Management.Automation.Host.Rectangle 7 | $rect.Left = 0 8 | $rect.Top = 0 9 | $rect.Right = $rui.WindowSize.Width 10 | $rect.Bottom = $rui.CursorPosition.Y 11 | $script:screen = $rui.GetBufferContents($rect) 12 | } 13 | 14 | # http://d.hatena.ne.jp/newpops/20080515 15 | # スクリーンバッファのリストア 16 | function Restore-ScrBuf { 17 | Clear-Host 18 | 19 | if (-not (Test-Path 'variable:screen')) {return} 20 | 21 | $rui = Get-RawUI 22 | $origin = New-Object System.Management.Automation.Host.Coordinates(0, 0) 23 | $rui.SetBufferContents($origin, $script:screen) 24 | $pos = New-Object System.Management.Automation.Host.Coordinates(0, $script:screen.GetUpperBound(0)) 25 | $rui.CursorPosition = $pos 26 | } -------------------------------------------------------------------------------- /src/ps1/poco_screen.ps1: -------------------------------------------------------------------------------- 1 | function Write-Screen ($state, $config) { 2 | switch ($config.Layout) { 3 | 'TopDown' {Write-TopDown $state $config} 4 | 'BottomUp' {Write-BottomUp $state $config} 5 | } 6 | } 7 | 8 | function Write-TopDown ($state, $config) { 9 | Write-ScreenLine $state 0 $state.Screen.Prompt 10 | Write-RightInfo 0 $state 11 | 12 | if ($state.Entry.length -ne $state.PrevLength) { 13 | $h = $state.Screen.RawUI.WindowSize.Height 14 | $entries = $state.Entry | Format-Table | Out-String -Stream | Select-Object -First ($h - 1) 15 | if ($entries -is [string]) {$entries = ,@($entries)} 16 | foreach ($i in 0..($h - 2)) { 17 | $line = if ($i -lt $entries.length) {$entries[$i]} else {''} 18 | Write-ScreenLine $state ($i + 1) $line 19 | } 20 | 21 | $state.PrevLength = $state.Entry.length 22 | } 23 | 24 | $x = Convert-CursorPositionX $state 25 | Set-CursorPosition $state $x 0 26 | } 27 | 28 | function Write-BottomUp ($state, $config, $entries) { 29 | if ($state.Entry.length -ne $state.PrevLength) { 30 | $h = $state.Screen.RawUI.WindowSize.Height 31 | $entries = $state.Entry | Format-Table | Out-String -Stream | Select-Object -First ($h - 1) 32 | if ($entries -is [string]) {$entries = ,@($entries)} 33 | foreach ($i in 0..($h - 2)) { 34 | $line = if ($i -lt $entries.length) {$entries[$i]} else {''} 35 | Write-ScreenLine $state $i $line 36 | } 37 | 38 | $state.PrevLength = $state.Entry.length 39 | } 40 | 41 | Write-ScreenLine $state ($h - 1) $state.Screen.Prompt 42 | Write-RightInfo ($h - 1) $state 43 | 44 | $x = Convert-CursorPositionX $state 45 | $y = $state.Screen.RawUI.CursorPosition.Y 46 | Set-CursorPosition $state $x $y 47 | } 48 | 49 | function Write-ScreenLine ($state, $i, $line) { 50 | $w = $state.Screen.RawUI.BufferSize.Width 51 | Set-CursorPosition $state 0 $i 52 | Write-Host ([string]$line).PadRight($w) -NoNewline ## TODO: replace 53 | } 54 | 55 | function Write-RightInfo ($i, $state) { 56 | $f = $state.Screen.FilterType 57 | $n = $state.Entry.Length 58 | $w = $state.Screen.RawUI.WindowSize.Width 59 | # $h = $state.Screen.RawUI.WindowSize.Height 60 | 61 | $info = "${f} [${n}]" 62 | Set-CursorPosition $state ($w - $info.length) $i 63 | Write-Host $info -NoNewline ## TODO: replace 64 | } 65 | 66 | function Convert-CursorPositionX ($state) { 67 | $str = $state.Screen.Prompt.Substring(0, $state.Screen.X) 68 | $state.Screen.RawUI.LengthInBufferCells($str) 69 | } 70 | 71 | function Set-CursorPosition ($state, $x, $y) { 72 | $pos = [System.Management.Automation.Host.Coordinates]::new($x, $y) 73 | $state.Screen.RawUI.CursorPosition = $pos 74 | } 75 | -------------------------------------------------------------------------------- /src/ps1/poco_update_state.ps1: -------------------------------------------------------------------------------- 1 | function Update-State ($state, $config, $action, $key) { 2 | switch ($action) { 3 | 'AddChar' {Add-Char $state $config $key.KeyChar} 4 | 'ForwardChar' {Move-ForwardChar $state} 5 | 'BackwardChar' {Move-BackwardChar $state} 6 | 'BeginningOfLine' {Move-BeginningOfLine $state} 7 | 'EndOfLine' {Move-EndOfLine $state} 8 | 'DeleteBackwardChar' {Remove-BackwardChar $state} 9 | 'DeleteForwardChar' {Remove-ForwardChar $state} 10 | 'KillBeginningOfLine' {Remove-HeadLine $state} 11 | 'KillEndOfLine' {Remove-TailLine $state} 12 | 'RotateMatcher' {Select-Matcher $state} 13 | 'ToggleCaseSensitive' {Switch-CaseSensitive $state} 14 | 'ToggleInvertFilter' {Switch-InvertFilter $state} 15 | 16 | default {} # None, Cancel, Finish = identity 17 | } 18 | 19 | $state 20 | } 21 | 22 | function Add-Char ($state, $config, $char) { 23 | $x = $state.Screen.QueryX 24 | $q = $state.Query 25 | 26 | $state.Query = $q.Insert($x, $char) 27 | $state.Screen.QueryX++ 28 | $state.Screen.X++ 29 | 30 | $state.Screen.Prompt = Get-Prompt $state $config 31 | } 32 | 33 | function Move-BackwardChar ($state) { 34 | $x = $state.Screen.QueryX 35 | if ($x - 1 -ge 0) { 36 | $state.Screen.QueryX-- 37 | $state.Screen.X-- 38 | } 39 | } 40 | 41 | function Move-ForwardChar ($state) { 42 | $x = $state.Screen.X 43 | $l = $state.Screen.Prompt.length 44 | 45 | if ($x + 1 -le $l) { 46 | $state.Screen.QueryX++ 47 | $state.Screen.X++ 48 | } 49 | } 50 | 51 | function Move-BeginningOfLine ($state) { 52 | $state.Screen.X -= $state.Screen.QueryX 53 | $state.Screen.QueryX = 0 54 | } 55 | 56 | function Move-EndOfLine ($state) { 57 | $state.Screen.QueryX = $state.Query.length 58 | $state.Screen.X = $state.Screen.Prompt.length 59 | } 60 | 61 | function Remove-BackwardChar ($state) { 62 | $x = $state.Screen.QueryX 63 | $q = $state.Query 64 | 65 | if ($x - 1 -ge 0) { 66 | $state.Query = $q.Remove($x - 1, 1) 67 | $state.Screen.QueryX-- 68 | $state.Screen.X-- 69 | 70 | $state.Screen.Prompt = Get-Prompt $state $config 71 | } 72 | } 73 | 74 | function Remove-ForwardChar ($state) { 75 | $x = $state.Screen.X 76 | $l = $state.Screen.Prompt.length 77 | 78 | $qx = $state.Screen.QueryX 79 | $q = $state.Query 80 | 81 | if ($x + 1 -le $l) { 82 | $state.Query = $q.Remove($qx, 1) 83 | $state.Screen.Prompt = Get-Prompt $state $config 84 | } 85 | } 86 | 87 | function Remove-HeadLine ($state) { 88 | while ($state.Screen.QueryX -gt 0) { 89 | Remove-BackwardChar ($state) 90 | } 91 | } 92 | 93 | function Remove-TailLine ($state) { 94 | while ($state.Screen.QueryX -lt $state.Query.length) { 95 | Remove-ForwardChar ($state) 96 | } 97 | } 98 | 99 | function Select-Matcher ($state) { 100 | $arr = @('match', 'like', 'eq') 101 | 102 | $n = $arr.length 103 | $i = $arr.IndexOf($state.Filter) + 1 104 | 105 | $state.Filter = $arr[$i % $n] 106 | 107 | $state.Screen.FilterType = Get-FilterType $state 108 | } 109 | 110 | function Switch-CaseSensitive ($state) { 111 | $state.CaseSensitive = -not $state.CaseSensitive 112 | $state.Screen.FilterType = Get-FilterType $state 113 | } 114 | 115 | function Switch-InvertFilter ($state) { 116 | $state.InvertFilter = -not $state.InvertFilter 117 | $state.Screen.FilterType = Get-FilterType $state 118 | } 119 | -------------------------------------------------------------------------------- /src/ps1/poco_util.ps1: -------------------------------------------------------------------------------- 1 | function Get-RawUI {(Get-Host).UI.RawUI} 2 | 3 | function Get-Prompt ($state, $config) { 4 | $config.Prompt + '> ' + $state.Query 5 | } 6 | 7 | function Get-FilterType ($state) { 8 | $type = '' 9 | if ($state.CaseSensitive) {$type += 'c'} 10 | if ($state.InvertFilter) {$type += 'not'} 11 | $type += $state.Filter 12 | 13 | $type -replace 'noteq', 'neq' 14 | } 15 | 16 | function Get-Entry ($state, $config) { 17 | Select-ByQuery $State $Config.Input 18 | } --------------------------------------------------------------------------------