Search code examples
javascriptnode.jskeyboardkeypressreadline

Node.js process/readline on keypress listener prevents the app from ever ending on its own


I'm trying to:

  • write a simple awaitable async function for a TUI node.js app
  • that waits for a single key press on the keyboard (without having to press Enter after)
  • and returns info about the key that was pressed
  • ...without using any NPM packages

Here's my function:

export async function inkey_promise() {
    console.log('Press any key...');

    return new Promise((resolve) => {
        readline.emitKeypressEvents(process.stdin);
        process.stdin.setRawMode(true);
        const listener = (str, readline_key) => {
            process.stdin.setRawMode(false);
            process.stdin.resume();
            console.log(`RETURNING: `, readline_key);
            return resolve(readline_key);
        };
        process.stdin.once('keypress', listener);
    });
}

And I call it like:

const key_info = await inkey_promise();

Problem:

After pressing a key:

  • As expected:
    • The promise resolves
    • And my code after the await carries on
  • But:
    • When there's nothing left to do, my node.js app remains running forever, it never exits unless I kill the process manually
    • I don't want to explicitly do a process.exit() here, because it will always be doing stuff after the key press... but when there's nothing left for it to do, it should still exit normally as it used to.

So it seems that there's something else I need to do to fully return the process/stdin/keyboard back to its default behaviour/state?


Solution

    • Managed to solve it.
    • Here's the code I ended up with (as TypeScript):
    import readline from 'readline';
    
    type Args_tui_prompt_inkey = {
        echo_debug: boolean;
    };
    
    export async function tui_prompt_inkey(args?: Args_tui_prompt_inkey): Promise<Output_tui_prompt_inkey> {
        return new Promise((resolve) => {
            const rl = readline.createInterface({
                input: process.stdin,
                output: process.stdout,
            });
            const listener = (_arg1: never, rkey: readline.Key) => {
                rl.close();
                if (args?.echo_debug) console.log(`rkey is: `, rkey);
                resolve({
                    rkey: rkey,
                });
            };
            // using ['input'] to suppress TS errors
            rl['input'].once('keypress', listener);
            rl['input'].setRawMode(true);
            rl['input'].resume();
        });
    }
    
    type Output_tui_prompt_inkey = {
        rkey: readline.Key;
    };
    
    
    • TypeScript didn't like me trying to access rl.input
      • Hence the usage of rl['input'] instead.
      • If anyone knows a simpler/cleaner alternative with better type safety there, please let me know.