DEV Community

vindarel
vindarel

Posted on • Updated on

Common Lisp GUI with Electron · quick how to

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

In a nutshell:

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();
    }
})
Enter fullscreen mode Exit fullscreen mode

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/

Top comments (0)