Can I read both lines and characters from Node's stdin *at the same time*?

Viewed 160

I have a simple bit of code for reading input for a terminal-based build script.

async function readLine(): Promise<string> {
  return new Promise<string>(resolve => {
    const callback = (data: any) => {
      process.stdin.off('data', callback);
      resolve(data.toString().trim());
    };

    process.stdin.on('data', callback);
  });
}

This works well enough, but I'd like to be able to detect if an up-arrow is pressed so a user can back up to a previous prompt.

Every example I've seen so far that would allow me to detect an up-arrow key requires totally abandoning line-based input, instead handling all input one character at a time. I'd rather not do that.

What I'd like is to have a callback that's triggered only by the up-arrow key (and maybe other special keys later on), with all other keys coming through the usual 'data' callback, one full Enter-terminated line at a time.

Is there a way to do this?

1 Answers

Having found no better answer, I settled for handling all characters one character at a time, and writing my own terminal line input routine.

import * as readline from 'readline';
import { Key } from 'readline';

readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);

function write(s: string): void {
  process.stdout.write(s);
}

async function readUserInput(): Promise<string> {
  return new Promise<string>(resolve => {
    let buffer = '';
    let length = 0;
    const clearLine = () => write('\x08 \x08'.repeat(length));

    const callback = (ch: string, key: Key) => {
      if (ch === '\x03') { // ctrl-C
        write('^C\n');
        process.exit(130);
      }
      else if (ch === '\x15') { // ctrl-U
        clearLine();
        length = 0;
      }
      else if (key.name === 'enter' || key.name === 'return') {
        write('\n');
        process.stdin.off('keypress', callback);
        resolve(buffer.substr(0, length).trim());
      }
      else if (key.name === 'backspace' || key.name === 'left') {
        if (length > 0) {
          write('\x08 \x08');
          --length;
        }
      }
      else if (key.name === 'delete') {
        if (length > 0) {
          write('\x08 \x08');
          buffer = buffer.substr(0, --length) + buffer.substr(length + 1);
        }
      }
      else if (key.name === 'up') {
        clearLine();
        write('\n');
        process.stdin.off('keypress', callback);
        resolve('\x18');
      }
      else if (key.name === 'right') {
        if (length < buffer.length) {
          write(buffer.charAt(length++));
        }
      }
      else if (ch != null && ch >= ' ' && !key.ctrl && !key.meta) {
        write(ch);
        buffer = buffer.substr(0, length) + ch + buffer.substr(length++);
      }
    };

    process.stdin.on('keypress', callback);
  });
}
Related