Making Interactive Node.js Console Apps That Listen for Keypress Events

keypress

In preparing for an upcoming Node.js IoT tutorial focused on creating notifications based on sensor values (for example, audible alerts), I wave of brilliance came over me. 🙂  Wouldn’t it be awesome if these notifications could be interactively acknowledged and silenced from the console? Lo and behold, I discovered that Node.js CLI (console) applications can indeed respond to keystrokes, ushering in a whole new realm of possibilities.  In today’s tutorial, I will teach you how to build interactive Node.js console applications that listen for keypress events. We’ll build a simple stock quote application to bring this interactivity to life.

Building the Basic Keypress Interaction Framework

After a fair bit of googling, I found this Stack Overflow answer from a mysterious and intelligent user named arve0.  His answer focused on answering the question in the context of Node 6.X which is the latest version of Node at the time of this writing.  I had tried other options; however, these options were geared toward much older versions of Node.

After developing the code for this article, I found the keypress npm package which appears to work with both older and newer versions of Node. This is certainly an option as well. In this tutorial we seek to accomplish the goal without dependence on an npm module by relying on the core functionality that ships with Node. Our method is instructive, does not add a lot of extra code, and it’s more fun. 🙂

Let’s go ahead and try our solution by writing some basic code to see it in action:

const readline = require('readline');
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);

process.stdin.on('keypress', (str, key) => {
  if (key.ctrl && key.name === 'c') {
    process.exit();
  } else {
    console.log(`You pressed the "${str}" key`);
    console.log();
    console.log(key);
    console.log();
  }
});

console.log('Press any key...');

The first three lines provide some necessary ceremony that enables keypress events to be emitted from the console.

We then listen for the stdin keypress event and respond accordingly.  This event emits two values:

  • str – the key that was pressed such as “a”.  If you press Ctrl+a, for example, str provides the unicode equivalent character to the console so using the key object (explained next) is more useful for providing that information.
  • key – the key object is highly useful and yields the following information about the key sequence (as explained in the Node.js Readline documentation):
    • ctrl –  a boolean that is set to true to indicate the Ctrl key has been pressed as part of the key sequence.
    • meta – a boolean that is set to true to indicate the Meta key has been pressed as part of the key sequence.
    • shift – a boolean that is set to true to indicate the Shift key has been pressed as part of the key sequence.
    • name – the name of the key that was pressed.

In the next section of code, we capture Ctrl+C and exit the program if this key sequence is initiated.  My initial code did not include this Ctrl+C capture and I created the longest running console application in history since there was no way to exit.  It reminded me of all the people who continue to run vim because they can never figure out how to exit. 🙂

Let’s run our basic keypress interaction and see how it works:

$ node stocks1.js
Press any key...
You pressed the "a" key

{ sequence: 'a',
  name: 'a',
  ctrl: false,
  meta: false,
  shift: false }

You pressed the "" key

{ sequence: '\u0013',
  name: 's',
  ctrl: true,
  meta: false,
  shift: false }

In the console interaction above, I first press the “a” key.  We see in the key object that ctrl, meta, and shift are all returned as false since none of those keys are used as part of the key sequence.

I next press the Ctrl+s key sequence.  As expected, ctrl returns as true in the key object.   Also, our console output does not print ‘You pressed the “s” key’ since we pressed Ctrl+s and this displays a Unicode string that is not readily viewable on the console.

Our very rough keyboard interaction code is working!  Let’s create an interactive console application to implement our basic framework in a more fun and meaningful way.

Build an Interactive Stock Quote Retriever

We’ll now build a console application that retrieves stock quotes depending on what key we press.  Here we go!

First, let’s write a function that retrieves stock quotes from Google finance.  To retrieve a stock quote from Apple (AAPL), for example, we invoke this URL: http://finance.google.com/finance/info?client=ig&q=AAPL

Go ahead and launch the Apple stock quote URL now and you will see output similar to the following:

// [ { "id": "22144" ,"t" : "AAPL" ,"e" : "NASDAQ" ,"l" : "109.20" ,"l_fix" : "109.20" ,"l_cur" : "109.20" ,"s": "0" ,"ltt":"2:23PM EDT" ,"lt" : "Aug 19, 2:23PM EDT" ,"lt_dts" : "2016-08-19T14:23:49Z" ,"c" : "+0.12" ,"c_fix" : "0.12" ,"cp" : "0.11" ,"cp_fix" : "0.11" ,"ccol" : "chg" ,"pcls_fix" : "109.08" } ]

