This is a small tutorial to get started with Electron as a regular Node.js / Npm dependency, so you can build stand-alone app that can be published to npmjs.org.

You should be aware that Electron is the new name of the formerly known Atom-Shell. If you can't figure out what it is, please start reading First encounter with Atom-Shell first. This previous article contains valuable informations and may help you deciding which node/chromium runtime technologies you should pick... the other major contender being NW.js (formerly known as Node-Webkit, name dropped because it is now using io.js and chromium).

Here, app.js is the entry point of the Electron application. When your application is not made the stand-alone way, you run with electron followed by the directory of your application, e.g. electron path/to/my/app (assuming Electron is installed globally). Electron needs a package.json, and fortunately, its package is fully compatible with node/npm package. So app.js is the entry point provided as the "main" property of your package.json, and it could be anything you want.

Have in mind that the root directory of your stand-alone application is the root directory of your package.

Also, I like app.js to be as simple and as short as possible. I choose to put Javascript code running in the browser context in the lib/ directory, and I choose to put Javascript code running in the renderer/webpage context into the front/js/ directory (the difference between both contexts has been explained here).

Any front-end code is put into the front/ directory. It could be useful to keep things separated, if one day we want an online website version of our app, and we have to bring back to life the good ol' client/server paradigm.

Secondly, add the electron-prebuilt dependency:

npm install electron-prebuilt --save

If you plan to use third-party package with native code, you should install electron-rebuild as well:

npm install electron-rebuild --save

... I will explain later how to use it properly.

Finally, you need to create your own executable file. For this step, I have simply copied, adapted and reformated the launcher that you can found at ./node_modules/electron-prebuilt/cli.js, and put it into the ./bin/ directory with the name my-app.

I do not append .js to the name, because it will be the real command your user will have to type in their console, if they installed your package globally (e.g. npm install -g my-app). In fact, you CAN provide a different name for the globally installed binaries (see below), but I don't like to confuse myself, I like to stay consistent...

Now you can run your application using the command ./bin/my-app, and by the way, if you installed your application globally (npm install -g my-app), you would run it simply with the command: my-app. You should just add that line into your package.json to let npm know about it:

"bin": {
"my-app": "./bin/my-app"
},

But wait? We don't have an application right now, do we?

Okey, let's copy-paste the code from the quick start documentation of Electron!

Here is our ./app.js file, slightly modified:

var app = require('app'); // Module to control application life.
var BrowserWindow = require('browser-window'); // Module to create native browser window.
// Report crashes to our server.
require('crash-reporter').start();
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the javascript object is GCed.
var mainWindow = null;
// Quit when all windows are closed.
app.on('window-all-closed', function() {
if (process.platform != 'darwin')
app.quit();
});
// This method will be called when Electron has done everything
// initialization and ready for creating browser windows.
app.on('ready', function() {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600});
// and load the index.html of the app.
mainWindow.loadUrl('file://' + __dirname + '/front/main.html');
// Open the devtools?
//mainWindow.openDevTools();
// Emitted when the window is closed.
mainWindow.on('closed', function() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
});

Now you can run your wonderful app! Now just run ./bin/my-app and let it shine!

Using package featuring native code

Electron use a version of Chromium that is different from node.js. So native packages cannot work without being rebuilt.

Rebuilding can be painful, hopefully electron-rebuild is here to do the job for us.

You should have installed it using npm install electron-rebuild --save already.

Anytime you install some packages featuring native code, you should run electron-rebuild afterward. E.g. if we want to install the package whatever, we have to do it in two step:

npm install whatever --save
./node_modules/.bin/electron-rebuild

I should warn you here:

“electron-rebuild is REALLY slow”

... and aside from that:

“electron-rebuild does NOT output ANYTHING”

It's really misleading...

The first time I used it, I was thinking that it was not working at all, and I Ctrl-C it multiple times... Then, I finally give it a last chance to shine, while I was making a tea. On my two year old laptop, it takes few minutes.

Also people installing your supa-ground-breaking package are not aware of electron-rebuild, so you have to add a line into your package.json, so it will kick-in at installation time. Here is the line:

"scripts": {
"install": "electron-rebuild"
},

You should also warn your user that the installation process can take a while...

