# Deskcal.log.1: Discovering the Electron

With a quiet evening at home, I decided to spend my bonus February day cobbling together a little application for viewing a Google Calendar from the desktop. Although a quick search reveals numerous options for this, it's always nicer to learn a new toolset by tinkering with it and building something yourself. I equipped myself with a fresh pot of coffee and a notebook, opened a new repository, and cracked on with **Deskcal**.

---

[Deskcal @ Github](https://github.com/cognophile/Deskcal)

### What to build? 🤔💭
Some background: I confess, I'm a tab hoarder. I've probably had some tabs open longer than I've had hair on my head. So for the new year, I thought I should start tackling this problem and cleaning up my messy tab habits in an otherwise well-organised existence. I toyed with building a Chrome extension to show me my upcoming calendar events but decided to build a cross-platform desktop client for viewing Google Calendar as that's one of my long-lived tabs on every desktop device I use. It also presented an opportunity to explore a tech stack that I'm considering for a future project. Though, despair not, I'll return to the Chrome extension idea soon enough. Anyway, back to the current idea.

### What to build... with? 🧱
Since I knew the application needed to be desktop-based and cross-platform, the natural starting point of **Electron** came to mind. It's one of those tools which, [though somewhat controversial](https://news.ycombinator.com/item?id=12119278), has real value to developers as it's fantastically easy to get a product up and running quickly. And as my goal was to build this in an afternoon, easy and fast were the droids I was looking for 👌

For the uninitiated, [Electron](https://www.electronjs.org/) is an open-source framework originally developed by Github for use in their [Atom Editor](https://atom.io/) (get it?), which allows users to create cross-platform desktop applications using widely accessible web technologies, meaning JavaScript, HTML, and CSS. It's a powerful tool for making the desktop user and developer experience align with that of a web application, whether existing or not. Some great examples of other products using Electron in their stack include [Slack](https://slack.com/intl/en-gb/downloads/mac?geocode=en-gb), [Github Desktop](https://desktop.github.com/), [Zulip](https://zulipchat.com/apps/mac), [Notion](https://www.notion.so/desktop), [Microsoft Teams](https://teams.microsoft.com/downloads), and a [bunch more](https://www.electronjs.org/apps)! As the range of applications illustrates, the possibilities with Electron go well beyond simply displaying HTML and JavaScript as it can access native platform libraries and tools!

> Want to hear more about how Electron is used beyond small projects? Check out a recent [Software Engineering Daily podcast interview with Anuj Nair of Slack](https://softwareengineeringdaily.com/2020/02/27/slack-frontend-architecture-with-anuj-nair/) where Anuj enlightens on the Slack frontend stack and some interesting engineering problems!

If you're sufficiently curious now, Electron goes one better than just stepping you through a nice tutorial and provides a desktop application to introduce and tour the possibilities of Electron development, namely [Electron Fiddle](https://www.electronjs.org/fiddle). The origins of the name are a nod to [JSFiddle](https://jsfiddle.net/), signifying the purpose of the application as a sandbox playground environment for tinkering and experimenting without having to install all the dependencies as these are built-in! With zero prior Electron experience, this is where I started for a quick tour and play with building electron applications which gave me a nice understanding of how simple it is to configure and start an electron application!

### Getting started 🏃‍♂️
Having played with Electron Fiddle, it was time to figure out how to get started with local electron development. As it's JavaScript-based, the natural start point was installing Node.js and NPM, which is effortless - head over to [Nodejs](https://nodejs.org/en/) to install both in one go and confirm it in the terminal using `node -v` and `npm -v`.

As a node newbie, a little bit of research was needed to get familiar with how to initialise the project, install dependencies, run it, and build it.  After a browse of the [NPM documentation](https://docs.npmjs.com/cli/install), with a dash of  `npm init` and a pinch of `npm i --save-dev electron`, the project was initialised and ready for building.

To turn the mass of code into a deployable application, Electron has a few options. Namely, [Electron Packager](https://www.npmjs.com/package/electron-packager) and [Electron Builder](https://www.npmjs.com/package/electron-builder). As I understand it, the difference distils down to the packager wrapping up the project into executables, whilst the builder (which uses the packager) adds some sugar on top such as creating automated installers. For now, I went with the packager to get underway and decided to revisit the builder, later. Since I knew it was possible to declare build scripts with npm via `package.json`, I added the packager as a development dependency by tapping `npm install electron-packager --save-dev` into a terminal in the project directory.

All that was left was to `npm install` and `npm start` to run the Electron development wrapper. Thankfully, the kind folks at Electron have distilled this into a ['Getting started' guide](https://www.electronjs.org/docs/tutorial/first-app) which covers the same content as the Electron Fiddle template project but with more detail, making starting out even easier.

### The construction ⚒
Getting started requires informing Electron which script to use as the entry point for the application at start-up. This can be found in `package.json`, as shown below. In this case, I named it `app.js`.

```
{
  "name":"Deskcal",
  "productName":"Deskcal",
  "desktopName":"Deskcal",
  "version":"1.0.0",
  "description":"An unofficial cross-platform desktop Google Calendar application",
  "main":"app.js",
  "repository":"https://github.com/cognophile/Deskcal",
  "keywords": [],
  "author":"cognophile",
  "license":"GNU General Public License v3.0",
  ...
}
```

To jump-start the project, I utilised the existing Electron template after a little analysis of what it's doing since it delivered all the basic interactivity with the Electron API to initialise, open, and close the application.

```
// Import the modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

  // and load the index.html of the app.
  mainWindow.loadFile('index.html')

  // Open the DevTools.
  // mainWindow.webContents.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
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', function () {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow()
  }
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
```

To start with, I tinkered with the `BrowserWindow` configuration object parameter to customise the properties of the initial application window. As the `BrowserWindow` [documentation](https://www.electronjs.org/docs/api/browser-window#class-browserwindow) details, there's a fair amount of options available. To improve the user experience, I set some minimum and default window sizes to ensure that the experience of using Google Calendar in this format is pleasant and, well, usable. The user can still resize the window to suit them best, of course.

The window is then set to be frameless and to hide the menu bar by default until `Alt` is pressed, which gives the application a more seamless appearance. Finally, it's setting the application icon. Interestingly, macOS and Windows can have their icons linked during the build with `electron-packager`, whereas Linux cannot, so the icon has to be set from the window configuration.

```
window = new BrowserWindow({
title: app.getName(),
minWidth: 800,
minHeight: 600,
width: 1600,
height: 1000,
resizable: true,
transparent: false,
autoHideMenuBar:true,
titleBarStyle: "hidden",
icon: process.platform === 'linux' && path.join(__dirname, 'resources', 'icon.png'),
    webPreferences: {
        nodeIntegration: true
    },
});
```

Now that we've got our window, let's put something in it! Since the aim of this application is simply to wrap around the Google Calendar website to provide the full experience, but locally, all that was required was to point the application at the URL. This can be done by importing the `path` module and issuing a one-liner. In my case, I scoped this into a function to make it a little clearer what's going on later, when it's called within the function which is run when the window load is completed.

```
const path = require('path');

let window
...

/**
 * Load the remote calendar service
 *
 * @author Cognophile
 * @returns void
 */
function loadService() {
    window.loadURL('https://calendar.google.com')
}
```

If you run this application now, it'd work, but the menus might look a little empty. This is because it's something we have to set. The easiest way I came across to do this is by importing the `Menu` class and [setting the menu](https://www.electronjs.org/docs/api/menu#menusetapplicationmenumenu) through a [template](https://www.electronjs.org/docs/api/menu#menubuildfromtemplatetemplate). The template can simply be an array of objects corresponding to the [MenuItem](https://www.electronjs.org/docs/api/menu-item) interface.

```
/**
 * Fetch the window template menu and shortcut bindings
 * 
 * @author Cognophile
 * @returns void
 */
function getApplicationTemplateBindings() {
    return [
        {
        label: "Application",
        submenu: [
                { label: "About Application", selector: "orderFrontStandardAboutPanel:" },
                { type: "separator" },
                { label: "Hide", accelerator: "Command+H", click: function() { app.hide(); }},
                { type: "separator" },
                { label: "Quit", accelerator: "Command+Q", click: function() { app.quit(); }},
                { type: "separator" }
            ]
        },
        {
            label: "Edit",
            submenu: [
                { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
                { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
                { type: "separator" },
                { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
                { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
                { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
                { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
            ]
        }
    ];
}
```

This is consumed by the static calls on the Menu class in the function which acts as a callback injected into the `app.on('ready', [callback])` call which starts our actions once the native application has been initialised.
```
    ...
    const appTemplate = getApplicationTemplateBindings()
    Menu.setApplicationMenu(Menu.buildFromTemplate(appTemplate)); 
    ...
```

#### About Window
After finishing the bulk of the application,  I didn't feel the standard 'About' window was sufficient enough and wanted to display more information in it. I took to npm and found `[about-window](https://www.npmjs.com/package/about-window)`. With a little JavaScript magic to replace the standard Electron about launcher in the template, I had a more detailed about window up and running in minutes with most information being collected from `package.json`. Kudos to the creators for their single public interface, documentation, and [example application](https://github.com/rhysd/electron-about-window/tree/718be20e9415329b466408cf6e029a8dc318b41d/example) 👍

![screenshot-2020-03-02-at-00.08.42-e1583107806766.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1621372000867/bfePCzmeG.png)

```
{ 
  label: "About", 
  click: () => openAboutWindow({
    icon_path: path.join(__dirname, 'resources', 'icon.png'),
    package_json_dir: __dirname,
    product_name: 'Deskcal',
    bug_link_text: 'Found a bug?',
    bug_report_url: 'https://github.com/cognophile/Deskcal/issues/new',
    homepage: 'https://github.com/cognophile/Deskcal',
    use_version_info: true,
    copyright: 'Copyright (c) cognophile 2020',
    adjust_window_size: true,
    win_options: {
        parent: window,
        modal: true,
    },
    show_close_button: 'Close'
  }),
  accelerator: "Command+,"
},
```

#### Frameless Dragging
As it turns out, setting a `BrowserWindow` to be frameless causes means we lose the ability to freely reposition the window by dragging it, as no title bar area exists on the window to target the native APIs. This meant it had to be added manually, so I set about searching for how it worked and what options existed. Many solutions indicated that since Electron has access to the `webkit` API so adding the below CSS will re-enable it without selecting title text or dragging page elements, as described by [Hans Koch on StackOverflow](https://stackoverflow.com/a/44823888/5012644).

```
.titlebar {
  -webkit-user-select: none;
  -webkit-app-region: drag;
}

.titlebar-button {
  -webkit-app-region: no-drag;
}
```

Now, there are a few NPM packages out there for doing this, such as `[electron-drag](https://www.npmjs.com/package/electron-drag)` which I considered, but I didn't feel such a small change warranted the inclusion of a whole package and its dependencies. After all, if NPM package bloat can be avoided, why not. As it turns out, Electron has [documented how to re-enable window dragging](https://github.com/electron/electron/blob/master/docs/api/frameless-window.md#draggable-region) for exactly these situations!

Since Deskcal simply wraps the Google Calendar web application, the simplest method to re-enable frame dragging was to inject it into the page by attaching it to the document and its body once the Google Calendar page had finished loading (thanks, [nfours](https://discuss.atom.io/t/inject-css-to-loadurl/26845/3)!). This can be done by adding the below after the `loadUrl('...')` call within my `loadService()` function.

```
window.webContents.on('did-finish-load', function() {
    window.webContents.insertCSS('html,body { -webkit-app-region: drag; !important; }');
});
```

And voilà!

#### Update

As it turns out, this CSS injection isn't as suitable when using the application on other platforms. Though macOS seems to be able to interact with the window content despite the catch-all targeting of `html, body`, Linux, for example, cannot, and it ends up attempting to drag the window from interaction anywhere. Thus, users can't resize or interact with the calendar itself. So rather than re-enabling the frame, I targetted the `header` region in the calendar page to ensure the dragging is not applied to the lowest-level of elements. With a little tidying of CSS into a dedicated file, `v1.0.4` was ready and pushed!

### The build 🏗
#### Scripting it up
Since `npm start` is for a development environment running within an Electron debugging wrapper application, I had to build the application using the `electron-packager` to output platform-specific runnable for users to, well, use! For more details and options, you can visit the Electron page about [Application Distribution](https://www.electronjs.org/docs/tutorial/application-distribution).

```
"scripts": {
        "start": "electron .",
        "build": "npm run build:macos && npm run build:linux && npm run build:windows",
        "build:macos": "electron-packager . --overwrite --asar --out=dist --ignore='^media
```

The `start` key here, when run through `npm run [script]` is equivalent to npm start. But the important parts are the build tasks, for which I must give acknowledgement to [Daniel Chatfield](https://github.com/danielchatfield) for his work on these build scripts in the [Trello Linux](https://github.com/danielchatfield/trello-desktop/blob/master/package.json) desktop client.

#### Running it
On the first run of these builds, I found that the Windows build task requires Wine to build on macOS and Linux. I managed to install Wine via Homebrew on my macOS device, thanks to [David Baumgold](https://www.davidbaumgold.com/tutorials/wine-mac/#part-2:-install-wine-using-homebrew), by running `brew cask install wine-stable`. However, when trying to build again, I ran into the below error.

```
Error: Cask 'wine-stable' requires XQuartz/X11, which can be installed using Homebrew Cask by running:
  brew cask install xquartz

or manually, by downloading the package from:
  https://www.xquartz.org/
```

To resolve this,  I obviously installed XQuartz with `brew cask install xquartz` and then re-run `brew cask install wine-stable`. Wine then took care of installing any other packages needed when building and all ran smoothly!

#### Setting up Continuous Integration
To enable continuous integration for this project, I once again turned to my old pal, Travis, who was happy to help. The environment definition requires stating it requires NodeJS for anything JavaScript-based, the documentation for which lives [here](https://docs.travis-ci.com/user/languages/javascript-with-nodejs/). After which, before the package installation happens it instructs Travis to [install the environment dependencies](https://docs.travis-ci.com/user/installing-dependencies/), namely Wine.

```
language: node\_js
node\_js:
- '13'

before\_install:
  - sudo apt-get update
  - sudo apt-get -y install wine1.6

install:
  - npm install

jobs: 
  include: 
    - stage: build \[macos\]
      script: npm run build:macos
    - stage: build \[windows\]
      script: npm run build:windows
    - stage: build \[linux\]
      script: npm run build:linux
```

Once everything is installed, it'll each build in its own job, making reporting much clearer on the Travis dashboard. Though I'm familiar with the concept as a user of Gitlab CI, I'd not used Travis Jobs before so I thought I'd share the documentation pages I found particularly enlightening for others here.

- Travis [Job lifecycle and orders](https://docs.travis-ci.com/user/job-lifecycle/)
- Travis [Build stages](https://docs.travis-ci.com/user/build-stages/)

### Conclusion 🏁
Despite just building a wrapper application, I found Electron to be great for getting up and running, fast. As a desktop client framework, its accessibility of using the typical web tech stack means the possibilities are countless in terms of design and functionality and a great number of developers (whether early or late in their career) can jump in with a low barrier to entry and have fun!

This was a fun little afternoon project to get my head around how to start an Electron project, giving me familiarity with the packaging, construction, and building with it for when it comes to doing some more advanced Electron projects I've got on the to-do list. Since I only built a simple desktop wrapper around a website, building more interesting applications with Electron is well-covered in is this [freeCodeCamp article by Carol-Theodor Pelu](https://www.freecodecamp.org/news/how-to-build-your-first-app-with-electron-41ebdb796930/) which is a natural next-step read, so check it out!

