πŸ“Ÿ Building Web based UIs for Terminals using JavaScript

6 min read Original article β†—

Atul

For a developer, most of his/her time is spent on the code editor or the terminal. As web developers, we mostly target browsers and nowadays even native mobile platforms (thanks React Native). But I recently figured out you could also target terminals for your web-based UIs.

What do I mean by this ?

Type: curl wttr.in in your terminal. This console app from

written in python is what inspired me to write this blog.

Press enter or click to view image in full size

wttr.in by

Amazing! right. So, I decided to dig a bit deeper and find out how it is done.

ANSI Escape Codes

ANSI escape sequences are a standard for in-band signaling to control the cursor location, color, and other options on video text terminals. Certain sequences of bytes, most starting with Esc and β€˜[β€˜, are embedded into the text, which the terminal looks for and interprets as commands, not as character codes.

In essence, ANSI Escape codes are characters that are interpreted by the terminal in a special way. These codes can be used to add colors to the text, control cursor location, etc. All major terminals support these codes including Windows πŸ˜‰.

In 2016 with Windows 10 β€œThreshold 2”[1] Microsoft unexpectedly started supporting ANSI escape sequences

The exact details of how ANSI escape codes work will not be covered here. There is an excellent Wikipedia article on that here: https://en.wikipedia.org/wiki/ANSI_escape_code

More examples for inspiration before we code

Before we go ahead and hack this out, I wanted to share more awesome examples:

Press enter or click to view image in full size

rate.sx by

curl rate.sx

Another one from

. This one even has charts 😲. That’s just crazy!!

parrot.live by

curl parrot.live

This one is from

. This has an animated party parrot !!

How cool is that !!!

byemck.atulr.com by

curl byemck.atulr.com or
curl byeiz.atulr.com

This one I built for last day at my previous job. Wanted to say goodbye in style πŸ˜‰

https://github.com/master-atul/byemck

There are more examples listed at: https://github.com/chubin/awesome-console-services

Lets build these using JavaScript (NodeJS)

The complete source code can be found here: https://github.com/a7ul/console-web-ui

First step: Simple hello world

Since this is a web API, I would be using express.js to build a simple API server using nodejs. Follow the guide here: https://expressjs.com/en/starter/hello-world.html to make a simple express js API server.

Now type: node app.js to launch the server.
In another terminal type: curl localhost:3000. You should see :

Press enter or click to view image in full size

There, hello world is ready πŸ˜†

Next step: Add colors to the text

As mentioned before, we need ANSI codes for the colored output to the screen. Fortunately, there is a really nice npm module for that. https://github.com/chalk/ansi-styles

Let’s see what hello world in color looks like.

const style = require('ansi-styles');const express = require('express');                                               const hello = () =>`                          ${style.green.open}Hello Green!${style.green.close}                         ${style.red.open}Hello Red!${style.red.close}                       `;                                               const app = express();                       app.get('/hello', (req, res) => {                          
res.send(hello());
});
const PORT = process.env.PORT || 3000;

app.listen(PORT, () => console.log(`Example app listening on port ${PORT}!`));

app.js

Get Atul’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

You should get the following output on curl http://localhost:3000/hello

Press enter or click to view image in full size

What about animations ?

Animations are a continuous sequence of frames that give an impression that something is moving. To achieve that we need to send multiple frames to the client side and clear out the old frame before showing the next.

  • To send multiple frames of data to the client side we will use node streams .
  • To clear out screen before displaying next frame we can use a special ANSI code \033[2J\033[H

Let’s say we want to show the current server time on the console that changes with every second. So our output would look like this:

Press enter or click to view image in full size

animated current time display using curl β€” NOTICE THE SECONDS (it changes πŸ˜†)

The code looks like this:

const express = require("express");
const { Readable } = require("stream");
// This is the special ANSI code to tell terminals to clear the screenconst PAGE_BREAK = "\033[2J\033[H";// This function gets the current date in string format along
// with a page break on top.
// Note that you would need to add a new line for the terminal to
// interpret it. That is `hello` will not work while `hello\n` will.
const getDate = () => `
${PAGE_BREAK}
${new Date()}
`;
// This function initialises the stream for nodeconst getStream = (req, res) => {
const stream = new Readable();
stream.pipe(res);
stream._read = () => {};
req.on("close", () => {
stream.destroy();
});
return stream;
};
// This is used to add delay between frames.
// Its just waits for a said millisec before resolving the promise
const delay = async (milliseconds = 0) =>
new Promise((resolve) => {
setTimeout(() => resolve(), milliseconds);
}
);
// animateHello is the main function which pushes each frame
// of animation in a delay of 1sec (max 30 times)
const animateHello = async (stream) => {
for (let i = 0; i < 30; i += 1) {
stream.push(getDate());
await delay(1000);
}
stream.push(null); //This ends the stream.
};
// App initialisationconst app = express();// routeapp.get("/anime-hello", async (req, res) => {
const stream = getStream(req, res);
await animateHello(stream);
});
const PORT = process.env.PORT || 3000;app.listen(PORT, () =>
console.log(`Example app listening on port ${PORT}!`)
);

What if same route can be used for both browser and terminal ?

Ideally, if we hit a URL on a browser we should get the HTML and CSS response, while if we hit it from curl or terminal we should get the response in the text format that terminal understands.

What do I mean ?
Okay try opening wttr.in on the browser β€” You should see HTML, CSS based website loading up. Now try curl wttr.in , the same URL now returns text-based output that the terminal can understand.

To do this in expressjs. We will use the concept of next .

const express = require('express');  

const hello = require('./src/ansi/animations/hello'); const PORT = process.env.PORT || 3000; const app = express();

// simple hello route
app.get('/hello', async (req, res, next) => {

const userAgent = req.headers['user-agent'];
// checking the useragent
const isCommandline = (userAgent.search(/curl|wget/i) !== -1);

if (isCommandline) {
await res.send(hello.hello());
// This handles the route if the request came from curl or wget
return null;
}
return next();
// This passes the control to the next matching route handler
});

app.use('/hello', express.static('static/hello/index.html'));app.listen(PORT, () => {
console.log(`Example app listening on port ${PORT}!`);
});

Outputs:

Now when you run
curl http://localhost:3000/hello

β¬… You get this

Press enter or click to view image in full size

If you open up the link in your browser, you β€˜ll get the HTML page you made.

This example is hosted on heroku at https://console-web-ui.herokuapp.com/

All the code for this blog is at :

https://github.com/a7ul/console-web-ui/

I hope this was fun πŸ˜¬πŸŽ‰ Please follow me on twitter @a7ulr/