The Google stock service is extremely handy because it returns real-time stock quotes in JSON format.  The only downside is that you must remove the first three characters since the “//” are comment characters that must be eradicated before the JSON object can be parsed.

Given that background, here is a function that we can use to retrieve stock quotes:

function getStockQuote(symbol) {
    const url = `https://finance.google.com/finance?q=${symbol}&output=json`;
    got(url)
        .then(response => {
            // Must remove the first three characters of the Google finance text returned to parse JSON.
            const stock = JSON.parse(response.body.substr(3));
            const quote = stock[0];
            console.log(`${quote.t} ${quote.l} ${quote.c} (${quote.cp}%)`);
        })
        .catch(error => {
            console.log(error.response.body);
        });
}

We use the excellent got npm module as our http client to retrieve the contents of the URL. As documented in the code comments, we remove the first three characters of the Google finance text returned so that we can parse the JSON text into an object on the next line.

Leveraging this function to get the current stock price, we create the following complete program for interactively retrieving stock prices:

const got = require('got');

const readline = require('readline');
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);

const keyMap = new Map();
keyMap.set('a', 'AAPL');
keyMap.set('b', 'BA');
keyMap.set('c', 'CSCO');
keyMap.set('d', 'DD');
keyMap.set('e', 'XOM');
keyMap.set('f', 'FB');
keyMap.set('g', 'GOOGL');
keyMap.set('m', 'MSFT');

function getStockQuote(symbol) {
    const url = `https://finance.google.com/finance?q=${symbol}&output=json`;
    got(url)
        .then(response => {
            // Must remove the first three characters of the Google finance text returned to parse JSON.
            const stock = JSON.parse(response.body.substr(3));
            const quote = stock[0];
            console.log(`${quote.t} ${quote.l} ${quote.c} (${quote.cp}%)`);
        })
        .catch(error => {
            console.log(error.response.body);
        });
}

process.stdin.on('keypress', (str, key) => {
  if (key.ctrl && key.name === 'c') {
    process.exit(); // eslint-disable-line no-process-exit
  } else {
    if (keyMap.has(str)) {
      getStockQuote(keyMap.get(str));
    } else {
      console.log(`No symbol defined for "${str}" key.`);
    }
  }
});

console.log('Press a key to retrieve a stock price');

Notice that we add a keyMap object to map specific keys on the keyboard to the company stocks of interest. In order to run this program, you will need to do an npm install of the got module before this code will work.  After installing got, we are ready to run our program:

$ node index.js
Press a key to retrieve a stock price
AAPL 109.21 +0.13 (0.12%)
BA 134.51 -0.49 (-0.36%)
GOOGL 798.59 -4.16 (-0.52%)

We have a working interactive console application!  You can’t see my keystrokes, but our Node console program is running interactively.  For example, I press “a” to get an Apple stock quote, “b” to get the Boeing stock quote, etc.  Who needs GUI applications when we can have CUI (Console User Interfaces)?  Well, we probably need some of both, but we’re having fun with the command line!

Add a Menu to Interactive Console Application

As a final step, we add a menu to our console application to list the available stocks and give the user a little more guidance.  This list appears when we invoke the application and when we press the “L” key on our keyboard.

const got = require('got');
const eol = require('os').EOL;

const readline = require('readline');
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);

function getStockQuote(symbol) {
    const url = `https://finance.google.com/finance?q=${symbol}&output=json`;
    got(url)
        .then(response => {
            // Must remove the first three characters of the Google finance text returned to parse JSON.
            const stock = JSON.parse(response.body.substr(3));
            const quote = stock[0];
            console.log(`${quote.t} ${quote.l} ${quote.c} (${quote.cp}%)`);
        })
        .catch(error => {
            console.log(error.response.body);
        });
}

const keyMap = new Map();
keyMap.set('a', 'AAPL');
keyMap.set('b', 'BA');
keyMap.set('c', 'CSCO');
keyMap.set('d', 'DD');
keyMap.set('e', 'XOM');
keyMap.set('f', 'FB');
keyMap.set('g', 'GOOGL');
keyMap.set('m', 'MSFT');

