I am creating an interactive console application that should run on both Linux and Windows operating systems. Within the program, there are prompts which ask for keyboard input from stdin
. At each prompt, the user can only enter specific ASCII characters, as well as a specific number of characters. The prompts also have special behavior if the <Escape>
or <Enter>
keys are pressed; <Enter>
submits the prompt to be evaluated by the internal program state, and <Escape>
triggers a command which exits the program without requiring the prompt to be submitted. At the moment, these prompts only work on Linux because I do not know how to implement them for Windows.
The input prompt is currently implemented via two functions: the generic handler function prompt_user_input
, and the platform-layer function platform_console_read_key
, which only has a working Linux implementation at the moment--not Windows.
platform_console_read_key
works like this:
- Immediately read up to six bytes from
stdin
into a buffer (multiple bytes allows for parsing ANSI escape codes for non-ASCII keys). - If the first byte read is not an escape sequence indicator, the first byte is returned as a regular ASCII character.
- If the first byte is an escape sequence indicator, bytes 2-6 of the buffer are parsed.
- If the rest of the buffer is empty, then the character is standalone, so do step 2.
- If there is something in the latter part of the buffer, then it is actually an escape sequence. Attempt to parse and return it as an "extended key code" (which is just a
#define
for an integer > 255). - If there is something in the latter part of the buffer, and it is an escape code which is not handled by the function, return the special value
0
.
- If there is any kind of critical I/O error, return the special value
KEY_COUNT
.
The reason I read the characters like this is because I handle each key code returned by platform_console_read_key
separately in prompt_user_input
to determine how the program should proceed. prompt_user_input
works like this:
- Take an argument which is the maximum number of characters which may be input.
- Read keys via
platform_console_read_key
in an infinite loop. - If last key matches
<Enter>
or<Escape>
, break from the loop. - Otherwise, see if it is a valid key using a
filter_user_input
predicate.- If it is valid, add it to an input buffer, as long as the number of the characters written to the buffer does not exceed that allowed by the function argument. Also print it to the screen (i.e. "echo" functionality).
- If max writes reached, or key is invalid, continue (i.e. goto step 2).
My question is how I would implement platform_console_read_key
for the Windows platform layer. How can I immediately read 6 bytes of keyboard input at the Windows terminal? Also, how do I determine how these 6 bytes will be formatted, so that I can map the Windows key codes to the function's return values such that the behavior of the function matches that of the Linux version?
Below is the working Linux code, where the platform layer includes <termios.h>
and <unistd.h>
. I omitted some irrelevant code, as well as added some printf
statements for clarity and ease of testing. Note also that in prompt_user_input
there are references to a global ( *state ).in
, which is the input buffer; assume it is always big enough for the supplied char_count
.
bool
prompt_user_input
( const u8 char_count
)
{
printf ( "Type your response: " );
// Clear any previous user input.
memset ( ( *state ).in , 0 , sizeof ( ( *state ).in ) );
KEY key;
u8 indx = 0;
for (;;)
{
key = platform_console_read_key ();
if ( key == KEY_COUNT )
{
// ERROR
return false;
}
// Submit response? Y/N
if ( key == KEY_ENTER )
{
return true;
}
// Quit signal? Y/N
if ( key == KEY_ESCAPE )
{
//...
return true;
}
// Handle backspace.
if ( key == KEY_BACKSPACE )
{
if ( !indx )
{
continue;
}
indx -= 1;
( *state ).in[ indx ] = 0;
}
// Response too long? Y/N
else if ( indx >= char_count )
{
indx = char_count;
continue;
}
// Invalid keycode? Y/N
else if ( !filter_user_input ( key ) )
{
continue;
}
// Write to input buffer.
else
{
( *state ).in[ indx ] = key;
indx += 1;
}
// Render the character.
printf ( "%s"
, ( key == KEY_BACKSPACE ) ? "\b \b"
: ( char[] ){ key , 0 }
);
}
}
KEY
platform_console_read_key
( void )
{
KEY key = KEY_COUNT;
// Configure terminal for non-canonical input.
struct termios tty;
struct termios tty_;
tcgetattr ( STDIN_FILENO , &tty );
tty_ = tty;
tty_.c_lflag &= ~( ICANON | ECHO );
tcsetattr ( STDIN_FILENO , TCSANOW , &tty_ );
fflush ( stdout ); // In case echo functionality desired.
// Read the key from the input stream.
char in[ 6 ]; // Reserve up to six bytes to handle special keys.
memset ( in , 0 , sizeof ( in ) );
i32 result = read ( STDIN_FILENO , in , sizeof ( in ) );
// I/O error.
if ( result < 0 )
{
key = KEY_COUNT;
goto platform_console_read_key_end;
}
// End of transmission (I/O error).
if ( in[ 0 ] == 4
|| in[ 1 ] == 4
|| in[ 2 ] == 4
|| in[ 3 ] == 4
|| in[ 4 ] == 4
|| in[ 5 ] == 4
)
{
key = KEY_COUNT;
goto platform_console_read_key_end;
}
// ANSI escape sequence.
if ( *in == '\033' )
{
// Standalone keycode.
if ( !in[ 1 ] )
{
key = KEY_ESCAPE;
goto platform_console_read_key_end;
}
// Composite keycode.
else
{
if ( in[ 1 ] == '[' )
{
// ...
}
else
{
key = 0;
}
goto platform_console_read_key_end;
}
}
// Standalone ASCII character.
else
{
// Backspace key is mapped to ASCII 'delete' (for some reason).
key = ( *in == KEY_DELETE ) ? KEY_BACKSPACE : *in;
goto platform_console_read_key_end;
}
// Reset terminal to canonical input mode.
platform_console_read_key_end:
tcsetattr ( STDIN_FILENO , TCSANOW , &tty );
fflush ( stdout ); // In case echo functionality desired.
return key;
}
EDIT
Solved, thanks to the accepted answer. Here is the minimal Windows implementation of platform_read_console_key
that keeps the behavior of prompt_user_input
the same. It uses the <conio.h>
header.
KEY
platform_console_read_key
( void )
{
i32 getch;
getch = _getch ();
// Error.
if ( getch < 0 )
{
return KEY_COUNT;
}
// Standalone ASCII keycode.
if ( getch != 0 && getch != 224 && getch < 0x100 )
{
return newline ( getch ) ? KEY_ENTER
: ( getch == '\033' ) ? KEY_ESCAPE
: getch
;
}
// Extended keycode.
getch = _getch ();
switch ( getch )
{
default: return 0; // Unknown keycode.
}
}
Here is a simple solution user
Weather Vane
hinted at, and that I've used for years. Running this code will also allow you to discover and hence define many more F-keys -- you know,F1
,F2
, alsoarrow keys
, and the like.NOTE: you want that add of 256 to
_getch()
for the extended keys inGetKey()
. This places extended key values outside the range of common keys likeA thru Z
....noting thatF10
by itself would only be value68
-- and that matches with the key value forSHIFT + 'D'
. No good, see?This code will also work for
CTRL
+ key combos. Also works forALT
key combos within limits.ALT
key combos get a little hairy becauseALT+TAB key
, for example, switches between applications in MS Windows. Hence, when your app starts competing with the OS for keystroke actions, behavior might become undefined. And -- per experience -- you can add 100s of lines of code to remedy these things but only increase performance and scope nominally.Code was compiled in Visual Studio and tested on Win10 before posting to SO.