In the blue corner, the defender, the world wide web superstar, having no less than 70% of website using it as their server-side solution, the good friend of all daddy's personnal homepages, the companion of some of the top-ten biggest websites in the world including Wikipedia and Wordpress.com, having a tons of fans and a tons of haters, here is the three-letters world wide famous P... H... P... !

In the red corner, the challenger, the rising star of web's server-side solution, the solution of choice for any real-time, modern & reactive applications, using the programming language the most used in the world, the interpreted language powered by the fastest engine in the world, a language with an ever-growing popularity and use cases that even gnome-shell used it to create apps, a language with "good parts"... and bad parts, the most misunderstood language, here is Node.js & Javascript!

Some backgrounds of mine

Since 2007, I was a big fan of PHP.

PHP has a friendly learning curve and open up a whole world of possibility.

At that time, it was crystal clear to me that I MUST master this technology. I'm not into Windows, so ASP, .NET & co was not fitting my usual dev environment and philosophy. I have always found that Java's technologies was bloated and cumbersome. Perl looks too esoteric for me, Python looked interesting, but PHP's ecosystem was already beating those two for web applications.

So, at that time, learning PHP was the good choice for me.

Later, I was hired to build a large application, a full-featured project management SaaS, with file-sharing and file-synchronization.

Then a new player appears: Node.js.

I first heard it when I was searching for a decent way of using websocket alongside with PHP. That was really a pain in the neck. However, the young Node.js was already the best solution for that task.

Later, I was searching for an alternative for my company's file-sharing/file-synchronization service.

PHP was not exactly the better tools for that.

I put a lot of love and engineering into my PHP algorithm, so it worked pretty well for years, but as people join and add thousands of files, it becomes more and more complicated to scale.

Meanwhile, internet has changed...

People were more and more used to real-time application. The old request-response server paradigm was begining to fall apart. Our file-synchronisation service used to receive a request for update every 2 minutes per client applications.

But then our customers wanted real-time.

They wanted that when a coworker add a new file, the file would appears immediately on all the company's computers.

Reducing the update delay was not an option: it would cost at least +25% of resources to support one request per minute per client, and still, the customers would argue that one minute is not real-time. Perhaps one request every 10 seconds would pleased them, but this service would cost us at least four times more resources, not to mention the scalability nightmare.

If our customers want real-time, we need a persistent connection: the client should be wired to the service, and Websocket is the solution. So again, I considered Node.js for that task.

I started to realize that the web was changing, and that the good old PHP doesn't fit the new deal very well.

So I started learning intensively Node.js in the middle of 2013.

And believe me or not: it's a true love story.

The Node.js paradigm was blowing my mind, this wonderful event-driven/asynchronous/non-blocking I/O thing can do wonders. This model is just what make NginX kick Apache's ass. Its natural affinity with real-time and cutting edge technologies was really appealing.

Finally, we can code both side with the same language (No! Do not insist! Java is NOT an option! Come on: nobody is using browser-side Java anymore, the Dark Age is over).

Also Node.js is capable of more than just webservices, it can be used as a general purpose language/runtime, for various applications, even desktop applications and games using node-webkit / nw.js or atom-shell / electron.

Also, I found out that Node.js has a really fine community.

Here people are enthusiast, people are curious, people are happy. People are humble here, something really rare nowadays, since the Java's spirit has spread the world with its arrogant and obscurantist mindset, and now the majority of coders are passionless, serious and pretentious people. As for PHP, I always found it has a bloated community.

So thanks to Node.js, I have finally decided to go out of the dark, and start publishing various tools of mine. Thanks to the wonderful NPM registry, it is really easy and friendly.

Keyboard inputs!

Raw mode is the other of the two character-at-a-time modes. The line discipline performs no line editing, and the control sequences for both line editing functions and the various special characters ("interrupt", "quit", and flow control) are treated as normal character input. Applications programs reading from the terminal receive characters immediately, and receive the entire character stream unaltered, just as it came from the terminal device itself.

The first thing you need to know: your program will not exit when you hit CTRL-C anymore. Instead, you will have to watch for CTRL-C and use process.exit() by yourself.

isCharacter boolean is true if this is a regular character, i.e. not a control character

