Electron Fundamentals
Learn About Electron
Intro
Welcome to this introduction on Electron. I'm so happy you're here, and I hope you're ready to have fun learning with me. At the end, I hope there's a great tool in your pocket for creating desktop applications. It's going to be a good time, and you're going to learn a lot. Let's get started. Electron is a technology made by GitHub. It's now open source as well, so there are hundreds of other contributors. Previously it was called Atom Shell. Today it is a great option for using web technologies such as HTML, CSS, and JavaScript, to create native desktop applications. In order to get this job done, Electron is the melding of two great technologies into a new, interesting combination. It bundles the Chromium content module for UI display, this is the open source part of Chrome, and it bundles Node.js and its via JavaScript runtime. When building with Electron, the output is a native binary that uses these two tools internally and runs just like another native application. Electron allows you to create applications that run on Mac, Windows, and Linux platforms.
Prereqs
In this course, it will be important for you, as a learner, to be familiar with the basics of JavaScript. It is the main language of the demos, and we'll be using all language constructs available on the current latest version of Node.js. Basic knowledge of CSS is also important. This is how we'll be laying out the UI of our applications. Finally, a basic familiarity with Node.js is a plus, because we'll be running various npm commands in our setup and using CommonJS modules in our application.
Why Web Tech?
So why would you build an application with Web Tech, specifically a desktop application? Many companies have a website and web developers, fewer have specialized desktop application programmers. If you don't have the budget or can't find a specialist, this mixing of Web Tech and desktop platform might be compelling to you. Maybe you just love web technology and want to reuse your skills on a new platform. It may at first seem like an odd combination, Web Tech and desktop, but anyone who had to layout UIs using Java Swing's layout managers, might actually be happy to use CSS to layout UIs. In the end, this is just another option to create your applications that will appear in yet another channel of distribution for your users to find and enjoy.
Why Desktop Apps?
And if the tech choice wasn't unexpected enough, why create a desktop app at all? It's true, the web and other platforms provide great options and reasons to use those platforms, but when you consider where you, yourself, spend your time every day on the computer, you'll likely find that you still find great utility in some core desktop applications. The body of apps that have used Electron to create successful products is growing. You've probably heard of or even use some of these yourself, Atom, Visual Studio Code, Light Table, are all code editors that have been created using Electron. Discord is a chat tool, GitKraken is a Git UI, and Postman as a REST tool, are all useful utilities made with Electron. There is even a new compelling browser that is being built on Electron called Brave. As we'll see, there are some great features that are immediately available to you as a desktop application that might be problematic or simply not available on another platform like the web.
Electron Abilities
Let's point out some of the differentiating abilities of Electron. Enumerating some of the highlights will help us create a context in our brain for when and where this technology might help us in the future. If we're considering features that might be supported by these abilities in the future, then Electron might be a fitting solution. Electron offers access to the native desktop platform, be it Mac, Windows or Linux. This means that these applications can access things like the filesystem. It has access to OS systems such as the task bar, dock icons, recently used documents, media previews, window title bars, native notifications, power management, and network connectivity detection. It can help you define native menus with custom options, even global shortcuts. You have access to the box on which your app is running, so node modules, such as child_process, give you the ability to fork and run any process on the machine that you have permission for as the app-running user. Perhaps obvious, but as a desktop app it is offline by default. Your app is previously installed on machines, and can run without access to the network. Because this is the default, support for this mode is much more straightforward than it is on the web. As an installable app, your Electron app will feel just like another native app. It will live outside the web, sit in the dock, and run in the background on your operating system. You can command tab to your app, it just feels more elegant. On Electron, you have full access to the Node API. There are some sharable modules that you can bundle in your browser-based apps today, but now you get all of them. Native modules that you have to compile are not even available in browser tools like Webpack. For example, the LevelDB package. Here you are also able to use all the node-supported syntax, such as CommonJS modules, without transpolation. Thankfully, some of the headaches of producing an application for a web browser environment are not here. There are no cross-browser support problems. You're targeting one browser, specifically Chrome, and you have a good feature support there. You don't have the security or sandbox restrictions of a browser either, such as CORS and device permissions. Finally, this is a way for you to get your product and content into new channels of distribution. Electron as a technology will allow you to compile your code binaries for each desktop platform. So let's check it out for ourselves and take the first easy steps to run Electron, and see how it feels.
Try out Electron
Environment Setup
Now is the part we've actually all been excited for. We're going to get in and really try out Electron. You'll see some of the basic APIs, you'll see some of the basic technology, and you'll see that there is really a low barrier to entry. It's an exciting technology, and I hope you'll like your first tastes. You are going to want to make sure that you've installed Node by going to nodejs.org. This can be done by going to their website, or for more advanced users by using the excellent NVM tool, which you can find on GitHub. Next we'll want a place to put all of our new project files. Each project should get its own directory. We'll call our project ipc-demo, after some of the features of Electron that we're going to show within this demo. First, we want to initialize this directory with a package JSON containing some of the description of our project. Notably, here we'll install electron-prebuilt, and we'll save it to the dev dependencies. Electron-prebuilt is a package that has already been constructed for your native operating system. This allows you to not have to build Electron from source, rather you will have a Mac-specific or Windows-specific or Linux-specific distribution of Electron with Node and Chromium prebuilt for you. Once you're all installed, open the project in your favorite text editor or IDE. Now everyone's project is set up a little differently, but we're going to set the main entry point of our application to src/main.js. This is the file that will be entered first when we run Electron on our app. And then to make running Electron easier, we'll add an npm script that describes what we're running, Electron, and the directory that we're running it on, which is our current project directory. If we run npm start in the console, it works without error.
All the Tech
Now we're ready to add some source code. Here we want to just touch all the tech in Electron, which remember, is just Web Tech, HTML, CSS, and JavaScript. Let's get a quick feel for where all this tech goes in an Electron app. First let's go to Main and confirm a little better that the script actually works. So we'll add a console.log and then start up our application, and sure enough, it prints main. Now, of course, the first thing that we do when we start building an Electron app is import Electron. Notice that we can import it with the electron name and not electron-prebuilt. It is that same package. On the Electron package there are a number of sub-modules. We're going to use the one called app, and import that. Now app has an event called ‘ready', so on(‘ready'), much like a jQuery onReady, or something to that effect. We are going to console.log and just make sure that Electron spins up and is ready for us to do some future work. And sure enough, it works. Now notice that when we ran our Electron app there was no UI yet, so we're going to import the sub-module of Electron that allows us to see a UI. It is called BrowserWindow. So on(‘ready'), we're going to instantiate a new BrowserWindow, give it some basic dimensions. So when npm is starting now, we actually see a browser window pop up in a native UI on our desktop. It's awesome. It's just this easy. Now let's do a couple of other things to manage the lifecycle of this browser window. We want to make sure that we're good citizens here, so let's save it to a variable, and then when we detect that this window is closed, we're actually going to null out this reference so that it can be garbage collected. Let's add a console.log and make sure that this is called. And sure enough, it is. Now we're ready for a new type of file. Let's make an HTML file. Remember, HTML is the UI technology of the web, and we're going to use it to back our application's UI. HTML in Electron looks just as it does in any other web application. We're going to have an HTML root, head, body, all of the things that we're familiar with. Let's just write out some text here, and make sure that the UI will appear as we expect. We start our Electron application again, but still we see no UI. There must be a piece that we're missing, and there is. This BrowserWindow is instantiated, but it's not actually loading any content, so we're going to use the loadURL API to put in a reference to our HTML file. The interesting thing here about what we are loading is that we're using the file protocol instead of something that could be served off of a web server. Remember, we're running on the local operating system, so we have access to the filesystem. And there it is, it works. Now as cool as that piece of text is, we can make it a little cooler. Remember, we want to use all the pieces of Web Tech, so let's go on to our Style Sheets. We import it the exact same as we usually would, and we use the exact same kind of selectors as we usually would here. So let's set up some basic style. It's not too interesting, but it just lets us know that the CSS is working. And there it is again, very easy to get this thing integrated. So that leaves one piece left, and that's our JavaScript. We can include it the same way we usually do, by using a script tag, but within that script tag you'll see that it feels quite a bit different. Immediately it almost begins to feel like Node.js inside of this HTML file, which is a little weird at first. It feels like Node.js, because we're using CommonJS modules using the require statement. We'll require a file that we'll name renderer for now, and we'll make a new js file, and just make sure that it's being loaded. There's nothing interesting here yet, just a console.log. When running this application, we'll see our URI, as we are used to, and now you'll notice that our console.log is appearing in a new place, a different place from before. Before we were in the main module, now we're in the renderer module, and the console.logs are actually appearing in the renderer, or in Chrome's Dev Tools console. And that's it, we made a connection to all the types of basic technology available to us in Electron.
Processes Intro
In the demo, we pointed out some differences in the files and processes that we were in, but let's make some more formal distinctions. In Electron, there are always multiple processes running at once. The two types of processes are Main and Render processes. There is always one main process. It is the process that starts first, there's just one, and it acts as the main controller of the app. It manages the application lifecycle, it's headless, in other words it has no UI, but it is in charge of spawning new render processes. Each render process is usually represented in the app by a UI window. There can be many render processes. This is the part of Electron apps that a user will see and interact with. It's the part of the app that really makes an Electron app an Electron app, when it could otherwise just be a Node script. Render processes, while usually visual, can also be used for creating other processes for concurrent work. Each process type has its own API. There are different modules provided in Electron that are available or unavailable depending on what kind of process your code is running in. Because each of these processes must work together to accomplish some task, there is a mechanism for those processes to coordinate and communicate with each other. It's called IPC, or inter-process communication. Each process type, Main and Render, gets its own IPC module. Each IPC module is an instance of Node's event emitter, thus the APIs for this IPC modules feel like most other pub-sub APIs. Here's an example IPC flow. The render process captures a user event, like a button push. When the button is pushed, the render process broadcasts an event called ‘start'. The main process listens for the start event. Once it hears it, it sends all of its child render processes a processing event. Each of the renderer processes were listening for that, and when hearing it, update their UIs to indicate to the user that something is happening.
Countdown Demo
We're going to implement that exact IPC flow in a toy demo app. We're going to make a countdown timer. Let's pick up where we left off in our previous source code. Let's start with the guts of what a countdown timer must do, and implement a timer. We'll make a new countdown module. It has one function called countdown. We'll start at 10, and then at an interval we'll decrease that count. If eventually we come to 0, we'll say it's over. To clear this interval, we must save the timer, so let's create a variable here, and make sure that this runs every 1 second, great. To test the countdown is working as we expect, let's just call it in our main module. In order to test that it's actually working, let's go back in and add a console.log. Now when we run the app we see the console, the countdown happening, it starts at 9 because of the place that we decided to log this, so it's a little low. Now if we're going to replicate the IPC flow that we talked about in the previous section, we're going to need to have a button to start this event flow rolling. So let's create a start button, give it an id of Start, and then go to our JavaScript where we capture the button and add a click handler. Let's just verify that the click handler works correctly by console.log and (‘start clicked'). Starting up the app again. We can click on the Start button. The console.log is happening in the Chrome dev tools because we clicked and we logged within the renderer module, not within the main module. Now that we know our click event is working, we're going to import the IPC renderer sub-module from Electron. This will be the IPC module that we can use within the renderer module. Instead of logging now, when we click start we want to send a new event, and we're going to identify that event by the string ‘countdown-start'. This is up to us to decide what we're going to call it. We could call it anything. The flip side to sending the countdown-start from the renderer module is that the main module must listen for a countdown-start. So within the main module we'll capture that event, and we'll make sure to import IPC, but this time ipcMain, so that we can use this inside the main module. To test that it works, let's console.log, click the button, and sure enough we caught it. Now let's replace our console.log with what we actually want to do when we receive the buttonClicked event. We're actually going to start our countdown, except now we need countdown to act a little differently. We need something to come out of countdown. At the interval we need to know what the count actually is so that we can take that count and send it back to the renderer module. To send it back, we're going to grab mainWindow.webContents. WebContents is just an event emitter instance. Now we're going to call send on a new event here called ‘countdown', sending the count. The count will come from the tick function that we send into the countdown function. Now, when the count is decremented, we'll send it to the callback function. Now we have main sending an event back to the renderer, and we have to make the renderer care about it, so we'll pull in the IPC module again, and now listen using the on API, for the countdown event. The countdown event will receive the event and any attributes or arguments sent with the event, so here we'll capture the count. When we receive the count, we want to display it on the screen, so let's put it into an element called count. We don't have that in our HTML yet, so let's switch to our HTML and implement the count div. Now when we click Start nothing happens. There's a couple different layers that this has to go through, so let see what's the matter. Countdown-start looks okay, we're sending it correctly. Let's make sure that the count is coming back from the tick function. Logging it, it looks like it is. Ah, here in the renderer we're listening for the countdown to come back to the renderer, but we're actually misspelling the name of the event. Spelling it correctly let's try it again. We click Start, we see the logging, and now in the UI we see the countdown. It's coming back into the renderer, we're making it update in the HTML, we've come full circle with the IPC. Notice that the countdown doesn't go quite low enough. This is because we are clearing our interval a little bit too soon. We still want to see our 0 coming into the UI, so let's take this down. And just so we don't have to wait so long, let's move our count to 3 instead of 10. As a next step, let's make this look a little better. Let's introduce a new container div, and we'll put the count, a new title, and the start button within this container. Let's add some classes where we don't have any yet so that we can make the appropriate style adjustments. Back in our CSS, let's just remove what we had before and create selectors for our new elements. With the container, we'll go straight to flexbox. Remember, a great feature of Electron is that we can code any of the features that Chrome only supports without having to worry about a cross-browser compatibility environment. Now when we restart our app, we should see something a lot nicer. Hmm, I don't have that nice pretty pink color I was expecting from that RGB value. Looking at it, check it out, we added an a, but no alpha value, so this won't work. We start it again, and great, it looks good. Even better, it still works. We're counting down in style. Now finally, you might be asking yourself, why would we go through all the work of IPC just to accomplish what could have been done within the renderer module itself? Well, this is a pattern that we want to use for future more complicated scenarios. For instance, in this case what would the code look like if we had multiple windows instead of just one, multiple windows counting down simultaneously. Let's try it. Let's go back into our main process and instead of main window let's create an array of windows. Then when we instantiate each new browser window we'll push it onto that collection. Let's do this three times. Now that we have multiple windows, when we receive a start click from any of those windows, we're going to want to send back the count to each of the windows in turn. Each of those windows has its own WebContents module, so we should be good to go. And now starting the app, if we split this apart we see we actually have three separate windows because of the three browser windows that we instantiated. Clicking start, they all work, it's awesome. Now you know how to talk between modules using IPC. Great job!
Experiment with Native APIs
Experiments Intro
Now that first taste of Electron was pretty good, right? These next bits will be even better, because we're going to take a look at what makes Electron uniquely special. These are the desktop APIs that this platform allows us to use. We're going to take a look at each in turn, and see how we might use it to our creative benefit. Let's have a look. We're going to create five quick projects where we can try out some Electron APIs in turn. Each of these projects will be standalone. A couple of the later projects will reuse some knowledge gained about particular APIs in earlier projects. First we'll try out the application menu. Next, we'll place our app in the system tray of the OS. Next, we'll demonstrate our ability to access the system clipboard programmatically. Next, we'll take a screenshot from the desktop. Finally, we'll demonstrate the ability for Electron to use other binaries on the host machine.
Application Menu
For this application menu, we're not making a particular app, we're just experimenting with the application menus to get ideas on how we might use this in a future project. The application menu is that bar of dropdown menus that usually appears at the top of the screen or window. It commonly contains menu options like File, Edit or View. In Electron, it's called the Menu module. Let's try it out. Now every project that we make will need its own directory. This one will be no different. We'll start and see this pattern a number of times. We're going to make a new directory, and immediately call npm init. We're going to pass -y so that we don't have to answer all the default questions again and again. Next, open your favorite text editor so we'll have a place to do our coding. Now npm init gave us a package.json, so we're going to open that up and make a few changes. As before, we're going to change the main script, the entry point to our app, to be src/main.js. Next we're going to make a convenience script for calling npm start, which will in turn invoke Electron on the current directory. Now for us to use Electron we must install it, so we'll do an npm install electron-prebuilt. We're going to save that in our dev dependency so that it makes a change to our packagejson. Once that's downloaded, we're ready to go back into our editor and create our main.js. We're going to import Electron, or course, and then grab the app submodule off of Electron. Now, when we know the app is ready, we can start doing things for our Electron app. For now, we'll just verify that this pattern is good to go, and we'll console.log. So calling npm start, we see that the console is actually logged, and we know that this pattern must be appropriate. Now let's get rid of the console.log and create a BrowserWindow. Remember, this is the viewable part of our Electron app. There it is, it looks great. Now by default a BrowserWindow comes with its own set of menu items. These are things that appear in the application menu that you didn't have to programmatically put there, it's a part of the Electron defaults. But we want to take that and make it our own, so we're going to use the Menu submodule. The Menu submodule will be the way that we control what actually appears in the application menu. The main API for menu is buildFromTemplate. Template is just going to be a standard data structure that this API expects. It will be an array of particular menu items, and menuItem can have a Label. This is the UI that's viewable. In Mac OS X, the operating system I am currently working in, it's the convention that the first menu item will be named after the application itself. Electron gives you an API to retrieve the application name using Electron.app.getName. This name is set in the package.json using the attribute "productName". We haven't set this yet, so let's take care of that now, there. Once we build the menu, we actually have to set it as the menu for our application, so it's a two-step process. Now you'll see the Label hasn't actually changed. This is because when we're running Electron in this development mode, it won't change the name of the menu item. We could name it whatever we wanted it to be. We could change that label to be anything, and it still wouldn't show up, it wouldn't change. Once we learn to package our applications in later modules, we'll actually be able to observe this first menu item changing in OS X. For now, let's go into this first menu item and create a submenu. These are going to be things that appear in a dropdown. So let's make the first one an About link, and we're going to use this same name in that label. If we start up the app and pull down the dropdown, we see that we have a new submenu item, it looks great. When we click on it, though, it does nothing, so let's take care of that. To provide a click handler, we need a function called click. For now, let's just make sure that this click handler is activated when we click on it in the UI. And it is. Now let's try a couple other things. Let's try this new Type attribute. There are several different types that can be used, these are enumerated in the Electron documentation. Here we'll demonstrate the simplest type called ‘separator'. This is simply a horizontal bar that is a visual separator. If we create another menu item below that, called Quit, it should appear below the separator, and it does, great. Let's make that ‘Quit' link actually do something now. If we invoke app.quit, which we have access to, then when we click it it actually quits the app. Finally, let's look at the accelerator attribute. This is another name for a keyboard shortcut. When we're in the Electron app, if we type this shortcut, it will actually invoke this particular menu item's click event. If we use the shortcut, instead of clicking on it with our mouse, it still quits, awesome. Next let's look at another attribute called Role. Role is applicable in OS X, where it maps to a specific menu type that the OS recognizes. Electron knows about some of these, and so it allows us to actually apply logic to these based on a role only. If we applied the role About, and then click on this link, OS X knows to pull up the About dialog. There are several of these, and the ones that I found to use have been found in the Electron documentation. The menu API is great for setting up native menus, their app level, they have that native feel, they're up and out of the way from the other UI. They're a great place for stowing this heaps of functions and settings for your app.
System Tray
Again, as before, for the system tray we're not making a particular app, just showing a quick demo of how you might use this API. The system tray is that place in your OS where quick access or long-running apps store an icon or a menu. It sounds like a privileged place to be, right? Let's try putting our app there. We'll use Electron's Tray module. We'll also see, again, how we can incorporate the Menu module. This one will be pretty short and sweet. Again, we'll make a directory for our Tray demo. Next, we'll initialize this directory with a package.json. Next we want to make sure we have Electron installed so we can use it in our app. Open up the directory in your text editor of choice. Let's edit that package.json, again, changing main to have a new entry point for src/main.js and creating our npm start script to make it easy on ourselves. In src/main.js, let's import Electron. This time we want to try out the Tray module, so let's take that off of the Electron variable and instantiate a new tray. When we try to run our app, we get an error saying we have an insufficient number of arguments. These arguments are meant to go into the Tray constructor. Looking at the Electron documentation, we see that we need an icon. I happen to have one here on my desktop, so I'm going to copy it into our source tree. It's called trayicon.png, and it looks like this. So let's capture that icon's path and put it into the Tray. We use path.join instead of slashes so that it will work on any system that we run this code in. Starting the code up again, we have a different problem, Cannot create Tray before the app is ready. Ah, we need our old friend appOnReady. So let's de-structure these submodules out of Electron, and when the app is ready, we'll move the Tray invocation inside of the callback. Before our app starts, there's no app icon in the system tray for our app. It runs, no errors. We see our new icon in the system tray. It's working. Now let's go back in and make it even better. Let's use our old friend, Menu, and put a context menu onto this trayicon. The template that we're going to build uses the exact same data structure as the previous section. The Label is what we see, the click handler is what happens when we click it with our mouse. So let's put in a few items, and see if they appear. The only thing that should happen when we click them are logs to the console. Let's introduce a couple of variables so we can connect these things. Starting the app again, we see our icon, and now, oh, we have a problem. Menu is not defined. I used it, but I hadn't de-structured it out of the Electron module as I had the other submodules. Fixing that, start it up again, and it works as expected. We see the console.logs and everything. Let's look at one more simple API, and that's a tooltip. Here we can add helpful messages for those that might not know what our icon is or what's going to happen when they click it. This one is obviously super helpful, isn't it? And there it is, that was easy, right? So if you are looking for a place to access your app where you need instant access, it's always visible, and it can always be on, look no further, and use the Tray module.
Clipboard Buffer
The clipboard is an awesome API that we're going to try out. We're going to throw a couple other APIs in the mix here as well, and make a slightly more interesting project. We'll create a clipboard buffer that can remember multiple copies and allow you to paste them in any order. We'll primarily focus on the clipboard and globalShortcut modules of Electron. We'll also use the Tray and Menu modules, as before. For this project, as before, let's create a project directory. Let's do npm init so that we can get a package.json. Next let's install electron-prebuilt, so we have the Electron binaries to run our app with. Then open up your favorite text editor, and let's have a look at package.json. As before, we'll tidy up to make it a little more convenient for us. Then let's start by editing the main.js file. Import Electron, and we'll be ready to go. We'll create a new app from the app submodule, and when app is ready we can do our work. Just like in the previous section, we'll create a trayicon for our application. The Tray constructor takes an icon path. We'll use a path to trayicon.png, which we'll put into _____ src tree in a moment. Let's go copy the trayicon.png off the desktop so that it's available to use here. Now for this tray we'll use another familiar API, that is the Menu. We'll create a contextMenu. Remember, here we're building a buffer of things that we've copied onto the clipboard, meaning we want to store multiple things from the clipboard onto this stack that we store in our app. But initially the stack will be empty, so let's create a single menu item that says as much. Enabled: false simply means that the user can't interact with this item. When we run our application, we see that our trayicon is there and it has the single empty menu item. Onto the next phase, we'll actually start looking at the clipboard. Now the clipboard, it has a simple API, but in this case we wish it would do more for us. There is, for instance, no event that is emitted from the clipboard that says when things come into it. So we're going to actually have to poll it to say, has it changed since the last time I've checked it. This polling will happen on some interval, and within the interval we're going to read out the latest text from the clipboard. If the latest text is different than our cache, then we know we need to refresh our cache. And let's start our cache as just whatever is in the clipboard at the time. And we'll set our interval for 1 second. This could be shorter or longer depending on your desire. To make this function work, we're going to need the clipboard, and we're going to need a callback here called onChange, where we can inform those who care that there are new things that have come into the clipboard. Now in that callback, we're going to start managing our stack. This text that comes back, we're going to add it to the stack. For the addToStack implementation, we'll take the item that we're adding, and the stack itself. We'll create a new array, putting the new item on the front, and if the stack size is previously to the max sizxe that we want to keep within this buffer, we're actually going to slice off the oldest part of the stack. Otherwise, we'll take all of the stack that's there. And for now, let's just say that our max size for the stack is 5 items. Let's assign stack to be the new stack, and let's console.log that to just see if it's working. If we start up the app, and we start copying a few things and seeing if they show up in the console.log, it seems like they do. And once we get to our max size, the oldest item in the stack, here, the string called ‘clipboard', actually falls off the end of the stack, so this is great. Now that we have a stack that's updated correctly, instead of just console logging, we want to change that Tray ContextMenu to be some form of the stack. So we'll build a new template from some new format that we're going to decide here. So let's pass in the stack and see what we can come up with. Mapping each item will create a new object that is the menu item format that we've seen previously. The Label will have the words copy, and then the actual item from our Copy buffer. Some things in the buffer could be a little unwieldy when shown in the UI, so we're going to format those to give it a max length. If the length of the text is longer than some max length, let's say 20, we're going to substring it and add an ellipsis. Now let's run our app again and try it. Copying our first item, hmm, it looks like we have a problem here, Cannot read property ‘position' of undefined. Somewhere in the template building we have a problem. Ah, here, where we are returning new menu items, I'm actually not returning anything, yet. Let's try it again, copy some text, hey, this is logging a lot better. Hey, there we go, that looks much better. Now what we're going to do is add a click handler so that when I go to the Tray and I click on the item in the clipboard buffer that we have created, it will actually write it back to the clipboard. It'll put it in that place where now you can paste it freely. When we run our app again, we save something to the clipboard, and then we try to paste from the clipboard. The pasting works because it actually is still in the clipboard, we haven't actually used the buffer in this case, and if we look, our app has actually blown up when we try to paste out of the buffer. Hmm, writeitem is not a function. Well that's because I used the wrong API. Here we're using text, so it's readText and then writeText. And that should do it. With that fixed, I'll start the app again and copy a few things to the clipboard. Now if we get an empty file here, pull something off our buffer, put it into that file, it looks like everything is pulling out just great. You can also see that when we click on one of these to pull it out of the clipboard, it's actually, again, added to the top of that buffer. This is as expected, since we're monitoring every second what's in the clipboard and adding it to the top of our buffer if it's changed compared to the latest. Now we can do one more thing that can make this even nicer, and that is to add some globalShortcuts. So on Electron, globalShortcut is the submodule on Electron that allows this. So we're going to register some new shortcuts. We're going to need the clipboard and the stack here, and we're going to set that up in our registerShortcuts function. For every possible element in this clipboard buffer, we're going to register a new shortcut, so that when the user clicks Cmd, Alt, and then some number, whether this is 1 through 5, we're going to take what's in our stack variable at that index, and put it into their clipboard so that they can paste it wherever they want. (clicking) Copying as normal works, and now let's add a few more pieces of text to this buffer. We can find a few in our source code. (clicking) And yep, it looks like it grabbed the right one. To make it more obvious in the UI what shortcuts are available, we're going to add the accelerator here, and put the exact same text as we registered in the globalShortcut, here. Now the globalShortcut can be used anywhere, whether you're in Electron or in another app on your operating system. Accelerators can only be used while you're in the app itself, but the presence of the accelerator is what controls the display of the keyboard shortcut in the menuitem in the UI. And sure enough, there they are. When the app is about to close and receives the will-quit event, it's best practice to clean up all these globalShortcuts. We pieced it all together. Hey, that was pretty fun, kind of a cool project. The clipboard API is pretty fun, and in concert with other APIs we can quickly start to make more powerful features. There was no flash dependency required for this clipboard feature, that's a relief, and we were able to programmatically control reading and writing from the clipboard. But there are a couple of limitations that we ran into that didn't quite allow us to make the kind of clipboard buffer app that we wanted. First, there are no clipboard events that are published, Thus we have to poll the clipboard to discover changes. Neither does Electron provide us access to the cursor position for text selection outside of our Electron app itself. We bypass this issue entirely in our app when we use the clipboard. We copied desk from the desktop to the clipboard, and then from the clipboard into memory. Wouldn't it be cool if we could know what's actually selected under the text cursor and copy that straight into our in-memory buffer, but that's just not possible today.
Screenshot
Now let's make a toy project that allows us to take screenshots of our computer desktop. We'll capture those screenshots to the file system for later. Here we'll focus on the use of the desktopCapture and screen module. We'll use the globalShortcut module again, as well. As always, let's start with a fresh directory for our demo and do an npm init so that we have a package.json. After that, let's install electron-prebuilt. Then open up the project in your favorite editor. Let's edit package.json so that we have a new entry point at src/main, and have a start script that will start Electron for us. Editing src/main, we'll do the usual and start with importing Electron, and starting up app, and receiving the ready event. To use the desktopCapture module, we're going to need to use a renderer process in this application, so let's create a BrowserWindow. For our app, we really don't need the window to be visible however, so let's create one that is essentially invisible, height and width 0, resizeable: no, and a frame: no. Let's point that window at a new HTML file called ‘capture.html', and let's take care of cleaning up the window when it's closed. Let's create the new html file and create something very simple. Again, there won't be UIs, so we want to get to JavaScript as quickly as possible, so let's create a capture.js file. Again, we'll need Electron in this module. In this module, we're going to pull out the ipcRenderer submodule from Electron, and we'll start listening for an event called capture. Now this is to set up for something that we haven't done in the main process yet, and that is send a capture event when the user presses a globalShortcut. So back in the main process, let's import globalShortcut and register a key combo for that. We'll console.log, and start up the app to see if our shortcut is correctly registered. Immediately it looks like something is wrong. And I've got a typo. Second try, matching the appropriate keys, it looks like it works. So now removing the console.log, let's send the capture event to the mainWindow's event emitter. Now this time we should see the onCapture console.log, but we don't. There's nothing appearing in our console. Why? Well, let's open the dev tools on the mainWindow. This is going to take a little space, so we're going to have to get rid of our 0 height, 0 width. Starting the app again, and pressing the keyboard combo, now we know why we didn't see the console.log in the terminal app. It's actually appearing in the renderer process, which renders into Chromium. Now we should be ready to start looking at what's actually on our desktop, what appears on the screen, so we'll pull in the desktop capturer module. We're going to look for what we're going to call the MainSource. There are many sources of information that can be captured from the desktop. These could be screens, this could be audio, this could be image, video, that kind of thing. There could be multiple monitors as well. Here we want only screens. And we're going to set the capture size as the original screen size. We're just going to assume that the PrimaryDisplay has the size that we desire. When we try to get these sources, we could have an error. In this case we'll do something very simple and unhelpful, and console.log out that we cannot capture the screen. When we do find sources we could find multiple, so we want to be able to filter to that source that is the single PrimaryDisplay that we're looking for. We're going to use the name of the source, either ‘Entire screen' or ‘Screen 1', to make that determination. With this filter function in place, we can filter through those sources and grab the first that matches the description, passing that to the callback, done. Now in our callback we have the source, which is the monitor, the PrimaryDisplay. We're going to pull the thumbnail, which we had previously sized, change it to a png, and store it in the png variable. Now we need a place to save that, so let's create a file path. We'll put everything in a targetDir, and just name it the date, the current date, .png. We're going to go back to the main process and send the targetDir from that process, because here we have access to app, which gives us access to places like the ‘pictures' folder. Once we have the png and the path, we have enough information to actually write the screenshot to the file system, so we'll use node's fs module, .writeFile. Now let's start up the app and try it out. Take a screenshot of the "Wow", and targetDir is not defined. Let's fix that, start the app again, take a screenshot. We do this using our globalShortcut Ctrl, Alt, Cmd D. And it appears in the Pictures directory, "Wow!". Let's finally go back and get rid of our UI that we didn't want or need. This time no BrowserWindow appears. We take the screenshot, and look, our terminal window in sharp black and white. You did it. We demonstrated that we're able to capture the full visuals of the desktop. That means we're clearly not constrained by a browser sandbox. Similar APIs are also available for capturing audio and video, we just didn't use them here.
Git Status
Alright, this demo project will not explore Electron APIs, per se, rather it will explore that Electron has access to the machine that it's installed on. It can run binaries installed on that machine, as long as the OS user that you run Electron as has permissions to do so. We'll create a little GUI for testing the git status of directories. To accomplish this, we'll use Node's child_proces and os modules. As always, let's make a fresh directory for our demo, run npm init so we get a package.json, and install Electron. Open up the project in your favorite editor, and let's edit package.json to change the main entry point and add a "start" script. In main.js, import Electron, and make sure the app is ready before proceeding. Since we're making a GUI, we'll need the BrowserWindow, and let's point it at some html. Let's call it status.html. And let's make sure we're good stewards and clean up after ourselves. Next let's create the html file. For now let's just put something really simple in. Hmm, even in the simple case we have a problem, BrowserWindow not defined. Let's make sure we import that, much better. Now for our real GUI we want an input field where we can enter the path of a directory. Then we'll have a status indicator to show what the git status of that directory is. We'll need some JavaScript, so let's create status.js. We want to add a ‘keyup' listener so that as we type the directory we can capture that path. Starting up our app, it appears that it is captured. We can type pretty fast, and we don't want to be running the process to check for the git status for every letter that we type necessarily, especially if we're typing fast, so we're going to debounce that a little bit. We're going to say, you must wait half a second and kind of have a stutter or stop in your typing before we're actually going to do this git status process. This kind of debouncing can be important if whatever process you are running is more expensive to run. Running the app again, now with it debounced, we're still capturing, but you can see there is a delay, great. Now instead of console logging, let's take care of our actual logic. If what we have typed is a directory, we want to check the git status, so let's capture the directory name from the event target.value, which should be the input elements value, passing that in to our two functions. Now let's create the first, isDir. Here we'll use Node's fs module again, and we'll look for the stats on that particular path. If that path is determined to be a directory, then we return true. If for some reason this synchronous call to lstatSync blows up, then it's obviously not a directory. The path probably just doesn't exist. Now let's implement the next function, checkGitStatus. Here we're going to call exec. Exec comes from the Node child_process module. This is a way of breaking out of this Node process and running or executing a different command in the host operating system. The command that we want to run is ‘git status', and importantly, we're going to run that in the directory that we've typed in. CWD means current working directory. This is where this command will be run. What comes back from this is any error from Node, the standard out from running this command, and the standard error from running this command. For now, let's just console.log all three. We also want to provide an extra bit of functionality, and that is to handle the home directory, especially in Unix-style systems. Anything that starts with a tilde is kind of a shortcut for home, and I like to write this because it's shorthand instead of having to write my user directory every time, so we're going to handle that here in formatDir. We're going to use Node's os module to know where the home directory is, and then we'll take everything after the tilde and slap it on the end. Starting the app again, we type in a directory, and we immediately get a lot of feedback from these three console.logs. Importantly, we've run git status where it's not possible to have a git status in a non-Git repository. I happen to have a Git repository on this machine, and navigating to it and running git status we have a different result. It looks like that Git repository is actually dirty in the local directory. Now instead of console logging, we want to set up for actually displaying an indicator of the status to the screen. We'll do that in the setStatus function. When setting a particular status, first we want to remove all previous statuses, so we'll get the status indicator element and remove our enumeration of these classes that are going to show a different kind of status indicator. We'll have three, one for unknown, then clean, then ‘dirty'. After we remove all of these statuses, we'll add in the actual status that we've discovered from running get status. If there's an error, we'll say, well, we don't know what the status is. If there is a nothing to commit message, then we'll know that git status found a clean Git repository. Finally, all of their directories must be dirty. Let's make this a little bit more fun to look at with a bit of styling. So let's create a new status.css file. We'll push out a bunch of styles, making sure to style all the main elements on the screen, and most importantly, we'll have three main colors denoting what is unknown as a git status, what is dirty, and what is clean, unknown being gray, green being clean, and red being dirty. Let's start the app up again, and hey, it looks a little different. Let's try out our first directory, dev. It's not a Git repository, so it shows unknown, gray. If we go to react-drift, as before, now it's red. We knew it was dirty from the console logs. Let's go to a different directory, react-styleable. This one is green, which means it's clean. Awesome, we're seeing all of the statuses come through. Finally, I want to tweak this so that as I type I get rid of all the status indicators, they don't linger around. So anything that was dirty, or was clean, or was unknown, that might not be true as I start typing. Trying it out one more time, that looks about right. Now I think this demo was pretty awesome. Obviously this opens up many, many options for what an Electron app could do. It can interact with the system that it's installed on. This ability to fork and run other processes also seems to require an extra measure of developer mindfulness and responsibility. Implications around security, compatibility, and performance, should probably be considered when making apps that run other binaries, but hey, have fun with it.
Make an Electron App
Introduction
At this point, we've had good exposure to a wide range of Electron APIs. They've all been fun, they've all been pretty good, and in turn we've seen the flavor of each, but now we're going to take them and combine them into something new and interesting that we might actually want to download and install this new project on our computer later. Let's try it out and see. Our project is a photo booth app. It will utilize the web cam on our computer to show us a live feed of the camera subject. We'll also be able to capture a snapshot and store them in the filesystem. We'll be able to add live filters to our image to show different image effects. There will be a nice countdown to help us prep our cheese. We'll be able to browse recent pics and jump to them on the filesystem. We should be able to use the strengths of the Electron platform and API to make a fun project to work through together.
Project Layout
We're going to start this application from scratch in a new directory called electron-photobombth, obviously the name a great contraction of photobomb and photobooth. So, hard to say, but should be fun to code together. Of course, as an Electron app we'll want to install electron-prebuilt. Once installed, let's open up this directory in our editor of choice. First we'll make a few changes to package.json. We'll change the description to be "Photobooth app", we'll change main to have the entry point of "src/main.js", and then let's add a script to start Electron using npm start. Let's create that new file, main.js, for our entry point. The first thing we want to do is import Electron. Then we'll deconstruct out the two Electron modules that we need in this section, app and BrowserWindow. Then we'll test that the app is ready to respond to us. In the callback, we'll instantiate a new BrowserWindow and assign it to the mainWindow variable. We're going to give this BrowserWindow some specific dimensions, width of 1200 pixels, height of 725 pixels, and then we're going to set resizable: false. These UI dimensions are preselected to support the style of layout that we desire. Resizable: false will help keep everything in place, even on this small computer screen size. This BrowserWindow will have a UI loaded that is seen in the capture.html file. Here, we'll programmatically open the Chrome DevTools for easier debugging. Finally, when mainWindow is closed, meaning the BrowserWindow has closed, we'll null out the reference to mainWindow so that it can be garbage collected. In the course materials, I've included three things that will help jumpstart us in the project. The interesting parts of this project are the Electron parts, but there is some supporting HTML and CSS that must be written to complete the project, so those files have been included. Also included is a vendor directory, which includes a third-party library called seriously.js. We'll use this later. For now, I'm going to copy all three of these things from my desktop into the source tree. Let's have a look at some of this premade code. Here is capture.html. It includes a link to capture.css, has a container div where there is a videoContainer, a recordContainer, and the photosContainer. The videoContainer will contain the live preview, or the video element, where we can see what the web cam is capturing. The recordContainer will have the record button. The photosContainer will include all of the recently taken photos in the application. Lower, we can see the counterContainer, which will contain a 3, 2, 1 countdown for when we take a picture. And then we have a flash, which will be the flash effect for when we press record. Finally, you can see the script includes for all the third-party seriously.js files, and then a capture.js require for what we will write, so let's create that file now. Before we go on, let's also take a quick look at capture.css. All the styles are prebuilt for the entire project. You won't have to look in here, just know that there is quite a bit of styles that are backing this.
Capture the Video Stream
So here we are in capture.js. We're in a renderer process. So here we're going to be dealing with the browser code. We'll start with an onReady event, ‘DOMContentLoaded'. Once the COM is ready, we'll interrogate it to find all the elements that we'll be interested in for this project. And next we're going to jump straight in to capturing some of this video content that we need for our photobooth app. We'll use navigator.getUserMedia to access the web cam video. This being a newer API, the browser still used vendor prefixes in their specific implementations, so we'll reassign navigator.getUserMedia to navigator.webkitGetUserMedia. GetUserMedia takes three parameters, constraints, a Success callback, and an Error callback. These constraints deal with what media devices we're trying to capture. We don't want audio, but we do want video, and we're going to set some specific parameters for that. Min and mx will be the same, because we want one specific size. Now let's implement the Success and Error callbacks. For Success, we're going to pass in a video element and a stream. The stream is what comes back from getUserMedia. The video element is what we've captured below in the DOM. We want to set the video element's source to the stream from the web cam. In the Error callback, we'll do something very unuseful and just console.log. Now let's see if it works. Let's start up the app using npm start. Hey, look, it works, well, kind of. We see a frame of the video, but it's actually not moving. It seems to be stuck for some reason, and if we go to capture.html we see why that is. We never actually called videoplay programmatically, so let's add an autoplay attribute here. Let's restart the app and see if it's better. Oh yeah. This app is going to get a little bit bigger, so we can already tell that we're going to be doing quite a bit in this capture.js file, so let's extract a video.js. In video.js, we'll do the initial setup in an init function. Init will take the navigator and video element parameters, and set getUserMediaType to be what we desire. We'll also move all those media constraints in here as well. Next we'll move in the callbacks for handleSuccess and Error. Let's start up the app and make sure we didn't break anything with our extraction. Oh yeah, still going.
Capture Bytes to Recent Photos
Up until now we have seen the image of the video on the screen, but we haven't actually captured that image. Let's do that now. We're going to capture the video to a canvas element. We'll create a new method on video.js called captureBytes that takes the video element, a canvas context, and the canvas element itself. Let's make sure we initialize that 2D context. In CaptureBytes, we'll take the data found in the video element, and draw the current frame onto the canvas context. Then from the canvas we'll export a png. We're going to take these bytes and create a recently-taken photo in the UI. So we're going to create an image tag for those bytes. In the formatImageTag function, we're going to take the document and the bytes. Using document, we'll create new elements. An image will consist of an outer div with a class of photo that has two things inside, another div for a close button, and an image tag with the actual bytes of our image as the source. Let's start up the app, take a few photos, and sure enough, they appear on the bottom.
Countdown
To make this photobooth app a bit more fun, we're going to add a countdown, so that when we click record it will wait 3 seconds, showing us the countdown before it actually snaps the photo. We'll implement a new countdown module with a start method. It will take the counter element, the number that we're counting down from, and a callback for when we're done counting down. Let's move the actual capturing of the bytes into this callback. Let's import the countdown file and create the file itself. Inside countdown, we'll create the method as described before. We'll loop from 0 to the number we're counting down from, and within this loop we'll set up the functions to be called at second intervals, to actually set the current count. To set the count, we'll do nothing more than show that count in the UI. We'll start up the app, we'll press record, and we see the numbers start to countdown from 3, 2, 1, yay. We have the countdown now, and we can press record, but our images actually aren't being saved anywhere, so let's go make that happen now.
Save to Filesystem
We're going to send an image-captured event back to the main process. To do this, we're going to need Electron, and pull out the IPC renderer module. In main.js, we're going to import ipcMain, and then we're going to listen to our newly-minted image-captured event. When we called this event, we sent the contents, or the bytes, of the image. We're going to create a new module now instead of cluttering up the main.js file. It's called images, and it's going to have a save method. We're going to save to a directory, the directory which images also knows about that we'll pass in here, and we'll pass in the bytes along with it. Let's create image.js. We'll set up the shells of these functions. The getPicturesDir function needs the app from Electron so that it can get access to directories on the OS, such as the pictures directory. We're going to get the subdirectory inside of the pictures directory called ‘photobombth', after our application. We'll need to use the path module from Node to join these two paths. Now inside save, the contents, or the bytes, that come out of the canvas.png, is a string of Base64 encoded image data. When we save these bytes to the operating system, we don't need the header in front of that string, so we'll strip it right out. We'll use Node's fs module to write the file. We'll write it to the picturesPath, and we'll name it just the new Date .png. We'll pass it the data, make sure that fs.writeFile knows this Base64 encoded, and finally give it a callback for just logging errors, for now. The log error function will be very simple. Now back into main for a moment, we're saving files to a subdirectory, photobombth, inside the Pictures directory, but we don't have that directory created yet. So in our on ‘ready' callback we're going to call a new function on the images module, mkdir, and this is going to take the path that we're trying to make a directory in, which will be the Pictures directory. Now to implement mkdir. It will take the picturesPath, and it will call Node's fs.stat function. Stat will return an error code of ENOENT if the path in question does not exist. If it's not, and no such file or directory error, then we'll log the error. Otherwise, if the error still exists and it's not a directory yet, then we'll call fs.mkdir on that path. It's time to test the app again. We'll start up the application with npm start, we'll snap a photo. Switching over to Finder, we'll open up our Pictures directory where there is a photobombth subdirectory, and inside that we see our new file, awesome.
Browse and Remove Photos
Now that we're saving the file and it's showing up in our recent photos UI, we want to be able to click on that UI and jump to the file inside the OS Explorer or Finder. So we'll go back to capture.js and create a click listener on the photos element, which is that bottom recent photosContainer. We'll query for all of the images inside of this container, and then we'll find the index of the one that matches the event target. From there we're going to implement a new method on our images module, getfromCache, which takes the index found. Before we leave this file, we'll prep for the later step to come, and call shell.showItemInFolder, which is an Electron built-in API on the shell submodule allowing the OS to jump to the folder represented in the path. To use showItemInFolder, we'll need to deconstruct the shell submodule, and then here for our images import we will have a slightly different syntax. In capture.js, we're currently in a renderer process, but we want to talk to images in the cache, which was originally set up in the main process. To speak between renderer and main processes, we must use ipc. But this doesn't look like ipc as we've used it in the past. Remote is a simple way to do ipc between renderer and main processes, so internally Electron is handling the ipc for us. When we call remote.require, Electron is returning the module to us as if we had called require within the main process. Thus, when we call images.getFromCache, we'll actually be able to access the cache that lives within that module. If we use just a vanilla require in this instance, we would never have access to the internal images cache, because it was originally set up in the main process. If we wanted to set up our own synchronous ipc call, we could do that as well. This is just very simple and convenient. Now onto the images module. As the name suggests, we're now going to implement a cache called images. The cache will simply be an array of image paths. Now if we're going to pull from the cache, we need to have something in the cache to start with, so let's implement our cache function, which will do nothing but concat on the end of the images.cache, new imgPaths. Back to main.js, we're going to change images.save to take a callback so that we can do caching after we know we're successfully saved. We'll change the save internals to take a callback in the fs.writeFile that's a little more smart. If it has an error it'll log it, otherwise we'll return the imgPath. Now to test this feature let's start up the app again. I'll snap a photo of myself. It appears in the recent photos, and when I click it, voila, Finder is up, and my file is selected. Now beyond just seeing the recent photos, let's say I want to remove something. Well let's go back to capture.js in our photosContainer on click Listener. We're going to test the target of the event that bubbles to this listener, to see if it contains the class of ‘photoClose', which is the class of the remove button. If it does, we know this is a remove operation, and we'll change the selector as well. We'll be a little safer here and test for a result, and now we have two potential actions that a user could intend. If it is a remove, we'll send a new imageRemove event back to the main process. Otherwise, we'll call what we had just seen in shell.showItemInFolder. Back in main.js let's listen for image-remove. The callback receives evt and index, and we'll pass the index onto images.rm, or remove, which is a new method. Inside the images module, we'll implement rm. It takes index and a callback for done. We'll use Node's fs.unlink, pass the cache at that index, and then check for errors. If there are no errors, we will splice out the image from the cache and say that we're done. Back in main.js, once we are done we send an event back to the sender of the original event, image-removed. Now to complete the circle, back in capture.js, we receive image-removed, and we're going to get the photos removing the child photo that matches the index that we just received back, the one that we have successfully removed. This will remove that photo from the UI. To make sure we know that this is working, I'm going to go to the photobombth picture directory and remove all previous photos. We'll start up the app again, snap a photo, we'll click on it so the finder is opened, and then we'll click remove. To check to make sure that the remove worked, we'll switch back over to Finder, and sure enough, the file is gone, awesome.
Flash Photography
Now for a bit more fun, let's add a flash for when we record the image. We'll save a reference to the DOM node, and then as soon as our countdown is over we'll call flash. We'll create a new flash module. In that module we'll export a single function that takes an element. To that element, we will add a class of is-flashing. This class is already defined in our style sheet. At some point, we want to stop flashing and remove it. The CSS animation in the style sheet takes 2 seconds, so we'll wait until that 2 seconds is over. We'll also make sure to clear that timer whenever we start a flash. And for safety's sake, if for some weird reason there is an is-flashing on this classList already, we'll remove it right away. Now let's go and test this out. Who doesn't like a good fireworks display? 3, 2, 1, boom. It works.
First Filter
Well, now for more fun, this time maybe even useful. Really, image filters are probably the main reason I would ever go to a photobooth app, it's fun to play around with. We're back in capture.js, our renderer module, and we're going to need three new variables to assist us, canvasTarget, seriously, and videoSrc. After the DOM is loaded, we can instantiate seriously.js. Seriously.js is a third-party library that allows video effects to be applied to live video. It accomplishes this via WebGL shaders. Luckily, seriously has created these shaders for us. Otherwise, we'd have another course prerequisite. It has a number of shaders for different effects. We'll show just a few. If you're interested in more, see the included vendor code or the GitHub for the seriously.js project. This is another example of where Electron really shines. We're using Chrome only, and Chrome supports WebGL, so we're free to use this technology without wondering if someone is going to come to this application and not be able to see our content. In the next steps, we're going to connect seriously to the source and target. The source is the input source where the video information comes from, which is our videoEl, id'd by video, so we'll use ‘#video' here. The output is still the canvas, so we'll target ‘#canvas'. Beyond that, we'll move into a new module that we're going to make called effects. The first method will be choose, and we're going to pass seriously, the Source, the Target, and the name of the effect. The first that we're going to pass is called ‘ascii', which is one of the included effects. Let's import effects, and create the new file. For the choose function we'll implement the entire parameter list, and then if there is no effect given we'll have a default called ‘vanilla'. Next let's create our dictionary of effects. We'll have one, vanilla, which is a function which takes the seriously instance, the source, and target. So we'll connect the target.source to the source given, until seriously we're ready. This essentially bypasses any effects. For ascii the function signature looks the same, but in this case we're going to pull out an effect, ‘ascii', from seriously's library. Then we'll let our source pass through the effect to the target, and then tell seriously we're ready. Now on app startup we should have selected the ‘ascii' effect, so let's start up the app and see if it worked. Uh-oh, syntax error. Fix that, we'll start it up again, blast, we have a new error, unable to create WebGL context. This is coming from seriously. Hmm, we're already initializing that context. Let's check our capture.js. Ah, here, from before we are getting the 2D context from the canvas element. Now we're going to have to let seriously do that by itself. This main context is no longer available, so we're going to have to change our captureBytes method as well. Since seriously is already connecting the context, we're going to now capture bytes from the live canvas, so all we'll need here is the canvas element. Let's implement this new method on our video module, start up the app again to test, and blast, it still looks vanilla as ever. Let's look at capture.html. Hmm, both the canvas and the video are position absolute, so the last one, currently video, will appear on top, which makes sense, because as we move we're capturing to the canvas, but we can see ourselves moving after we take the photo. So now seriously is compositing the video, and it's updating the canvas on the fly, always, so that it looks like a video stream. This is where the output of the effects should be shown, so let's switch the order of these so that canvas appears on top in the UI. Whoa, gnarly. Hey, now that's what we come to photo booths for.
Application Menu
It's fun to have these effects in code, but now we're going to make them available to the user so that the user can decide which effect he'd like to see. Going back to main.js, we're going to create a menu built from a new template that we'll design here. We'll put the menu in menu.js. The first menu item will have a label of the app name, which needs to be set as productName in package.json, so we'll take care of that now. We'll make the rest of this first menu, submenu, look about normal as it would exist in Mac OS. (typing) Most importantly, for our purposes here, we'll keep quit and its accelerator so we can easily exit. We'll start at the app, check it out, and it looks about like we'd expect. For the next, more interesting menu, we'll have the effects menu, which will include our two effects. When you click one of the effects, the main process will, via RPC, send an ‘effect-choose' event to the renderer processes. Based on the effect chosen, a different effect name will be sent. So now we need to go to capture.js and receive the ‘effect-choose' event, passing that on to our effects module choose method. Let's start up the app to test, go to the Effects menu, select one, select the other, hey, it looks like they're switching. Let's add another fun bit, an effect ‘Cycle' option. So this will cycle through the events one by one. You can shortcut to this option by pressing Shift Cmd E, and when we click we're going to send ‘effect-cycle', a new event to our renderer processes. We're also going to implement this function, enabledCycleEffect. It will help set a checkmark on the currently active effect in the menu. First, we know that there are two menu items above the effects that are not effects. Then for cycling we find the currently checked effect, then we get the next one. If we have reached the end of the list, we'll circle back around. Then we set the checked attribute on that item. This will work once we change each of those menu items to be of type ‘radio'. Back in capture.js, we'll receive the event cycleEvent, passing it on to the effects module, and implement a new cycle method there. Here we're going to have to start keeping track of the currently active effect as well, and we'll keep track of that with an index. So in cycle we'll set the NextIndex. We'll usually increment the index by 1, as long as we have not run off the end of the list of effects. To have that list, we'll capture all the keys from our effects dictionary into effectNames. We'll initialize the currentIndex to 0, and then in setNextIndex we'll reassign currentIndex to the nextIndex. Finally, in the cycle method we can find the current effect and call that function, passing seriously, src, and target. Now that we have indexes, choose needs to keep them up to date as well. With choose, we can jump to a specific index, so we need to be able to find the index based on an effect name. We'll start this up to test cycle, and oops, got a syntax problem, add that comma in the menu, the app runs, I select Cycle, and hey, it changes. Also, pressing the keyboard shortcut, Cmd Shift E, that also works. We're golden.
Final Touches
Of course, two effects are not nearly enough, one even being vanilla, so we'll add a few others. Next is ‘daltonize'. We'll pass it a few parameters to spice things up, and now we want to do the same things that we did in the end of the ‘ascii' function, so we're going to create a new connectEffect helper, which will wrap the effect between the src and the target. Next is the hex effect, kind of like pixelate. We'll give it a size, and then connect. Back to menu, we'll keep that updated as well, copy and adjust these, and now the most fun part, we'll try it out. Let's close DevTools to get a better picture here, obviously better. Hey, these are pretty trippy. Now let's add another feature that will be convenient and help us fill out our menu, and that's an open Pictures directory feature. When I click this, I want it to open the directory for the photobombth pictures. In openDir, we'll also highlight that depending on the platform you're on, you might have to do some leg work to accommodate the commands available. For instance, on darwin, or the Mac OS platform, open is a command available to open a directory. On Win32 or Windows, explorer is the command. On Linux, nautilus is often available, although this is probably naïve. Process.platform will be the way that we determine what platform we were on. If we find a command, then we'll spawn it. We'll run this using the child process spawn from Node. If this fails, we'll fall back to showing the item in the folder. In my opinion, showItemInfolder, however, is inferior because it will show the parent directory, instead of opening the directory that we're trying to get to. So let's open it up to test, select the Photos Directory menu option, and hey, the open command worked well, we're in the photobombth picture directory. I'm also just realizing that we haven't actually saved one of these video-composited images, so let's try it now. Flash bang, and yeah, it looks good. Okay, let's do some finishing touches and we're set. As long as we're checking the process platform, let's check to see if we want to include that first very Mac OS menu. We'll pull it out of the template variable, and then if the process platform is darwin, we'll unshift, or prepend it to the font of the template. We'll start the app for a sanity check, it still works. Finally, let's tighten things up visually. Let's go back into main.js, change the width so that we don't have to accommodate the DevTools, and don't open the DevTools. Then we'll go to capture.js, and remove that ‘ascii' default, and just make vanilla the default. Starting up the app, ready to have another good time in the photobooth, or photobombth. Yay! We did it. Well that was an extensive project. We used many of our learned Electron skills in the making of a non-trivial application, and yet it didn't seem too bad, right? We were able to capture the webcam without permissions, we were able to write to the filesystem without permissions, we could navigate via native OS finder applications to directories and photos that we wrote to. We used WebGL for adding image filters, without regard to old, decrepit browsers. And it was all inside a very native-feeling experience. Next up, we'll put a bow on it all, and package up our app so it really feels ready to share.
Package Native Apps
App Icon
Now we've done the work to build out our entire photobombth application. Now we want to share it. We want to package it up and send it off for mom to use at home. We want to go through those last few steps that will get it ready for others to use. We want to share it, so let's do it. Let's create an icon for our application. I'm a succor for logos, so this part is quite fun for me. You're free to produce your own logo here. I have one produced already that's in the course material. If you make your own, make sure that it's at least 512 x 512 pixels square. This is the largest size that will be eventually exported. I started with a 1200-pixel square composition in Affinity Designer, which is similar to Adobe Illustrator. I'd recommend you use your own favorite vector art program, if you're making your own logo. When you're happy with your creation, we need to export a png file. We're going to upload that png file to a site called iconverticons.com/online. This tool will help us easily convert this png to a couple of other formats that different platforms expect for their icons. As long as we use the online tool, this is free. You can also export your own if you have the means, this is just easier. Mac uses the .icns file, Windows uses the .ico file, and Linux will use the .png. We'll export each one of these we need, and place each file in the root of our project.
Build The App
Next we're going to build our application. We've been running the app in dev mode up until now. Now we want to get to where we have an application that has an executable that feels just like another application on our machine. On Mac we want an .app, on Windows an .exe, and on Linux a binary that won't have an extension. We can go through the building steps manually by ourselves, but there are some community projects that can help make this more straightforward. One such project is electron-packager. We're going to npm install it, and another library called rimraf that will accomplish cross-environment recursive directory removals. In other words, it will help us clean up the directories we'll create when building multiple times. Now that we have these tools, we'll create a convenient npm script. Here we'll call electron-packager and pass a few important flags. We'll choose a certain combination today. Just know that there are other options that you may want to explore in the electron-packager docs. Today we'll create a build for each of the Mac, here called darwin; Windows, win32; and Linux platforms. We'll only build for a 64-bit architecture, and we'll also point to the name of the icons that we just produced. All we have to do now is run npm run build in our terminal. The first time you run this, it may take quite a while, as Electron files per each platform and architecture combination, are going to be downloaded. When execution is completed, we'll list the files in our project directory and see three new directories, where each of the separate app builds have been placed. Awesome, let's look inside one or two. Here we see the Mac .app file, and here in the Windows directory we see the .exe file, pretty cool, huh?
Setup Windows Virtual Machine
Now I've been developing on a Mac. You may be on a Mac, Windows, or Linux development box, but now we want to take what we've built and try it out on other operating systems. To do that, and not need three separate boxes, we're going to install some virtual machines on our development box. These will be separate OS's on simulated hardware, all hosted through virtual machine software. There are a couple of VM vendors that create a good virtual machine application. We're going to choose VirtualBox in this instance. It's free and generally works as well as anything. VMware also makes a good product. I'll go to the virtualBox website and download the VM app for my computer. Since I'm on a Mac as the host machine, I want to download the VM software that is made for a Mac. Having a VM will make things easier and cheaper than setting up three separate boxes. But as you might expect, setting up two extra operating systems is going to take a fair bit of work. I'll list out the highlight steps in the process here. First we'll set up a Windows VM. Microsoft has created a site for downloading VMs that are usually used for browser testing, but these VMs should work for our purposes as well. Once downloaded on Mac, you'll need to get yet another tool to open this archive format. Check out the archiver. To run a Windows VM on Mac, you'll also need Wine installed. Check out this guide as my favorite thorough install method. In short, use Homebrew. Now that Wine is installed, you can import your Windows VM into VirtualBox. Simply double-click the .ova file that you unarchived, take the defaults given by VirtualBox, and you should be fine. Now start it up for the first time, and soon you will see Windows running on your Mac. Pretty cool feat. Now the OS is set up, but there are a few levers that we need to pull to make the VM more hospitable to the app we're about to test out. First we want to be able to easily get our app into the VM to test, so we're going to set up file-sharing over the network. To do this, we need to do what VirtualBox calls installing guest editions. After you've started the VM, go to the Devices menu and insert Guest Editions CD Image option. This will initiate a Guest Editions install process. You only need to do this once for this VM. Now that the feature is available, we need to mount our local Dev project directory so it's available inside the VM. So go to the Devices menu, Shared Folders, and select Shared Folders Settings. Select the option to add a folder, browse to your project folder, select it, and mark this network share as Read-only, Auto-mount, and Make Permanent. Now if you go to the file Explorer, then Network area, you'll see a VBOXSVR computer. Click into that, and you should see your mounted project directory. Clicking into that, we can now click and drag to copy from the host machine into our VM OS. Copy in the whole Win32 built app directory. We have another step to accomplish because of the type of app we're testing. Our app needs to access the webcam. This is something else that we need to set up for the VM to share with our host machine. To do this, we'll need the VirtualBox Extension Pack, which is another download. Go to the VirtualBox website again, download and install that, and make sure you download the appropriate version matching your version of VirtualBox. Once installed, you should be able to enable the USB controller, which is what controls the webcam. So shut down the virtual machine, right-click on the virtual machine, click Settings, Ports, USB, and then Enable USB Controller. If it's not already checked, check it, and then click OK. If it's grayed out and still not checked, you may be forced to use the VBoxManage command lien utility to try and enable it. In my experience, this was an unfortunately finicky process. Now restart the virtual machine. Assuming you can get VirtualBox to obey you, now you're ready to share the webcam. Go to Devices, Webcams, and you should see your particular webcam listed. For me, I see the built-in FaceTime HD Camera. Make sure it's checked. Now we should finally be ready to see our app in action. Let's start it up. It looks great. Our video is being captured on the webcam. Let's press the record button, and we have a new picture. Click it to go see it, and nothing happens. Let's take a look in our Pictures directory. Hmm, it's empty. Something is the matter with the saving of the photo. I'm not sure what it is. The path is right. Maybe it's the filename itself. We are including weird spaces and non-alpha characters when we use the data as the filename, as we do. To make it more benign, let's change the data to be the time, which is just milliseconds. To try out the app in Windows again, we'll need to build it, then open our Windows VM, and copy it onto the desktop. Then open the exe again, snapping a picture this time, clicking on it, it works, it's there, awesome. Well, this is a small example of how even though Electron supports multiple platforms, when you interface with the host OS, such as in filenames, there is still a difference from OS to OS that you'll have to navigate. Luckily this was a small problem that we could fix easily. Let's go onto Linux and have a taste of that.
Setup Linux Virtual Machine
For our chosen Linux distribution, we'll use the latest Ubuntu. In slight variance from the Windows VM installation, in this case there is no premade VirtualBox VM for Ubuntu, so instead we're going to have to find an iso image of the OS available from the Ubuntu website. Once we download it, we're going to set up a new Linux VM using mostly just the defaults in the new VM dialog of VirtualBox. We'll point our VM to use that iso that we've just downloaded. If we boot to that iso, we'll be guided through the Ubuntu installation steps. Once the OS is installed, again, we must get it working well with the host operating system. As before, we'll install the VirtualBox Guest Editions on this OS. After you've started the VM, go to the Devices menu and insert Guest Editions Image option. The Guest Edition's install process should be complete in just a few moments. Then again, as before, we need to mount our local dev project directory so it's available inside the VM. So go to the Devices menu, Shared Folders, and select Shared Folders Settings. Select the option to add a folder, browse to your project folder, select it, and mark this network share as Read-only, Auto-mount, and Make Permanent. Now if you open the Files app, you should see your mounted project directory listed next to other shortcuts in the left-hand navigation. Clicking into that, we can now click and drag to copy from the host machine into our VM OS. Copy in the whole Linux-built app directory. The next step, as before, will be to give the VM OS access to our webcam. You've already installed the VirtualBox Extensions Pack, so you should be able to enable the USB Controller, which is what controls the webcam. Go to Machine, Settings, Ports, USB, Enable USB Controller. If it's not already checked, check it. Again, if the availability of this checkbox is finicky for you, do your best to troubleshoot it. Once USB devices are available on your VM, go to Devices, Webcams, and you should see your particular webcam listed. Make sure it's selected there. Now to try out our app in Linux. In the Linux project directory that you have copied to your VM, double-click the photobombth binary. This should start up the application. We see the video feed, that looks right. We go to cycle through the video effects, well that's not good, these didn't work. Let's try to capture a snapshot. (clicking) That doesn't even seem to show the preview image of what we thought we had captured. I'm led to think that WebGL is not working for us on this platform. A quick Google search for WebGL Linux Electron, shows that we're not the first with this problem. The highlight GitHub issue points to what is probably a video driver issue. The potential solutions, including replacing our video driver for our Ubuntu OS, are too involved, and potentially troublesome for us to tackle here. Again, we find that our cross-platform application still has to deal with the host OS in certain ways. Here, variety of hardware, specifically the video card and video drivers, are a giant pain. For now, for this demo, we are out of luck, and not really compatible with Linux. Were we building an application that we still wanted to deploy on Linux in the real-world, we'd be working to engineer a more general video capture solution in our app, or a graceful fallback for Linux, for instance, a solution that doesn't include a WebGL requirement. From this experience, we learn a couple of things. If you really care about a particular platform, test on it throughout development, perhaps even make it your development platform. The other is that since we're now deploying native applications, albeit ones that package their own runtimes, like Chromium and Node, we still have the potential to touch features, such as WebGL, that are influenced by native hardware and host operating systems.
Future Topics
As we've seen, Electron is home to many impressive and interesting features. We have tried some of the highlight ones. This course has been focused on fundamentals. There are still many other topics related to Electron that you might be interested in. The Electron Docs site is a great place to continue learning about the platform. You may be interested in the instructions that they offer on App Store Submission, auto updating, live debugging, online or offline detection, crash reporting, and other APIs. You may even want to jump in and learn to contribute new features to Electron itself.
Thank You
We've had a lot of fun together. We have learned how to take an Electron app from scratch to an OS-specific executable. We've done this by repeating the steps of getting the project off the ground, and we have shown how some of our favorite Electron APIs can be used to create some compelling features. Together we've created a countdown timer, testing inter-process communication. We've learned how to specify an application menu. We've learned how to keep an app running and available in the System menu. We've learned how to control the clipboard in an extended clipboard buffer app. We've built a screenshot capturer, capturing images from our desktop via globalShortcuts. We've built a Git status checker that utilizes other binaries found on the host machine. And finally, we've built a more sophisticated photo capture app that has brought many of these concepts and APIs together. Through it all, we've shown Electron's great utility and approachability. It's really fulfilled some good use cases for us. It turns out that it's a great option for developing native desktop applications, and it's made even better because we can use the Web Tech skills that we already have to get our ideas put into practice. Who knew building native desktop applications could be so much fun. So I'm really grateful that you decided to learn this subject with me. I hope that it's a great addition to your skills, and that in the future days, the things that you want to build are made more possible through what you've learned here. So I hope you go and practice what you've learned here, and I'm excited to see you implement and build your own ideas. Thank you, and good luck.