|
| 1 | ++++ |
| 2 | +title = "Electron" |
| 3 | +weight = 290 |
| 4 | ++++ |
| 5 | + |
| 6 | +Electron is heavy, but really cross-platform, and it has many tools |
| 7 | +around it. It allows to build releases for the three major OS from |
| 8 | +your development machine, its ecosystem has tools to handle updates, |
| 9 | +etc. |
| 10 | + |
| 11 | +Advise: study it before discarding it. |
| 12 | + |
| 13 | +It isn't however the only portable web view solution. See our next section. |
| 14 | + |
| 15 | +{{% notice info %}} |
| 16 | +This page appeared first on [lisp-journey: three web views for Common Lisp, cross-platform GUIs](https://lisp-journey.gitlab.io/blog/three-web-views-for-common-lisp--cross-platform-guis/). |
| 17 | +{{% /notice %}} |
| 18 | + |
| 19 | +## Ceramic (old but works) |
| 20 | + |
| 21 | +[Ceramic](https://github.com/ceramic/ceramic/) is a set of utilities |
| 22 | +around Electron to help you build an Electron app: download the npm |
| 23 | +packages, open a browser window, etc. |
| 24 | + |
| 25 | +Here's its getting started snippet: |
| 26 | + |
| 27 | +```lisp |
| 28 | +;; Start the underlying Electron process |
| 29 | +(ceramic:start) |
| 30 | +;; ^^^^^ this here downloads ±200MB of node packages under the hood. |
| 31 | +
|
| 32 | +;; Create a browser window |
| 33 | +(defvar window (ceramic:make-window :url "https://www.google.com/" |
| 34 | + :width 800 |
| 35 | + :height 600)) |
| 36 | +
|
| 37 | +;; Show it |
| 38 | +(ceramic:show window) |
| 39 | +``` |
| 40 | + |
| 41 | +When you run `(ceramic:bundle :ceramic-hello-world)` you get a .tar |
| 42 | +file with your application, which you can distribute. Awesome! |
| 43 | + |
| 44 | +But what if you don't want to redirect to google.com but open your own |
| 45 | +app? You just build your web app in CL, run the webserver |
| 46 | +(Hunchentoot, Clack…) on a given port, and you'll open |
| 47 | +`localhost:[PORT]` in Ceramic/Electron. That's it. |
| 48 | + |
| 49 | +Ceramic wasn't updated in five years as of date and it downloads an |
| 50 | +outdated version of Electron by default (see `(defparameter |
| 51 | +*electron-version* "5.0.2")`), but you can change the version yourself. |
| 52 | + |
| 53 | +The new [Neomacs project, a structural editor and web browser](https://github.com/neomacs-project/neomacs/), is a great modern example on how to use Ceramic. Give it a look and give it a try! |
| 54 | + |
| 55 | +What Ceramic actually does is abstracted away in the CL functions, so |
| 56 | +I think it isn't the best to start with. We can do without it to |
| 57 | +understand the full process, here's how. |
| 58 | + |
| 59 | +- Ceramic API reference: http://ceramic.github.io/docs/api-reference.html |
| 60 | + |
| 61 | +## Electron from scratch |
| 62 | + |
| 63 | +Here's our web app embedded in Electron: |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | +Our steps are the following: |
| 68 | + |
| 69 | +- follow the Electron installation instructions, |
| 70 | +- build a binary of your Lisp web app, including assets and HTML templates, if any. |
| 71 | + * see this post: https://lisp-journey.gitlab.io/blog/lisp-for-the-web-build-standalone-binaries-foreign-libraries-templates-static-assets/ (the process will be a tad simpler without Djula templates) |
| 72 | +- bundle this binary into the final Electron build. |
| 73 | +- and that's it. |
| 74 | + |
| 75 | +You can also run the Lisp web app from sources, of course, without |
| 76 | +building a binary, but then you'll have to ship all the lisp sources. |
| 77 | + |
| 78 | +<!-- You can also have a look at --> |
| 79 | +<!-- https://github.com/mikelevins/electron-lisp-boilerplate for this, --> |
| 80 | +<!-- their main.js has the pattern, using child_process. --> |
| 81 | + |
| 82 | +### main.js |
| 83 | + |
| 84 | +The most important file to an Electron app is the main.js. The one we show below does the following: |
| 85 | + |
| 86 | +- it starts Electron |
| 87 | +- it starts our web application on the side, as a child process, from a binary name, and a port. |
| 88 | +- it shows our child process' stdout and stderr |
| 89 | +- it opens a browser window to show our app, running on localhost. |
| 90 | +- it handles the close event. |
| 91 | + |
| 92 | +Here's our version. |
| 93 | + |
| 94 | +```javascript |
| 95 | +console.log(`Hello from Electron 👋`) |
| 96 | + |
| 97 | +const { app, BrowserWindow } = require('electron') |
| 98 | + |
| 99 | +const { spawn } = require('child_process'); |
| 100 | + |
| 101 | +// FIXME Suppose we have our app binary at the current directory. |
| 102 | + |
| 103 | +// FIXME This is our hard-coded binary name. |
| 104 | +var binaryPaths = [ |
| 105 | + "./openbookstore", |
| 106 | +]; |
| 107 | + |
| 108 | +// FIXME Define any arg required for the binary. |
| 109 | +// This is very specific to the one I built for the example. |
| 110 | +var binaryArgs = ["--web"]; |
| 111 | + |
| 112 | +const binaryapp = null; |
| 113 | + |
| 114 | +const runLocalApp = () => { |
| 115 | + "Run our binary app locally." |
| 116 | + console.log("running our app locally…"); |
| 117 | + const binaryapp = spawn(binaryPaths[0], binaryArgs); |
| 118 | + return binaryapp; |
| 119 | +} |
| 120 | + |
| 121 | +// Start an Electron window. |
| 122 | + |
| 123 | +const createWindow = () => { |
| 124 | + const win = new BrowserWindow({ |
| 125 | + width: 800, |
| 126 | + height: 600, |
| 127 | + }) |
| 128 | + |
| 129 | + // Open localhost on the app's port. |
| 130 | + // TODO: we should read the port from an environment variable or a config file. |
| 131 | + // FIXME hard-coded PORT number. |
| 132 | + win.loadURL('http://localhost:4242/') |
| 133 | +} |
| 134 | + |
| 135 | +// Run our app. |
| 136 | +let child = runLocalApp(); |
| 137 | + |
| 138 | +// We want to see stdout and stderr of the child process |
| 139 | +// (to see our Lisp app output). |
| 140 | +child.stdout.on('data', (data) => { |
| 141 | + console.log(`stdout:\n${data}`); |
| 142 | +}); |
| 143 | + |
| 144 | +child.stderr.on('data', (data) => { |
| 145 | + console.error(`stderr: ${data}`); |
| 146 | +}); |
| 147 | + |
| 148 | +child.on('error', (error) => { |
| 149 | + console.error(`error: ${error.message}`); |
| 150 | +}); |
| 151 | + |
| 152 | +// Handle Electron close events. |
| 153 | +child.on('close', (code) => { |
| 154 | + console.log(`openbookstore process exited with code ${code}`); |
| 155 | +}); |
| 156 | + |
| 157 | +// Open it in Electron. |
| 158 | +app.whenReady().then(() => { |
| 159 | + createWindow(); |
| 160 | + |
| 161 | + // Open a window if none are open (macOS) |
| 162 | + if (process.platform == 'darwin') { |
| 163 | + app.on('activate', () => { |
| 164 | + if (BrowserWindow.getAllWindows().length === 0) createWindow() |
| 165 | + }) |
| 166 | + } |
| 167 | +}) |
| 168 | + |
| 169 | + |
| 170 | +// On Linux and Windows, quit the app main all windows are closed. |
| 171 | +app.on('window-all-closed', () => { |
| 172 | + if (process.platform !== 'darwin') { |
| 173 | + app.quit(); |
| 174 | + } |
| 175 | +}) |
| 176 | +``` |
| 177 | + |
| 178 | +Run it with `npm run start` (you also have an appropriate packages.json), this gets you the previous screenshot. |
| 179 | + |
| 180 | +JS and Electron experts, please criticize and build on it. |
| 181 | + |
| 182 | +**Missing parts** |
| 183 | + |
| 184 | +We didn't fully finish the example: we need to automatically bundle |
| 185 | +the binary into the Electron release. |
| 186 | + |
| 187 | +Then, if you want to communicate from the Lisp app to the Electron |
| 188 | +window, and the other way around, you'll have to use the JavaScript layers. Ceramic might help here. |
| 189 | + |
| 190 | +## What about Tauri? |
| 191 | + |
| 192 | +Bundling an app with [Tauri](https://tauri.app/) will, AFAIK (I just |
| 193 | +tried quickly), involve the same steps than with Electron. Tauri might |
| 194 | +still have less tools for it. You need the Rust toolchain. |
0 commit comments