Very interesting, in a different context I had to tackle a pretty similar problem with Redtamarin [0]

Traditionally under the CLI you will manage key input wih a readline() command or something similar to kbhit() and depending on your needs you'll use getchar() then track if either a CR or LF is entered for the "end of command", also EOF.

This is blocking, so nothing else can happen, and depending on how you do it, you can only read single byte chars and not mutlibyte chars (like CJK input)

something like

while( run )
{
i=kbhit();

if( i != 0 )
{
key = String.fromCharCode( getchar() );

if( (key == "\n") || (key == "\r") )
{
run = false;
}
else
{
buffer += key;
}

i = 0;
}
}

another way to do it

while( run )
{
b = fgetc( stdin );

if( b == EOF )
{
if( kbuffer.length == 0 )
{
return null;
}

run = false;
}
else if( b == LF )
{
run = false;
}
else
{
kbuffer.writeByte( b );
}
}

which has 2 greats advantages, being able to read multiple bytes input (thanks to fgetc() which read the raw byte) and detect EOF (CTRL+D under POSIX, CTRL+Z under WIN32), but still blocking forever, using fgetc() the detection of EOF is done automatically for you (while getchar() getc() etc. do not detect that EOF)

now because Redtamarin is based on AVM2 and AS3, there is one part of the API which try to reimplement the Flash API with such things like KeyboardEvent that should be able to be non-blocking but still for a CLI environment

That KeyboardEvent should detect keyUp or keyDown but yeah it is hard to detect and if you try to do that for multipel platforms liek Windows / macOS / Linux it gets nigthmarish

In a little experiment [1] I found out you can do "stupid things" that actually work, like spawning a child worker (AVM2 uses pthread), blocking on the user input (like above) and then send back a message to your main worker and so receive a "key event", all that allow to listen for input asynchronously

But then, what about making the difference between keyUp and keyDown ?
I decided to ignore the keyUp because in fact it does not really matters on the CLI or I least I don't see any use to it, for a GUI yes I can see the use cases, but for a CLI? not so much

Purely on the CLI (no X Server) you don't really listen for key events you read the stdin stream,
the only special events are signals like SIGINT SIGHUP etc. or special kind of signals like EOF.

The other things you can alterate is the buffering of that stdin and the raw mode/cooked mode and echo off/on.

So for a use case like navigating something using the 'w', 'a', 's', 'd' keys you just need to go async to listen for chars input (which key is pressed) and probably a mix of timeout on the last key pressed and a "diff" between the "prev key" and "last key".

If last key pressed is 'w' then go up, if you keep receiving a 'w' key you keep going up,
if prev key was 'w' and the last key is different you change direction.

And to detect when to stop to go up if last key pressed was 'w' you just keep the time when this key was pressed, if 1 second elapsed and no more key events are received you stop going up, ergo you don't need to detect keyUp, but maybe I'm missing something.

[0]: https://github.com/Corsaair/redtamarin
[1]: https://twitter.com/redtamarin/status/900794336031510530