TL;DR
- This post describes what I had to learn about Electron to be able to build a cross platform pomodoro timer that I’ve named Electric Tomato
- This post is a “How-to get started with Electron” for people familiar with HTML, CSS and JavaScript
- If you just want the timer you can get it here. You need npm to run it.
I first heard of Electron because of Visual Studio Code. Electron enables you to write applications using HTML, CSS and JavaScript that run equally well in Windows, Linux and macOS. You might have also heard of Atom Shell which was the previous name for Electron when it only powered the Atom editor.
So how does this work? What kind of tricks enable writing a desktop app using HTML, CSS and JavaScript? It turns out that Electron is Node.js packaged together with Chromium for rendering the HTML. Because it’s Node.js you can do things like access the file system. And because Chromium is used for rendering, you can create designs that look great everywhere because Chrome is so prevalent and stable (Chrome is based on Chromium).
Since .Net Core 1.0.0 came out I’ve been experimenting with it in Ubuntu. I’ve been using the latest 16.04 LTS and have been very happy with it (for the first time in ages everything worked perfectly right out of the box). Since I’ve been spending more and more time in Ubuntu I’ve been trying to find alternatives to applications that I use in Windows. One of them is a simple pomodoro timer (there’s gnome pomodoro, but I could not get it to work).
Since I was using Visual Studio Code a lot, and the editor looks great (I’m using it right now to write this post using Markdown, with a Spell checker and word count), I toyed with the idea of using Electron to create a pomodoro timer. But it wasn’t until I heard an episode of Hanselminutes where Jessica Lord (works at Github on Electron) was interviewed that I realised how easy it was to create an Electron app.
If you are a web developer than there’s not much you need to learn to be able to write Electron apps. As they mention in the Hanselminutes episode you can create your first Electron app in less than 5 minutes. Just clone the quick start and in main.js
change the URL in mainWindow.loadURL('file://${__dirname}/index.html')
to any webpage you like, and you’ll be able to launch it as a desktop app.
Since my first experience with Electron was building a pomodoro timer I want to share with you all I had to learn that was Electron specific to achieve that. That’s the rest of this post.
The basics
To get started you need to have npm installed. After you’ve installed npm the first thing you need to do is create a package.json
file. The easiest way to do this is to type
npm init
This will ask you a set of questions, such as how do you want to name your project, and will create the pacakge.json
file for you. Next install electron by doing:
npm install electron --save-dev
This will download electron, and also update package.json so that electron is listed as a development dependency (you can read about npm dependencies in the docs. You can leave that for later, it’s not important to understand the rest of the post).
Now you need to create your index.js file (your “main” file if you used the defaults in npm init).
The code you create on this file is the code that runs in electron’s “main process”. The main process is the process in electron where you have access to all of what Node.js offers, for example you’ll be able to access the file system.
Here’s an example of a simple index.js file:
var app = require('electron').app;
app.on('ready', function(){
//this is where all starts
});
The app
object is where you can control your application’s lifecycle. You can use it to handle events such as ready
, before-quit
, quit
, etc.
After the ready
event we can now create a window. In Electon’s parlance this will be a “renderer process”. You can think of a renderer process as a browser window.
We need to show something in that window, so create an index.html with some markup and save it, now update your index.js file to this:
var app = require('electron').app;
var BrowserWindow = require('electron').BrowserWindow;
app.on('ready', function () {
var mainWindow = new BrowserWindow({
width: 640,
height: 480,
});
mainWindow.loadURL('file:///' + __dirname + '/index.html');
});
BrowserWindow is how you create renderer processes. A renderer process is a window in the desktop environment that you are using. You can configure how that window will open (width, height, and more). The way you specify the contents of the window is through the loadURL
method where you can use __dirname
to refer to the folder where the index.js file is located.
The easiest way to test our simple electron app is to use npm to start it. Edit package.json and in the scripts
section add:
"scripts": {
"start": "electron ."
}
Now if you run npm start
in your command line you should see a window open with your index.html rendered inside.
Communication between the renderer and the main process
When you are in the renderer process you have limited functionality. There are certain actions that you can only do from the main process, like opening windows. Because of this Electron offers a way for the main and renderer processes to communicate with each other.
You can cause an event to be raised in the main process from the renderer process and vice-versa. Here’s an example of how you would trigger an event named “myEvent” with argument “myArgument” in the main process by a renderer process.
Renderer Process:
var interProcessComunicaton = require('electron').ipcRenderer;
interProcessComunicaton.send('myEvent', 'myArgument');
Main Process:
var interProcessComunicaton = require('electron').ipcMain;
interProcessComunicaton.on('myEvent', function(event, argument){
//event.sender is of type webContents, more on this later
//argument is 'myArgument'
});
The event in Electron’s parlance is named a channel, and you can have an arbitrary number of arguments. Also, notice that in the renderer process you have to use ipcRenderer and in the main process you use ipcMain.
If you need to send a message “someMessage” with argument “arg1” from the main process to a renderer process here’s how you could do it:
Main Process:
var theWindow = new BrowserWindow({...});
theWindow.webContents.send('someMessage', 'arg1');
Renderer Process:
var interProcessComunicaton = require('electron').ipcRenderer;
interProcessComunicaton.on('someMessage', function(event, argument){
//argument is 'arg1'
});
To send a message from the main process to the renderer process you need a the reference to the BrowserWindow
where you want the message to be sent to. You then call .webContents.send(channel, arg1, ... argN)
on that BrowserWindow
instance.
Remote
Because it’s so common to send messages to the main process form a renderer process to do things like close, minimize, maximise a window it is possible to use a remote to do that, here’s how you can minimize a window from the window itself:
Renderer Process:
var remote = require('electron').remote;
remote.getCurrentWindow().minimize();
Examples of other common things you can do (and that I did with electric tomato) are closing, .close()
, .maximize()
, and setting the window to be always on top, .setAlwaysOnTop(true)
.
Chromeless Windows
One really nice feature that is very simple to take advantage of and makes for nice looking UI’s is to turn off the chrome/frame of the window. To do this, on your main process when you create a new BrowserWindow
add frame: false
in the options:
var chromelessWindow = new BrowserWindow({
...
frame: false
});
Be aware that when you do this you lose the ability to drag the window around (the frame is where you usually click to drag the window around). To deal with this you can add an element to the page, for example a div
with height: 50px;
at the top of the page and set this style on it:
-webkit-app-region: drag
If you feel that all your window needs to be draggable by putting -webkit-app-region: drag
in the body of the page, be aware that any button inside the page will stop working (the click will trigger the dragging of the window). If you do this you have to make sure that every element that you want the user to be able to click on has the style: -webkit-app-region: no-drag
.
Some tips and things to have in mind
There are a few small things you can do to make your Electron app look nicer. One of them is to open windows with their background color already set. When you open a window in Electron it will always open with a white background by default and if the HTML and CSS you load for that window sets another color you will notice a flash from white to that color. To stop that do this:
Main Process:
var newWindow = new BrowserWindow({
...
backgroundColor: "#202020"
});
A thing that is probably common in Electron and that might not be obvious is how do you open a new window from another window (new renderer process from a renderer process). Turns out you can’t do that directly. You have to send a message to the main process and create the window there, for example:
Renderer Process:
var ipc = require('electron').ipcRenderer;
...
ipc.send('newWindow');
Main Process:
var ipc = require(‘electron’).ipcMain;
...
ipc.on('newWindow', function(){
var newWindow = new BrowserWindow({
//...
});
});
Or say you want to add a button to close the application (and all open windows from the app). You would have this code run in that button:
Renderer Process:
var ipc = require('electron').ipcRenderer;
...
ipc.send('shutdown');
Main Process:
var app = require('electron').app;
var ipc = require('electron').ipcMain;
...
ipc.on('shutdown', function(){
app.quit();
});
Even though it might be obvious by now, I wanted to mention something that I did not think about when I was creating my pomodoro timer. The JavaScript code you write that is not in the main process/main file is running in a browser. This might have implications that you are not expecting. For example I decided to use easytimer.js to create a countdown timer. Turns out that easytimer.js probably relies on setTimeout
or setInterval
to run on every second, e.g.:
setTimeout(function({
//a second has passed
}), 1000);
Turns out that browsers will throttle timeouts and intervals when the tab/window is minimized(I’ve seen it increase by more than 2 seconds). The consequence is that if you start and minimize the pomodoro timer it will take more than 25 minutes to get to the break. In the end I had to drop easytimer.js and create my own implementation.
Electric Tomato, my first ever Electron app
This post describes all I had to learn to be able to create a pomodoro timer that I’ve named Electric Tomato. I’ve tried it in Windows and Ubuntu.
If you don’t know about the pomodoro technique, it’s a way to help you get work done, manage distractions and avoid burnout. The idea is that you do work for 25 minutes followed by 5 minute breaks. When you are doing work, you are doing work and that means, no going on reading random stuff off the internet. During the break you relax. This might seem simple, but it really helps.
Here’s the snapshot of how Electric tomato looks like. The dark background is for when you are doing work and the light backgound is for a break:
The source is in my github at: https://github.com/ruidfigueiredo/electric-tomato
After getting the source from github, to run it first do a
npm install
You only need to run install once. To actually start the timer
npm start
I’m planning to package it for Windows and as a .deb for Ubuntu, but that will probably come in another blog post.
Another thing you might find interesting is another post I’ve written about creating a web page that looks like a desktop app. Might come in handy when you start building your Electron apps.