codepoint number (optional) the utf-8 code point of the character, if relevant

code number or Buffer, for multibyte character it is the raw Buffer input, for single byte character it is a number
between 0 and 255

Usually, if the name argument's length is 1, this is a regular character, if it is longer, it is a special key code, like CTRL_C, ENTER, DELETE, TAB, UP , HOME , F1, and so on... But be careful! A single asian character (Chinese, Japanese and Korean) always has a length of 2, so you should not rely on that and instead always data.isCharacter if you want to know if it is a true regular character or not.

Note that there are few issues with the way keys produce inputs in a terminal application that you should be aware of.

That's not the lib that should be blamed for that, but the way terminals actually works. Have in mind that it's not a kind of keyboard driver that pass keys to our application, we are just reading from the Standard Input stream (STDIN). And that's your terminal that pushes bytes into that STDIN stream.

For that purpose, the matches argument contains all matched keys. This is because sometimes, the input stream produces code that matches many possibilities. E.g. ENTER, KP_ENTER and CTRL_M are all producing a 0x0d in STDIN. TAB and CTRL_I both produce 0x09, BACKSPACE usually produces a 0x08 like CTRL_H, ...

When multiple matches happens, Terminal-kit will pass as the name argument the most useful matches. By the way ENTER has a greater priority than CTRL_M, TAB has greater priority than CTRL_I and BACKSPACE greater priority than CTRL_H.

Actually, all Ctrl-letter combo key produce a control character, i.e. one of the 32 lower ASCII character. But most of those control characters are useless nowaday, so it is safe to use almost all Ctrl-letter except CTRL_M, CTRL_I and CTRL_H.

Finally, you should be aware that special keys produce input in STDIN that vary greatly from one terminal to another. E.g. there is rarely two terminals that produce the same escape sequence for all F1 to F12 keys. Terminal-kit try to abstract that away from you, but exotic terminals can still causes some detection troubles. That's because there isn't any standard for that.

Also, some terminals like Gnome-terminal will intercept function keys for their own stuffs, e.g. F1 will open the Gnome-terminal help window, F11 will go fullscreen, ALT_F4 will close the window, and your application will never get those intercepted keys. So, the best practice is to bind multiple keys for the same action in your application. If you are going to use function keys, try to bind a function key and it's shift or ctrl variant to the same action, e.g. F1, CTRL_F1 and SHIFT_F1: if the terminal intercepts F1, there are chances that SHIFT_F1 will work...

Even better, if it's relevant and you can afford it, allow your users to configure their own key binding.

When you are done with user input, you can turn input grabbing off with .grabInput( false ). The terminal will leave the raw mode and returns to the cooked mode.

MOUSE_BUTTON_RELEASED: a button were released, however the terminal does not tell us which one.

MOUSE_MOTION: if the option { mouse: 'motion' } is passed to grabInput(), every moves of the mouse will fire this event, if { mouse: 'drag' } is given, it will be fired if the mouse move while a button is pressed.

Again, there are some issues to be aware of.

Firstly, do not expect all terminals to emit all *_RELEASED subtype. You should not rely on this, or you should at least have some fallbacks. E.g. Gnome-terminal emits MOUSE_LEFT_BUTTON_RELEASED and MOUSE_RIGHT_BUTTON_RELEASED, but does not emit MOUSE_MIDDEL_BUTTON_RELEASED... don't ask me why... -_-'

Secondly, do not expect all terminals to support the option { mouse: 'motion' }. E.g. the KDE Konsole will only report the MOUSE_MOTION event-subtype when a button is pressed, the same way it works with the { mouse: 'drag' } mode.

Thirdly, some terminals intercept right click to display a context menu. Gnome-terminal used to do that, but it seems that newer versions (at least on my Fedora at time of writing) don't do that anymore when the terminal has switched to raw mode, which was done with .grabInput().

By the way, the good old Xterm works perfectly fine! Outdated UI/UX, but extremely reliable when it comes to raw features support.

The Ultimate Geek Touch: Terminal-kit even supports the mouse in the Linux Console by talking directly with the GPM driver if it is installed on your box. Seriously, I'm quite proud of that, since I have almost done reverse engineering to provide that. Yay, there is no documentation for the GPM driver, so one have to: read the source code, watch inputs and outputs, guess how it works, repeat.

