Read arrow keys and show color text in an efficient way
Posted: 30 Jan 2016 20:56
This topic has been a long request from Batch file programmers: read arrow and other extended keys (like function keys) and process they in efficient way. The conclusion from previous experiences is that read extended keys is not possible using just native Batch commands, so an additional application is required and the most used one is PowerShell. However, current solutions based on PowerShell are very inefficient because the whole PS environment must be executed each time that a key is read, so these programs are slow and unresponsive. Of course, another possible solution would be to write the complete process just in PowerShell, but we all know that this is not the solution we are looking for; we want a Batch-file based solution with the minimal part of non native code.
I developed a method that allows to use PowerShell in a Batch file in a very efficient way: the PS engine is loaded just one time and then repeatedly used to read all keys. The result is a Batch file that must wait just once, when PowerShell is loaded the first time, but that is very responsive after that. This key-entry method may be used in a wide range of applications. For example, the first program below show an horizontal one-line menu that allows to select their options via Home/LeftArrow/RightArrow/End keys:
We may add the findstr color text method in order to highlight the selected option. The next example is a standard CheckList/RadioButton selection form with the options placed vertically; this code also features the selection of options via the first letter of each one.
If we already used PS code to read keys, we may also use it for a very small additional point: move the cursor to a certain "screen home" position after each key is read. This simple detail add two important features to the whole application:
Below there is the classical vertical selection menu application; this example add a couple new features: the cursor is hidden while the menu is active in order to present a cleaner screen, and the menu may be displayed at any place in the screen preserving the text above it.
Finally, if we already accepted to use PS code to read keys and move the cursor to a home position, we may accept to also use it for two small additional points: move the cursor to any place in the screen, and show text in color. This way, this method would provide the features of my GetKey.exe, CursorPos.exe and ColorShow.exe auxiliary programs, so it would allow to write very fast animated games in color using just original Windows features. I will modify my original Snake.bat, 2048.bat, Tetris.bat and other animated games in order to use the PowerShell features described in this topic; I will post the new versions as soon as they are ready.
Enjoy!
Antonio
I developed a method that allows to use PowerShell in a Batch file in a very efficient way: the PS engine is loaded just one time and then repeatedly used to read all keys. The result is a Batch file that must wait just once, when PowerShell is loaded the first time, but that is very responsive after that. This key-entry method may be used in a wide range of applications. For example, the first program below show an horizontal one-line menu that allows to select their options via Home/LeftArrow/RightArrow/End keys:
Code: Select all
@echo off
if "%~1" equ "OptionSelection" goto %1
rem Activate an horizontal menu controlled by cursor keys
rem Antonio Perez Ayala
setlocal EnableDelayedExpansion
cls
echo/
echo Example of horizontal menu
echo/
echo Change selected option with these cursor keys:
echo Home/End = First/Last, Left/Right = Prev/Next.
:loop
echo/
echo/
call :HMenu select="Press Enter to continue, Esc to cancel: " Insert/Append/Update/Delete
echo Option selected: %select%
if %select% neq 0 goto Loop
echo End of menu example
goto :EOF
This subroutine activate a one-line selection menu controlled by cursor control keys
:HMenu select= prompt option1/option2/...
setlocal EnableDelayedExpansion
rem Separate options
set "options=%~3"
set "lastOption=0"
(
set "options="
for %%a in ("%options:/=" "%") do (
set /A lastOption+=1
set "option[!lastOption!]=%%~a"
set "options=!options! %%~a "
)
)
rem Define working variables
for %%a in ("Enter=13" "Esc=27" "End=35" "Home=36" "LeftArrow=37" "RightArrow=39") do set %%a
for /F %%a in ('copy /Z "%~F0" NUL') do set "CR=%%a"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"
rem Define movements for standard keys
set "sel=1"
set "moveSel[%Home%]=set sel=1"
set "moveSel[%End%]=set sel=%lastOption%"
set "moveSel[%LeftArrow%]=set /A sel-=^!^!(sel-1)"
set "moveSel[%RightArrow%]=set /A sel+=^!^!(sel-lastOption)"
rem Read keys via PowerShell -> Process keys in Batch
set /P "=Loading menu..." < NUL
PowerShell ^
Write-Host 0; ^
$validKeys = %End%..%LeftArrow%+%RightArrow%+%Enter%+%Esc%; ^
while ($key -ne %Enter% -and $key -ne %Esc%) { ^
$key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode; ^
if ($validKeys.contains($key)) {Write-Host $key} ^
} ^
%End PowerShell% | "%~F0" OptionSelection %2
endlocal & set "%~1=%errorlevel%"
echo/
exit /B
:OptionSelection
setlocal EnableDelayedExpansion
rem Wait for PS code start signal
set /P "keyCode="
set /P "="
:nextSelection
rem Show prompt: options
for %%s in ("!option[%sel%]!") do set /P "=%BS%!CR!%~2!options: %%~s =[%%~s]!" < NUL
rem Get a keycode from PowerShell
set /P "keyCode="
set /P "="
rem Process it
if %keyCode% equ %Enter% goto endSelection
if %keyCode% equ %Esc% set "sel=0" & goto endSelection
!moveSel[%keyCode%]!
goto nextSelection
:endSelection
exit %sel%
We may add the findstr color text method in order to highlight the selected option. The next example is a standard CheckList/RadioButton selection form with the options placed vertically; this code also features the selection of options via the first letter of each one.
Code: Select all
@echo off
if "%~1" equ "OptionSelection" goto %1
rem Activate a CheckList/RadioButton controlled by cursor keys
rem Antonio Perez Ayala
setlocal
set "switch="
set "endHeader="
:header
cls
echo/
echo Example of Check List / Radio Button
echo/
echo Move selection lightbar with these cursor keys:
echo Home/End = First/Last, Up/Down = Prev/Next; or via option's first letter.
echo/
%endHeader%
if not defined switch (set "switch=/R") else set "switch="
call :CheckList select="First option/Second option/Third option/Last option" %switch%
echo/
echo/
if "%select%" equ "0" goto endProg
echo Option(s) selected: %select%
pause
goto header
:endProg
echo End of example
goto :EOF
This subroutine activate a CheckList/RadioButton form controlled by cursor control keys
%1 = Variable that receive the selection
%2 = Options list separated by slash
%3 = /R (switch) = Radio Button (instead of Check List)
:CheckList select= "option1/option2/..." [/R]
setlocal EnableDelayedExpansion
rem Process /R switch
if /I "%~3" equ "/R" (
set "Radio=1"
set "unmark=( )" & set "mark=(o)"
) else (
set "Radio="
set "unmark=[ ]" & set "mark=[X]"
)
rem Separate options
set "options=%~2"
set "lastOption=0"
for %%a in ("%options:/=" "%") do (
set /A lastOption+=1
set "option[!lastOption!]=%%~a"
set "select[!lastOption!]=%unmark%"
call set "moveSel[%%option[!lastOption!]:~0,1%%]=set sel=!lastOption!"
)
if defined Radio set "select[1]=%mark%"
rem Define working variables
for %%a in ("Enter=13" "Esc=27" "Space=32" "End=35" "Home=36" "UpArrow=38" "DownArrow=40" "LetterA=65" "LetterZ=90") do set %%a
set "letter=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"
echo %BS%%BS%%BS%%BS%%BS%%BS% >_
rem Define movements for standard keys
set "sel=1"
set "moveSel[%Home%]=set sel=1"
set "moveSel[%End%]=set sel=%lastOption%"
set "moveSel[%UpArrow%]=set /A sel-=^!^!(sel-1)"
set "moveSel[%DownArrow%]=set /A sel+=^!^!(sel-lastOption)"
rem Read keys via PowerShell -> Process keys in Batch
set /P "=Loading menu..." < NUL
PowerShell ^
Write-Host 0; ^
$validKeys = %End%..%Home%+%UpArrow%+%DownArrow%+%Space%+%Enter%+%Esc%+%LetterA%..%LetterZ%; ^
while ($key -ne %Enter% -and $key -ne %Esc%) { ^
$key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode; ^
if ($validKeys.contains($key)) {Write-Host $key} ^
} ^
%End PowerShell% | "%~F0" OptionSelection
endlocal & set "%~1=%errorlevel%"
del _
exit /B
:OptionSelection
setlocal EnableDelayedExpansion
rem Wait for PS code start signal
set /P "keyCode="
set /P "="
set "endHeader=exit /B"
:nextSelection
rem Clear the screen and show the list
call :header
< NUL (for /L %%i in (1,1,%lastOption%) do (
if %%i equ %sel% (
set /P "=!select[%%i]! "
findstr /A:70 . "!option[%%i]!\..\_" NUL
) else (
echo !select[%%i]! !option[%%i]!
)
))
echo/
set /P "=Space=(De)Select, Enter=Continue, Esc=Cancel" < NUL
rem Get a keycode from PowerShell
set /P "keyCode="
set /P "="
rem Process it
if %keyCode% equ %Enter% goto endSelection
if %keyCode% equ %Esc% exit 0
if %keyCode% equ %Space% (
if defined Radio (
set "select[%Radio%]=%unmark%"
set "select[%sel%]=%mark%"
set "Radio=%sel%"
) else (
if "!select[%sel%]!" equ "%unmark%" (
set "select[%sel%]=%mark%"
) else (
set "select[%sel%]=%unmark%"
)
)
goto nextSelection
)
if %keyCode% lss %LetterA% goto moveSelection
set /A keyCode-=LetterA
set "keyCode=!letter:~%keyCode%,1!"
:moveSelection
!moveSel[%keyCode%]!
goto nextSelection
:endSelection
set "sel="
for /L %%i in (1,1,%lastOption%) do (
if "!select[%%i]!" equ "%mark%" set "sel=!sel!%%i"
)
if not defined sel set "sel=0"
exit %sel%
If we already used PS code to read keys, we may also use it for a very small additional point: move the cursor to a certain "screen home" position after each key is read. This simple detail add two important features to the whole application:
- It completelly eliminates screen flicker because the screen is not cleared in each screen refresh. Instead, the original screen contents is preserved and the refresh just change the parts that needs to be changed. This method presents a very pleasant movement/animation to the user, even if the screen contents is large.
- It allows to update/refresh just those parts of the screen that needs to be refreshed; this point decrease the time the screen refresh takes and allows faster animations and larger sprites.
Below there is the classical vertical selection menu application; this example add a couple new features: the cursor is hidden while the menu is active in order to present a cleaner screen, and the menu may be displayed at any place in the screen preserving the text above it.
Code: Select all
@echo off
if "%~1" equ "OptionSelection" goto %1
rem Activate a vertical selection menu controlled by cursor keys
rem Antonio Perez Ayala
rem Example: define menu options, menu messages and first letter of options (see description below)
setlocal EnableDelayedExpansion
set "option.length=0"
for %%a in ("First option =Description of first option "
"Second option=The option below don't have description"
"Third option = "
"Last option =Description of last option ") do (
for /F "tokens=1,2 delims==" %%b in (%%a) do (
set /A option.length+=1
set "option[!option.length!]=%%b"
set "message[!option.length!]=%%c"
call set "moveSel[%%option[!option.length!]:~0,1%%]=set sel=!option.length!"
)
)
:loop
cls
echo/
echo Description of :VMenu subroutine: how to show a vertical selection menu.
echo/
echo call :VMenu select options [messages]
echo/
echo select Variable that receives the number of selected option
echo options Name of array with the options
echo messages Name of array with messages, optional
echo/
echo "options" is an array with the text to show for each menu option;
echo all texts must be aligned (filled with spaces) to the same lenght.
echo/
echo A variable with same "options" name and ".length" postfix must contain
echo the number of options in the menu; for example: set options.length=4
echo/
echo "messages" is an optional array with companion descriptions for each option;
echo all descriptions must be aligned to the same lenght (even the empty ones).
echo/
echo The highlighted option can be changed with these cursor keys:
echo Home/End = First/Last, Up/Down = Prev/Next, or via option's first letter;
echo Enter = Select option and continue, Esc = Cancel selection (return zero).
echo/
echo/
rem For example:
call :VMenu select=option message
echo/
if %select% equ 0 goto exitLoop
echo Option selected: %select%
pause
goto loop
:exitLoop
echo End of menu example
goto :EOF
This subroutine activate a selection menu controlled by cursor control keys
:VMenu select= option [message]
setlocal
rem Define working variables
for %%a in ("Enter=13" "Esc=27" "End=35" "Home=36" "UpArrow=38" "DownArrow=40" "LetterA=65" "LetterZ=90") do set %%a
set "letter=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"
echo %BS%%BS%%BS%%BS%%BS%%BS% >_
rem Define selection bar movements for standard keys
set "sel=1"
set "moveSel[%Home%]=set sel=1"
set "moveSel[%End%]=set sel=!%2.length!"
set "moveSel[%UpArrow%]=set /A sel-=^!^!(sel-1)"
set "moveSel[%DownArrow%]=set /A sel+=^!^!(sel-%2.length)"
rem Read keys via PowerShell -> Process keys in Batch
set /P "=Loading menu..." < NUL
PowerShell ^
$console = $Host.UI.RawUI; ^
$curSize = $console.CursorSize; ^
$console.CursorSize = 0; ^
$curPos = $console.CursorPosition; ^
$curPos.X = 0; ^
$console.CursorPosition = $curPos; ^
Write-Host 0; ^
$validKeys = %End%..%Home%+%UpArrow%+%DownArrow%+%Enter%+%Esc%+%LetterA%..%LetterZ%; ^
while ($key -ne %Enter% -and $key -ne %Esc%) { ^
$key = $console.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode; ^
if ($validKeys.contains($key)) { ^
if ($key -ne %Enter% -and $key -ne %Esc%) {$console.CursorPosition = $curPos} ^
Write-Host $key ^
} ^
} ^
$console.CursorSize = $curSize; ^
%End PowerShell% | "%~F0" OptionSelection %2 %3
endlocal & set "%1=%errorlevel%"
del _
exit /B
:OptionSelection %1 option [message]
setlocal EnableDelayedExpansion
rem Wait for PS code start signal
set /P "keyCode="
set /P "="
:nextSelection
rem Show menu options
for /L %%i in (1,1,!%2.length!) do (
if %%i equ %sel% (
set /P "=.%BS% " < NUL
findstr /A:70 . "!%2[%%i]!\..\_" NUL
) else (
echo !%2[%%i]!
)
)
if defined %3[%sel%] (
echo/
echo(!%3[%sel%]!
)
rem Get a keycode from PowerShell
set /P "keyCode="
set /P "="
rem Process it
if %keyCode% equ %Enter% goto endSelection
if %keyCode% equ %Esc% set "sel=0" & goto endSelection
if %keyCode% lss %LetterA% goto moveSelection
set /A keyCode-=LetterA
set "keyCode=!letter:~%keyCode%,1!"
:moveSelection
!moveSel[%keyCode%]!
goto nextSelection
:endSelection
exit %sel%
Finally, if we already accepted to use PS code to read keys and move the cursor to a home position, we may accept to also use it for two small additional points: move the cursor to any place in the screen, and show text in color. This way, this method would provide the features of my GetKey.exe, CursorPos.exe and ColorShow.exe auxiliary programs, so it would allow to write very fast animated games in color using just original Windows features. I will modify my original Snake.bat, 2048.bat, Tetris.bat and other animated games in order to use the PowerShell features described in this topic; I will post the new versions as soon as they are ready.
Enjoy!
Antonio