Common Lisp GUI with Electron · quick how to
vindarel
Posted on February 21, 2024
Yes, it is possible to bundle a Common Lisp web app into Electron, with any web server, so Hunchentoot is possible.
I have a working POC but was planning to write a detailed blog post with more details and a demo sorted out, so stay tuned ;)
Our method starts the Electron process, starts the Lisp web app as a subprocess on localhost on the given port, and opens this address inside the Electron window.
In a nutshell:
- follow Electron installation instructions,
- build a Lisp web app as a binary
- see 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)
- bundle this binary into the final Electron build.
- and that's it.
I mean, you can run the Lisp web app from sources, but then ship all sources.
You can have a look at https://github.com/mikelevins/electron-lisp-boilerplate for this, their main.js has the pattern, using child_process.
What about Ceramic?
You can ignore the Ceramic project, unfortunately, it's a wrapper around Electron (npm) tools and is broken and outdated. It will try to install an old Electron version and fail. That being said, the Ceramic org has a few useful helper projects we can use.
Example main.js
This file is adapted from the main.js you get after installation, to run an application as a subprocess.
console.log(`Hello from Electron 👋`)
const { app, BrowserWindow } = require('electron')
const { spawn } = require('child_process');
// Suppose we have our app binary at the current directory.
var binaryPaths = [
"./openbookstore",
];
// Define any arg required for the binary.
var binaryArgs = ["--web"];
const binaryapp = null;
const runLocalApp = () => {
"Run our binary app locally."
console.log("running our app locally…");
const binaryapp = spawn(binaryPaths[0], binaryArgs);
return binaryapp;
}
// Start an Electron window.
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
})
// Open localhost on the app's port.
// We should read the port from an environment variable or a config file.
win.loadURL('http://localhost:4242/')
}
// Run our app.
let child = runLocalApp();
// We want to see stdout and stderr of the child process
// (to see our Lisp app output).
child.stdout.on('data', (data) => {
console.log(`stdout:\n${data}`);
});
child.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
child.on('error', (error) => {
console.error(`error: ${error.message}`);
});
// Handle Electron close events.
child.on('close', (code) => {
console.log(`openbookstore process exited with code ${code}`);
});
// Open it in Electron.
app.whenReady().then(() => {
createWindow();
// Open a window if none are open (macOS)
if (process.platform == 'darwin') {
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
}
})
// On Linux and Windows, quit the app main all windows are closed.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
})
What's left as an exercise: automatically bundle the binary into the Electron release.
Then, communicate from Lisp app <-> Electron window (optional though).
Any feedback and demos welcome, this was a quick post.
Happy lisping
ps: and Tauri?
I suppose it's the same process. Prove me wrong ;)
Will test and show an example soon©, if you have experience please share in the comments 🙏
https://lisp-journey.gitlab.io/
Posted on February 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.