Putting it all together

Here a small sample code that allows one to write anywhere on the screen, using arrow keys to move while other keys are echoed:

Misc inputs with the 'terminal' event

The terminal event is a general purpose event for all things coming from your terminal that are not key or mouse event.

The terminal event is emitted with two arguments:

name string the name of the subtype of event

data Object provide some data depending on the event's subtype

The SCREEN_RESIZE subtype is emited when the terminal is resized by the user. The data argument will contain the width and height property: the new size of the screen expressed in characters.

Finally, if .grabInput() was called with the { focus: true } option, a terminal event will be emited with FOCUS_IN or FOCUS_OUT subtype when the terminal gains or loses focus. Not all terminal supports that.

Next time?

So, we have learn many interesting things, but we have not explored all features Terminal-kit has.

Next time we will learn how to use higher level user-inputs methods, like .inputField().

This tutorial will focus exclusively on the terminal-kit lib for Node.js. Make sure to npm install terminal-kit before trying the given examples.

For those who have already done some C/C++ coding with Ncurses, you may know that there is a saying:

More than often, a programmer dealing with Ncurses will curse.

The goal of terminal-kit is to provide a simple and modern way to interact with the terminal. The design should be simple and intuitive, and yet give maximum power in the hand of the user. Hopefully, you will say goodbye to the good ol' Ncurses' days!

Moving the cursor

That cannot be easier!

To move the cursor relative to its position:

.up( n ): move the cursor up by n cells

.down( n ): move it down by n cells…

.left( n ): …

.right( n ): …

.nextLine(n): move the cursor to beginning of the line, n lines down

.previousLine(n): move the cursor to beginning of the line, n lines up

This creates a status bar with a white background and black text color, on the top of the screen. The update code saves the cursor's position and restores it, so it doesn't disturb the main text flow, that's why it still writes Mike says: "Hey hey hey!" as if no cursor movement happened.

Editing the screen

Now that we've learn how to move the cursor, let's see how to edit what is already displayed.

Let's see what we can do:

.clear(): clear the screen and move the cursor to the upper-left corner

.eraseDisplayBelow(): erase everything below the cursor

.eraseDisplayAbove(): erase everything above the cursor

.eraseDisplay(): erase everything

.eraseLineAfter(): erase current line after the cursor

.eraseLineBefore(): erase current line before the cursor

.eraseLine(): erase current line

.insertLine(n): insert n lines

.deleteLine(n): delete n lines

.insert(n): insert n char after (works like INSERT on the keyboard)

.delete(n): delete n char after (works like DELETE on the keyboard)

.backDelete(): delete one char backward (works like BACKSPACE on the keyboard), shorthand composed by a .left(1) followed by a .delete(1)

E.g., move the cursor and then erase anything below it to clean the area:

term.moveTo( 1 , 5 ) ;
term.eraseDisplayBelow() ;

For all erase-like methods, note that the screen is erased using the current background color! Hence, if we wanted to erase the screen, and paint anything below the cursor with a red background, we can modify the above code this way:

term.moveTo( 1 , 5 ) ;
term.bgRed() ;
term.eraseDisplayBelow() ;

This apply to .clear() as well, so term.bgBlue.clear() will erase the whole screen, paint it blue, and move the cursor to the top-left corner.

Also there is the advanced method .fullscreen( true ) (not chainable) that clears the screen, moves the cursor to the top-left corner, and if the terminal supports it, it turns the alternate screen buffer on.

If alternate screen buffer is available, when you invoke .fullscreen( false ), the screen will be restored into the state it was before calling .fullscreen( true ).

This is really a key feature for writing cool terminal application! Now you can code something that behaves like the htop linux command, using the whole terminal display area, and when users quit your app, the command history of their shell will be restored. Neat!

If alternate screen buffer is not supported by your terminal, it will fail gracefully.

Last words

Now the cool factor: you can change your terminal window's title with the method .windowTitle():

term.windowTitle( "My wonderful app" )` ; // set the title of the window to "My wonderful app"

Hehe ;)

Next time we will focus on input: keyboard and mouse!

I hope you have found this tutorial useful, and will be happy if you drop me a line or two!