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
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
curl rate.sx
Another one from
. This one even has charts π². Thatβs just crazy!!
curl parrot.live
This one is from
. This has an animated party parrot !!
How cool is that !!!
curl byemck.atulr.comorcurl 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.
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
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 promiseconst 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
const userAgent = req.headers['user-agent'];
app.get('/hello', async (req, res, next) => {
// checking the useragent
const isCommandline = (userAgent.search(/curl|wget/i) !== -1);if (isCommandline) {
app.use('/hello', express.static('static/hello/index.html'));app.listen(PORT, () => {
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
});
console.log(`Example app listening on port ${PORT}!`);
});
Outputs:
Now when you runcurl 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 :
I hope this was fun π¬π Please follow me on twitter @a7ulr/