Reputation: 253
Using PowerShell, I would like to read the value of key presses as they occur, without the user having to press enter. For example, if the user presses '1' I would like the PowerShell script to react to the choice immediately without an 'enter'.
My research has turned up ReadKey,
$input = $Host.UI.RawUI.ReadKey('IncludeKeyDown');
But ReadKey returns much more information in the string than I require:
72,h,0,True
While I can parse the key press from this string, I'd prefer a more direct option. Does one exist?
Upvotes: 15
Views: 35647
Reputation: 1
The command [console]::ReadKey() actually returns a ConsoleKeyInfo object not a string. ConsoleKeyInfo contains Key, KeyChar, Modifiers members.
From: https://learn.microsoft.com/en-us/dotnet/api/system.consolekeyinfo?view=net-8.0
Key Gets the console key represented by the current ConsoleKeyInfo object.
KeyChar Gets the Unicode character represented by the current ConsoleKeyInfo object.
Modifiers Gets a bitwise combination of ConsoleModifiers values that specifies one or more modifier keys pressed simultaneously with the console key.
It was mentioned by JackGrinningCat:
Key will give you for pressing some keys
1 = D1
1 on the NumPad = NumPad1
So the correct way to use it is:
$k = [console]::Readkey()
echo $k.KeyChar
Now 1 gives you 1 and 1 on the NumPad gives you 1 and it will treat capitalization correctly.
Upvotes: 0
Reputation: 463
Use [console]::ReadKey() but convert the .key to a string. That converts to either the character code or the key name - eg 'Enter'. Check the codes, they are a bit weird. This can then be wrangled with if or switch.
In this example I allow the user to press enter to accept the default action.
Note also that ReadKey writes the character to the screen, an easy way to clean this up is to begin the next console write with a return character (backtick r).
Write-Host 'Reload Profile? [Yn]'
$k = ( [Console]::ReadKey() ).key.tostring()
if( ( 'yY','Enter' ) -contains $k ) {
Write-Host "`rReloading profile..."
. $profile
} else {
Write-Host "`rProfile NOT LOADED."
}
Upvotes: 0
Reputation: 1009
Here is a Cmdlet/Function that will handle this but will also allow for capturing of Ctrl-C. I ran into the same problem with $Host.UI.RawUI.ReadKey()
that others did so this uses [System.Console]::ReadKey()
.
As with other [System.Console]::ReadKey()
solutions, this will not work in the ISE which does not have a console attached.
<#
.SYNOPSIS
Reads and returns a single character from the console.
.DESCRIPTION
Writes $Prompt to the console and waits for a single character of input. If
$Options is provided, input is taken until one of the characters in $Options
is entered and that character is returned.
.PARAMETER $Prompt
Text to write to the console as prompt for input.
.PARAMETER $Options
String of valid input characters. If not supplied, all characters are valid.
.PARAMETER $Default
Character to return when Enter key is pressed.
.PARAMETER $CtrlC
Character to return when Ctrl-C is pressed. Allows handling of Ctrl-C as input.
.PARAMETER $NoCase
Case-insensitive input. When true, all input is converted to uppercase.
.PARAMETER $NoEcho
When true, valid input characters are echoed to the console.
.OUTPUT
Character entered.
.EXAMPLE
if ('Y' -ne (Read-KeyChar "`nProceed (y/N)? " YN -Default N -CtrlC X)) {
exit 1
}
#>
Function Read-KeyChar {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0)][string]$Prompt,
[Parameter(Mandatory=$false, Position=1)][string]$Options = $null,
[Parameter(Mandatory=$false)] [char] $Default = $null,
[Parameter(Mandatory=$false)] [char] $CtrlC = $null,
[Parameter(Mandatory=$false)] [switch]$NoCase = $true,
[Parameter(Mandatory=$false)] [switch]$NoEcho = $false
)
if ($NoCase) {
$Options = $Options.ToUpper()
$Default = [System.Char]::ToUpper($Default)
$CtrlC = [System.Char]::ToUpper($CtrlC)
}
write-host -nonewline $Prompt
while ($true) {
if ($CtrlC) {
$TreatCtrlC = [Console]::TreatControlCAsInput
[Console]::TreatControlCAsInput = $true
}
$key = [console]::ReadKey($true)
if ($CtrlC) {
[Console]::TreatControlCAsInput = $TreatCtrlC
if ($key.Modifiers -eq "Control" -and $key.Key -eq 'C') {
return $CtrlC
}
}
if ($key.Key -eq "Enter" -and $Default) {
$chr = $Default
} elseif ($NoCase) {
$chr = [System.Char]::ToUpper($key.KeyChar)
} else {
$chr = $key.KeyChar
}
if (-not $Options -or $Options.Contains($chr)) {
if (-not $NoEcho) {
write-host $chr
}
return $chr
}
}
}
Upvotes: 0
Reputation: 2720
Seems $Host.UI.RawUI.ReadKey()
not works in VSCode console.
Here are my ReadKey implementation.
class DConsole {
static [System.ConsoleKeyInfo]ReadKey() {
return [DConsole]::ReadKey($true)
}
static [System.ConsoleKeyInfo]ReadKey([bool]$noEcho = $true) {
$key = [System.Console]::ReadKey()
if ($noEcho) {
$cTop = [System.Console]::CursorTop
[System.Console]::SetCursorPosition(0, $cTop)
}
return $key
}
}
Usage:
$key = [DConsole]::ReadKey()
# or
$key = [DConsole]::ReadKey($false)
Any improvements are welcome.
Back to the question, $key.KeyChar.ToString()
may what you want.
Upvotes: 0
Reputation: 307
I have extracted out the essentials of the input and menu system and gave a few example menu items.
MODE 1: Uses Read-Host, requires Enter, Runs in ISE. Use this for troubleshooting/building
MODE 2: Uses ReadKey, no Enter required, Does NOT run in ISE... You will need to run this in a PowerShell command line. The code below is currently in Mode 2.
##Formatting Variables
$fgc1 = 'cyan'
$fgc2 = 'white'
$indent = ' '
Function MainMenu {
CLS
Write-Host "###############"
Write-Host "## Main Menu ##"
Write-Host "###############"
Write-Host -NoNewLine "$indent" "A " -ForegroundColor 'red'; Write-Host "== Options A" -ForegroundColor $fgc2
Write-Host -NoNewLine "$indent" "B " -ForegroundColor 'red'; Write-Host "== Options B" -ForegroundColor $fgc2
Write-Host -NoNewLine "$indent" "C " -ForegroundColor 'red'; Write-Host "== Options C" -ForegroundColor $fgc2
Write-Host -NoNewLine "$indent" "D " -ForegroundColor 'red'; Write-Host "== Options D" -ForegroundColor $fgc2
Write-Host -NoNewLine "$indent" "E " -ForegroundColor 'red'; Write-Host "== Options E" -ForegroundColor $fgc2
Write-Host -NoNewLine "$indent" "F " -ForegroundColor 'red'; Write-Host "== Options F" -ForegroundColor $fgc2
Write-Host -NoNewLine "$indent" "G " -ForegroundColor 'red'; Write-Host "== Options G" -ForegroundColor $fgc2
Write-Host ""
#This gives you a way to set the current function as a variable. The Script: is there because the variable has to
#be available OUTSIDE the function. This way you can make it back to the menu that you came from as long as all
#of your menus are in functions!
$Script:SourceMenu = $MyInvocation.MyCommand.Name
# Mode 1#
#Use this for troubleshooting so that you can stay in ISE
# Uncomment the 2 lines below to use Read-Host. This will necessitate an ENTER Key. BUT, it WILL work in ISE
#$K = Read-Host - "Which option?"
#MenuActions
# Mode 2#
#Uncomment the line below to use ReadKey. This will NOT necessitate an ENTER Key. BUT, it ## will NOT work ## in ISE
ReadKey
}
Function ReadKey {
Write-Host "Please make your choice..."
Write-Host ""
Write-Host "Press Q to quit"
$KeyPress = [System.Console]::ReadKey()
#This gets the keypress to a common variable so that both modes work (Read-Host and KeyPress)
$K = $KeyPress.Key
#Jumps you down to the MenuActions function to take the keypress and "Switch" to it
MenuActions
}
Function MenuActions {
Switch ($K) {
A {CLS;Write-Host "You Pressed A";Write-Host "Going to pause now... ";&pause}
B {CLS;Write-Host "You pressed B";Write-Host "Going to pause now... ";&pause}
C {CLS;Write-Host "You pressed C";Write-Host "Going to pause now... ";&pause}
D {CLS;Write-Host "You pressed D";Write-Host "Going to pause now... ";&pause}
E {CLS;Write-Host "You pressed E";Write-Host "Going to pause now... ";&pause}
F {CLS;Write-Host "You pressed F";Write-Host "Going to pause now... ";&pause}
G {CLS;Write-Host "You pressed G";Write-Host "Going to pause now... ";&pause}
#This is a little strange of a process to exit out, but I like to use an existing mechanism to exit out
#It sets the $SourceMenu to a process that will exit out.
#I use this same process to jump to a different menu/sub-menu
Q {$SourceMenu = "Exit-PSHostProcess";CLS;Write-Host "Exited Program"}
}
#This next command will loop back to the menu you came from. This, in essence, provides a validation that one of the
#"Switch ($X.key)" options were pressed. This is also a good way to always find your way back to
#the menu you came from. See "$Script:SourceMenu = $MyInvocation.MyCommand.Name" above.
#
#This is also the way that the Menu Item for Q exits out
& $SourceMenu
}
# This runs the MainMenu function. It has to be after all the functions so that they are defined before being called
MainMenu
$KeyPress = [System.Console]::ReadKey()
#This gets the keypress to a common variable so that both modes work (Read-Host and KeyPress)
$K = $KeyPress.Key
#
#Then do something with $K
Just a little addition here. Since we are talking about a SINGLE key press... how about a double key press? Well, this will work fine. Just stack up the ReadKey commands and assign variables to each, then combine them:
Write-Host "Press the 2 character option you wish"
#Get KeyPress1 Variable
$KeyPress1 = [System.Console]::ReadKey()
#This gets the keypress to a common variable so that both modes work (Read-Host and KeyPress)
$K1 = $KeyPress1.Key
#Get KeyPress1 Variable
$KeyPress2 = [System.Console]::ReadKey()
#This gets the keypress to a common variable so that both modes work (Read-Host and KeyPress)
$K2 = $KeyPress2.Key
#This is just for troubleshooting to prove it works
CLS
Write-Host "This is the state of the variables right now"
Write-Host "Keypress1 is: $K1" -ForegroundColor Green
Write-Host "Keypress1 is: $K2" -ForegroundColor Green
$KEYS = "$K1"+"$K2"
Write-Host "The combined presses are: $KEYS" -ForegroundColor Red
pause
I look forward to questions or comments.
Upvotes: 1
Reputation: 510
Regarding the use of $Host.UI.RawUI.ReadKey('IncludeKeyDown');
Goyuix answer should be marked as the right answer.
I found the $Host.UI.RawUI.ReadKey
rather long and wanted to use [console]
.
But was a bad decision. This is a warning. Some might know how to use it right.
Key will give you for pressing some keys
Additionally the similar looking [console]::ReadKey
it will the Key Property.
$key = [console]::ReadKey()
if ($key.Key -eq 'D1') {
"Pressed 1"
}
Upvotes: 5
Reputation: 24340
Perhaps you could clarify a bit - but ReadKey returns a KeyInfo object not a string. KeyInfo
contains VirtualKeyCode, Character, ControlKeyState and KeyDown members - all fields in your output string in that order. In fact, it looks like PowerShell has just called the .ToString() method in your output example. You will probably want to look at the Character
property to find your desired character. Consider the following example where I press 1:
$key = $Host.UI.RawUI.ReadKey()
if ($key.Character -eq '1') {
"Pressed 1"
}
Upvotes: 28