This usually works just fine, but it has a couple of drawbacks. The main problem is that it shows a diff for every modified file, including project files. Sometimes, even if the majority of the files are source files, my code review really only concerns a subset of the files that I have modified, but my loop above dutifully runs WinMerge for every file, forcing my colleagues to waste precious seconds while I dismiss the files they aren't interested in.
Enter the Select-Multiple script. Objects piped into Select-Multiple are displayed in a kind of selection window with a "check box" to the left of each object. The list can be navigated with the arrow keys and options can be selected with the space bar. After the list is dismissed with the enter key, the selected objects are passed through to the pipeline. Here's what it looks like in action:
If you want to display a certain property of the objects passed in to Select-Multiple, you can use the Property parameter:
Even if you use the Property parameter, the complete object is passed through the pipeline. The above example would stop the Internet Explorer process.
There are a few keyboard shortcuts to aid in selecting objects. Page up and page down will navigate long lists a page at a time. Control-A will select everything, and Control-T will toggle the selection. Holding down shift while using the up and down arrow keys will select while moving. Escape will dismiss the window without selecting anything.
Here is the complete script:
#
# Objects piped into Select-Multiple are displayed in a kind of
# selection window with a "check box" to the left of each object.
#
# The list can be navigated with the arrow keys and options
# can be selected with the space bar.
#
# If the Property parameter is provided, the value of that
# property is displayed in the list, rather than the string
# representation of the object.
#
# After the list is dismissed with the enter key, the selected
# objects are passed through to the pipeline.
#
# Keyboard operations:
# Up/Down - navigate
# Space - select current option
# Page Up/Down - navigate long lists a page at a time
# Ctrl-A - select all
# Ctrl-T - toggle selection
# Shift-Up/Down - select and move
# Escape - exit with no objects selected
# configuration
$script:ForegroundColor = $Host.UI.RawUI.ForegroundColor
$script:BackgroundColor = $Host.UI.RawUI.BackgroundColor
$script:BorderForegroundColor = $Host.UI.RawUI.BackgroundColor
$script:BorderBackgroundColor = $Host.UI.RawUI.ForegroundColor
$script:SelectedForegroundColor = [ConsoleColor]"White"
$script:SelectedBackgroundColor = [ConsoleColor]"Red"
$script:StatusFormatString = "[X]: {0}/{1}"
function Select-Multiple( [string] $Property )
{
begin
{
$options = @()
# leave room for the border and "check box"
$maxLength = $Host.UI.RawUI.WindowSize.Width - 2 - 6;
function Make-Option( [object] $object )
{
$object | Select-Object `
@{ Name = "Selected"; Expression = { $false } },
@{ Name = "Object"; Expression = { $object } },
@{ Name = "String";
Expression = {
$string = ""
if ( $Property )
{
$string = ( Invoke-Expression "`$object.$Property" ).ToString()
}
else
{
$string = $object.ToString()
}
if ( $string.Length -gt $maxLength )
{
# add elipsis...
$string =
$string.SubString( 0, $maxLength - 3 ).PadRight( $maxLength, '.' )
}
$string
} }
}
}
process
{
if ( $_ )
{
$options += Make-Option $_
}
}
end
{
if ( $options.Length -gt 0 )
{
Show-Options $options
foreach ( $option in $options )
{
if ( $option.Selected )
{
$option.Object
}
}
}
}
}
function Show-Options( [object[]] $options )
{
begin
{
function Get-KeyboardInput( $state )
{
$keyOptions = [System.Management.Automation.Host.ReadKeyOptions] `
"NoEcho, IncludeKeyDown, IncludeKeyUp"
$done = $false
while ( $Host.UI.RawUI.KeyAvailable )
{
$key = $Host.UI.RawUI.ReadKey( $keyOptions )
if ( $key.KeyDown )
{
$shiftPressed = $key.ControlKeyState.ToString() -match "ShiftPressed"
$controlPressed = $key.ControlKeyState.ToString() -match "CtrlPressed"
switch ( $key.VirtualKeyCode )
{
38 { # up arrow
if ( $state.SelectedIndex -gt 0 )
{
Move-Up $state $shiftPressed
}
break
}
40 { # down arrow
if ( $state.SelectedIndex -lt $state.TotalOptions - 1 )
{
Move-Down $state $shiftPressed
}
break
}
33 { # page up
Page-Up $state
break
}
34 { # page down
Page-down $state
break
}
65 { # 'a' - select all
if ( $controlPressed )
{
Select-All $state
}
break
}
84 { # 't' - toggle selection
if ( $controlPressed )
{
Toggle-Selection $state
}
break
}
32 { # space - toggle current option
Select-Current $state $true
}
13 { # enter
$done = $true
}
27 { # escape
foreach ( $option in $state.Options )
{
$option.Selected = $false
}
$done = $true
}
}
}
}
$done
}
function Move-Up( $state, $shiftPressed )
{
if ( $shiftPressed )
{
Select-Current $state $false
}
Set-SelectionColors `
$state `
$script:ForegroundColor `
$script:BackgroundColor
if ( $state.SelectedIndex - $state.Offset -eq 0 )
{
--$state.Offset
Draw-Options $state
}
--$state.SelectedIndex
Set-SelectionColors `
$state `
$script:SelectedForegroundColor `
$script:SelectedBackgroundColor
if ( $shiftPressed )
{
Select-Current $state $false
}
}
function Move-Down( $state, $shiftPressed )
{
if ( $shiftPressed )
{
Select-Current $state $false
}
Set-SelectionColors `
$state `
$script:ForegroundColor `
$script:BackgroundColor
if ( $state.SelectedIndex - $state.Offset -eq $state.MaxOptions )
{
++$state.Offset
Draw-Options $state
}
++$state.SelectedIndex
Set-SelectionColors `
$state `
$script:SelectedForegroundColor `
$script:SelectedBackgroundColor
if ( $shiftPressed )
{
Select-Current $state $false
}
}
function Page-Up( $state )
{
Set-SelectionColors `
$state `
$script:ForegroundColor `
$script:BackgroundColor
if ( $state.SelectedIndex -eq $state.Offset )
{
$state.Offset -= $state.MaxOptions
$state.Offset = [Math]::Max( $state.Offset, 0 )
}
$state.SelectedIndex = $state.Offset
Draw-Options $state
Set-SelectionColors `
$state `
$script:SelectedForegroundColor `
$script:SelectedBackgroundColor
}
function Page-Down( $state )
{
Set-SelectionColors `
$state `
$script:ForegroundColor `
$script:BackgroundColor
if ( $state.SelectedIndex -eq $state.MaxOptions + $state.Offset )
{
$state.Offset += $state.MaxOptions
$state.Offset = [Math]::Min(
$state.Offset,
$state.TotalOptions - $state.MaxOptions - 1 )
}
$state.SelectedIndex = $state.MaxOptions + $state.Offset
$state.SelectedIndex = [Math]::Min(
$state.SelectedIndex,
$state.TotalOptions - 1 )
Draw-Options $state
Set-SelectionColors `
$state `
$script:SelectedForegroundColor `
$script:SelectedBackgroundColor
}
function Select-All( $state )
{
foreach ( $option in $state.Options )
{
$option.Selected = $true
}
$state.SelectedOptions = $state.TotalOptions
Draw-Options $state
Set-SelectionColors `
$state `
$script:SelectedForegroundColor `
$script:SelectedBackgroundColor
}
function Toggle-Selection( $state )
{
$state.SelectedOptions = 0
foreach ( $option in $state.Options )
{
$option.Selected = -not $option.Selected
if ( $option.Selected )
{
++$state.SelectedOptions
}
}
Draw-Options $state
Set-SelectionColors `
$state `
$script:SelectedForegroundColor `
$script:SelectedBackgroundColor
}
function Select-Current( $state, $toggleSelection )
{
$option = $state.Options[ $state.SelectedIndex ]
if ( $toggleSelection )
{
$option.Selected = -not $option.Selected
if ( $option.Selected )
{
++$state.SelectedOptions
}
else
{
--$state.SelectedOptions
}
}
else
{
if ( -not $option.Selected )
{
++$state.SelectedOptions
}
$option.Selected = $true
}
Draw-Options $state
Set-SelectionColors `
$state `
$script:SelectedForegroundColor `
$script:SelectedBackgroundColor
}
function Draw-Options( $state )
{
$optionStrings = @( $state.Options )[
$state.Offset ..
( $state.MaxOptions + $state.Offset ) ] | Foreach-Object {
if ( $_.Selected )
{
" [X] $($_.String) ".PadRight( $state.Width )
}
else
{
" [ ] $($_.String) ".PadRight( $state.Width )
}
}
$status = ( $script:StatusFormatString -f
$state.SelectedOptions, $state.TotalOptions ).PadRight( $state.Width )
$statusBuffer = $Host.UI.RawUI.NewBufferCellArray(
@( $status ),
$script:BorderForegroundColor, $script:BorderBackgroundColor )
$Host.UI.RawUI.SetBufferContents( $state.StatusPosition, $statusBuffer )
$optionsBuffer = $Host.UI.RawUI.NewBufferCellArray(
@( $optionStrings ),
$script:ForegroundColor, $script:BackgroundColor )
$Host.UI.RawUI.SetBufferContents( $state.OptionsPosition, $optionsBuffer )
}
function Set-SelectionColors( $state, $foregroundColor, $backgroundColor )
{
$position = $state.OptionsPosition
$position.Y += $state.SelectedIndex - $state.Offset
$rectangle = New-Object System.Management.Automation.Host.Rectangle `
$position.X, $position.Y, ( $position.X + $state.Width - 1 ), $position.Y
$optionBuffer = $Host.UI.RawUI.GetBufferContents( $rectangle )
$contents = @( [string]::Join( "",
( $optionBuffer | ForEach-Object { $_.Character } ) ) )
$optionBuffer = $Host.UI.RawUI.NewBufferCellArray(
$contents,
$foregroundColor,
$backgroundColor )
$Host.UI.RawUI.SetBufferContents( $position, $optionBuffer )
}
}
end
{
$width = @( $options | Foreach-Object { $_.String } |
Sort-Object Length -Descending )[ 0 ].Length + 4
$width = [Math]::Max(
$width,
( $script:StatusFormatString -f $options.Length, $options.Length ).Length - 2 )
$height = $options.Length
# create border
$windowPosition = $Host.UI.RawUI.WindowPosition
$windowSize = $Host.UI.RawUI.WindowSize
$cursorPosition = $Host.UI.RawUI.CursorPosition
$center = [Math]::Truncate( [float]$windowSize.Height / 2 )
$cursorOffset = $cursorPosition.Y - $windowPosition.Y
$cursorOffsetBottom = $windowSize.Height - $cursorOffset
$optionsBoxWidth = $width + 2
$optionsBoxHeight = $height + 2
$x = $cursor.X
if ( ( $cursorOffset -gt $center ) -and ( $height -ge $cursorOffsetBottom ) )
{
$maxHeight = $cursorOffset
if ( $maxHeight -lt $optionsBoxHeight )
{
$optionsBoxHeight = $maxHeight
}
$y = $cursorOffset - $optionsBoxHeight
}
else
{
$maxHeight = $cursorOffsetBottom - 1
if ( $maxHeight -lt $optionsBoxHeight )
{
$optionsBoxHeight = $maxHeight
}
$y = $cursorOffSet + 1
}
$borderStrings = 1 .. ( $optionsBoxHeight ) | ForEach-Object {
" " * ( $optionsBoxWidth + 2 )
}
$borderBuffer = $Host.UI.RawUI.NewBufferCellArray(
$borderStrings,
$script:BorderForegroundColor,
$script:BorderBackgroundColor )
$borderPosition = $Host.UI.RawUI.WindowPosition
$borderPosition.X += $x
$borderPosition.Y += $y
$borderTop = $borderBottom = $borderPosition
$borderBottom.X += ( $borderBuffer.GetUpperBound( 1 ) )
$borderBottom.Y += ( $borderBuffer.GetUpperBound( 0 ) )
$borderRectangle =
New-Object System.Management.Automation.Host.Rectangle $borderTop, $borderBottom
$oldContents = $Host.UI.RawUI.GetBufferContents( $borderRectangle )
$Host.UI.RawUI.SetBufferContents( $borderTop, $borderBuffer )
# initialize state object
$statusPosition = $borderPosition
$statusPosition.X += 1
$statusPosition.Y += $optionsBoxHeight - 1
$optionsPosition = $borderPosition
$optionsPosition.X += 1
$optionsPosition.Y += 1
$state = "" | Select-Object `
@{ Name = "Options"; Expression = { ,$options } },
@{ Name = "SelectedIndex"; Expression = { 0 } },
@{ Name = "Offset"; Expression = { 0 } },
@{ Name = "StatusPosition"; Expression = { $statusPosition } },
@{ Name = "OptionsPosition"; Expression = { $optionsPosition } },
@{ Name = "Width"; Expression = { $optionsBoxWidth } },
@{ Name = "Height"; Expression = { $optionsBoxHeight } },
@{ Name = "MaxOptions"; Expression = { $maxHeight - 3 } },
@{ Name = "SelectedOptions"; Expression = { 0 } },
@{ Name = "TotalOptions"; Expression = { $options.Length } }
Draw-Options $state
Set-SelectionColors `
$state `
$script:SelectedForegroundColor `
$script:SelectedBackgroundColor
$message = ""
do
{
$finished = Get-KeyboardInput $state
}
while ( -not $finished )
$Host.UI.RawUI.SetBufferContents( $borderTop, $oldContents )
}
}
Now, when I do a code review and I don't want to show every file, I run this command, which allows me to select only the files I want (I used aliases in this example to make the command shorter):