function listKeys() {
  console.log(`${eol}keys`);
  keyMap.forEach((value, key) => {
    console.log(`${key} - ${value}`);
  });
  console.log();
}

process.stdin.on('keypress', (str, key) => {
  if (key.ctrl && key.name === 'c') {
    process.exit(); // eslint-disable-line no-process-exit
  } else if (key.name === 'l') {
    listKeys();
  } else {
    if (keyMap.has(str)) {
      getStockQuote(keyMap.get(str));
    } else {
      console.log(`No symbol defined for "${str}" key.`);
    }
  }
});

console.log('Press a key to retrieve a stock price');
listKeys();

Here is some sample output:

$ node stocks3.js
Press a key to retrieve a stock price

keys
a - AAPL
b - BA
c - CSCO
d - DD
e - XOM
f - FB
g - GOOGL
m - MSFT

FB 123.56 -0.35 (-0.28%)
MSFT 57.62 +0.02 (0.03%)

 Conclusion

We’ve successfully built an interactive Node.js console application that listens for keypress events on the keyboard.  There are numerous contexts where this could be very useful.  As an alternative, we could have built a Node REPL (Read–eval–print loop) to provide additional sophistication; however, our key press method excels for simple commands since one key press initiates action rather than having to enter a key and press enter in order to initiate action.  Thanks for reading, and I hope you learned something today!

Follow @thisDaveJ on Twitter to stay up to date on the latest tutorials and tech articles.

Additional Articles

Beginner’s Guide to Installing Node.js on a Raspberry Pi
Using Visual Studio Code with a Raspberry Pi (Raspbian)
Visual Studio Code Jumpstart for Node.js Developers
Node.js Learning through Making – Build a CPU Sensor

Share

10 thoughts on “Making Interactive Node.js Console Apps That Listen for Keypress Events

  1. Thank you thank you thank you!!! I’ve been looking for this everywhere, wanted to figure it out without the use of a package. I didn’t succeed, and you just saved me from an incredible amount of stress about it.
    Cheers,

    1. Louis, thanks for letting me know my article was helpful to you! I’m glad you were able to experience success listening for keypress events in the console.

  2. Really great snippets of functionality in this series. I’ve noticed the google stock link doesn’t appear to be working. I substituted this string and adjusted some of the output parameters:

    const url = `https://finance.google.com/finance?q=NASDAQ:${symbol}&output=json`;

    Parameter Changes
    console.log(`Price ${quote.l} Open ${quote.op} Hi ${quote.hi} Lo (${quote.lo}%) for ${quote.name} `);

    1. Tom, thanks for taking the time to let me know! Google updated their API and the stock retrieval part of my code is no longer valid. I think you can also use the following URL which might be more generic since it will work for more than NASDAQ (but I also like what you are doing here to provide a “fully qualified” name for the stock ticker symbol):

      const url = `https://finance.google.com/finance?q=${symbol}&output=json`;

      Update: I modified my code above to reflect the updated Google Finance API.

    1. Hi Chris – great question. In the context of my code example, let’s say we wanted to register the spacebar to get the stock price for Walmart (WMT) with an option to disable the spacebar. We’d first register the spacebar key in the `keyMap`:

      keyMap.set(" ", "WMT");

      We could then register the `z` key, for example, as a way of enabling or disabling the spacebar by modifying the code to remove the spacebar from the `keyMap` when `z` is pressed:

      process.stdin.on("keypress", (str, key) => {
      if (key.ctrl && key.name === "c") {
      process.exit(); // eslint-disable-line no-process-exit
      } else if (key.name === "z") {
      if (keyMap.has(" ")) {
      console.log("disabling the spacebar");
      keyMap.delete(" ");
      } else {
      console.log("enabling the spacebar");
      keyMap.set(" ", "spacebar");
      }
      } else if (key.name === "l") {
      listKeys();
      } else {
      if (keyMap.has(str)) {
      getStockQuote(keyMap.get(str));
      } else {
      console.log(`No symbol defined for "${str}" key.`);
      }
      }
      });

      Hope this helps!

  3. Does the Author still respond to this post?

    I have a question, but first thanks for the tutorial it helped me a lot,

    the question is… is there any way of clear the buffer? I am using it to get any key pressed on the keyboard to come back to my menu, but sometimes it seems to come back with cache on it, is there any way of clearing it?
    thanks

Leave a Reply

Your email address will not be published. Required fields are marked *