Common Lisp GUI with Electron · quick how to
Tags: lisp, electron, commonlisp
Published: 2024-02-21 16:11:57

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.


![My Common Lisp web app running inside an Electron window and searching for "lisp" books on french bookstores](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lqjy44zgpae1jp5vkxwx.png)

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.

```javascript
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/

https://github.com/CodyReichert/awesome-cl

https://github.com/sponsors/vindarel/