Dominik
Dominik

Reputation: 6313

Readline wrangling or how to capture key events and draw inside a box

So I am hacking a bit with nodejs readline interface for practise and learning and I came across this weide behaviour.

What I am doing here is:

  1. create a readline interface
  2. move the cursor to top and draw a frame
  3. move the cursor to top again and print a timestamp inside the frame (test to see I redraw for each event)
  4. move the cursor to the end of the frame so when I exit I don't get my input in the middle of the buffer
  5. listen to keypress and call the same draw function (3.) for the inside timestamp

The idea is to only draw what is absolutely necessary and readline theoretically let's you do that.

It all works well and because I only listen to the arrow keys I use this custom stdout function to be able to ignore all other inputs.

const Writable = require('stream').Writable;
const Readline = require('readline');

//size of frame
const MINWIDTH = 120;
const MINHEIGHT = 40;

//custom stdout to suppress output
const customStdout = new Writable({
    write: function(chunk, encoding, callback) {

        if( !this.muted ) {
            process.stdout.write( chunk, encoding );
        }

        callback();
    }
});

//draw the frame only
function drawFrame( RL ) {
    customStdout.muted = false; //stdout enabled

    Readline.cursorTo(RL, 0, 0);
    Readline.clearScreenDown(RL);

    RL.write(`╔${'═'.repeat( MINWIDTH - 2 )}╗\n`);
    RL.write(`║${' '.repeat( MINWIDTH - 2 )}║\n`.repeat( MINHEIGHT - 2 ));
    RL.write(`╚${'═'.repeat( MINWIDTH - 2 )}╝\n`);

    drawBoard( RL ); //now draw inside
}


//reset cursor and draw inside of frame
function drawBoard( RL ) {
    customStdout.muted = false; //stdout enabled

    Readline.cursorTo(RL, 0, 2); //go to second line

    RL.write(`║ ${Date.now()}`); //print timestamp

    Readline.cursorTo(RL, 0, MINHEIGHT); //go to last nile

    customStdout.muted = true; //stdout disabled to ignore other input
}

//create the readline interface
const RL = Readline.createInterface({
    input: process.stdin,
    output: customStdout,
    terminal: true,
    historySize: 0,
});

//some options I've been playing with
Readline.emitKeypressEvents( process.stdin );
process.stdin.setEncoding('utf8');

if(process.stdin.isTTY) {
    process.stdin.setRawMode( true );
}

//event handler for when key is pressed
process.stdin.on("keypress", (chunk, key) => {
    if(
        key.name === 'right' ||
        key.name === 'left' ||
        key.name === 'up' ||
        key.name === 'down'
    ) {
        drawBoard( RL ); //redraw board only
    }
    else {
        return; //do nothing
    }
});

drawFrame( RL ); //now go off and draw frame

(This is a reduced test script that works and exhibits my problem as well)

All keystrokes are ignored besides the arrow keys. Now when I press the right, top or bottom keys the inside of the frame is drawn and the cursor is returned to the bottom. As expected.

enter image description here

However when I press the left key the frame is cleared and I find stdout prints the capital letter H.

enter image description here

In fact when you press a bunch of other keys (that are ignored and produce no output) and then press the left key you get all of them in one big chunk of output, replacing the H letter. I have no idea why... Repeated pressing the left key will add more Hs. All other arrow keys work as expected.

(When removing customStdout from the script I get the same behaviour for only the left key.)

Question

Please keep in mind I am not looking for a package that does that for me like bliss or charm. I am trying to learn and do it myself here

Upvotes: 2

Views: 1560

Answers (1)

Stavros Zavrakas
Stavros Zavrakas

Reputation: 3063

I 've tried to debug the program a bit and it seems that it has something to do specifically with the left arrow key and possibly this is a bug of node. It seems that there is a buffer overflow or there is no null terminated character and the write function is emitted multiple times for the arrow key. You will understand this if you use a logger to write the results into a file (I've used bunyan):

const bunyan = require('bunyan');

const log = bunyan.createLogger({
  name: 'keys',
  streams: [{
    level: 'info',
    path: 'keys.log' // log INFO and above to stdout
  }, {
    level: 'error',
    path: 'keys.log' // log ERROR and above to a file
  }]
});

const customStdout = new Writable({
  write: function (chunk, encoding, callback) {

    if (!this.muted) {
      log.info(chunk.toString('utf8'));
      process.stdout.write(chunk, encoding);
    }
    i++;
    return callback();
  }
});

The program has the proper behaviour if you press and keep the left alt and then start pressing the arrows buttons randomly. These two links will be helpful for you and maybe can give you more tips:

If you're a C or C++ programmer, you're no doubt aware that malloc() - the standard library function that returns allocated memory - does not initialize (fill with 0's) the memory that it allocates, by definition.

Turns out the same is true in Node.js; memory allocated for runtime usage in Node.js, as well as memory allocated for Buffer objects, is not initialized after being allocated.

https://nodesource.com/blog/nsolid-deepdive-into-security-policies-zero-fill-buffer-allocations/

There is this open bug about readline and emitKeypressEvents and it is about node v6.7.0 (the most possible is that this issue exists on previous versions as well)

https://github.com/nodejs/node/issues/8934

UPDATE

It seems that it was simpler than that (possibly the issue with the null-terminated character applies, though). The solution is the clear the the line using the clearLine of the readline api. This is a working solution for me:

'use strict';

const keypress = require('keypress');
const bunyan = require('bunyan');

const log = bunyan.createLogger({
  name: 'keys',
  streams: [{
    level: 'info',
    path: 'keys.log' // log INFO and above to stdout
  }, {
    level: 'error',
    path: 'keys.log' // log ERROR and above to a file
  }]
});

const Writable = require('stream').Writable;
const Readline = require('readline');

// size of frame
const MINWIDTH = 120;
const MINHEIGHT = 40;

let keyName = null;
let i = 0; 

// custom stdout to suppress output
const customStdout = new Writable({
  write: function (chunk, encoding, callback) {

    if (!this.muted) {
      log.info(keyName);
      log.info(Buffer.byteLength(chunk, 'utf8'));
      // log.info(chunk.toString('utf8'));
      process.stdout.write(chunk, encoding);
    }

    return callback();
  }
});

// draw the frame only
function drawFrame(RL) {
  customStdout.muted = false; // stdout enabled

  Readline.cursorTo(RL, 0, 0);
  Readline.clearScreenDown(RL);

  RL.write(`╔${'═'.repeat( MINWIDTH - 2 )}╗\n`);
  RL.write(`║${' '.repeat( MINWIDTH - 2 )}║\n`.repeat(MINHEIGHT - 2));
  RL.write(`╚${'═'.repeat( MINWIDTH - 2 )}╝\n`);

  drawBoard(RL); // now draw inside
}


// reset cursor and draw inside of frame
function drawBoard(RL) {
  customStdout.muted = false; // stdout enabled

  Readline.cursorTo(RL, 0, 2); // go to second line

  RL.write(`║ ${Date.now()}`); // print timestamp

  Readline.cursorTo(RL, 0, MINHEIGHT); // go to last nile

  customStdout.muted = true; // stdout disabled to ignore other input
}

// create the readline interface
const RL = Readline.createInterface({
  input: process.stdin,
  output: customStdout,
  terminal: true,
  historySize: 0,
});

// some options I've been playing with
Readline.emitKeypressEvents(process.stdin);
process.stdin.setEncoding('utf8');
process.stdin.setRawMode(true);

keypress(process.stdin);

RL.input.on("keypress", (chunk, key) => {
  console.log('keyname', key.name);

  RL.clearLine();

  if (
    key.name === 'right' ||
    key.name === 'left' ||
    key.name === 'up' ||
    key.name === 'down'
  ) {
    drawBoard(RL); // redraw board only
  } else {
    return; // do nothing
  }
});

drawFrame(RL); // now go off and draw frame

Upvotes: 2

Related Questions