Skip to content

Commit c85c727

Browse files
committed
Electron, web views
1 parent e469456 commit c85c727

File tree

3 files changed

+330
-2
lines changed

3 files changed

+330
-2
lines changed

content/building-blocks/building-binaries.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ weight = 150
44
+++
55

66

7-
### Running the application from source
7+
## Running the application from source
88

99
{{% notice info %}}
1010
See the tutorial.
@@ -53,7 +53,7 @@ running instance. See the following section:
5353
[connecting to a remote lisp image](https://lispcookbook.github.io/cl-cookbook/debugging.html#remote-debugging) on the Cookbook.
5454

5555

56-
### Building a self-contained executable
56+
## Building a self-contained executable
5757

5858
{{% notice info %}}
5959
See the tutorial.
@@ -93,3 +93,4 @@ stops. That happens because the server thread is started in the background, and
9393
~~~
9494

9595
If you want to learn more, see… [the Cookbook: scripting, command line arguments, executables](https://lispcookbook.github.io/cl-cookbook/scripting.html).
96+
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
![](https://media2.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flqjy44zgpae1jp5vkxwx.png "Our web app running on Electron")
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.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
+++
2+
title = "Web views: cross-platform GUIs"
3+
weight = 300
4+
+++
5+
6+
Web views are lightweight and cross-platform. They are nowadays a good
7+
solution to ship a GUI program to your users.
8+
9+
We present two: WebUI and Webview.h, through CLOG Frame.
10+
11+
{{% notice info %}}
12+
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/).
13+
{{% /notice %}}
14+
15+
## WebUI
16+
17+
[WebUI](https://github.com/webui-dev/webui/) is a new kid in town. It is in development, it has bugs. You can view it as a wrapper around a browser window (or webview.h).
18+
19+
However it is ligthweight, it is *easy to build* and we have Lisp bindings.
20+
21+
A few more words about it:
22+
23+
> Use any web browser or WebView as GUI, with your preferred language in the backend and modern web technologies in the frontend, all in a lightweight portable library.
24+
25+
- written in pure C
26+
- one header file
27+
- multi-platform & multi-browser
28+
- opens a real browser (you get the web development tools etc)
29+
- cross-platform webview
30+
- we can call JS from Common Lisp, and call Common Lisp from JS.
31+
32+
> Think of WebUI like a WebView controller, but instead of embedding the WebView controller in your program, which makes the final program big in size, and non-portable as it needs the WebView runtimes. Instead, by using WebUI, you use a tiny static/dynamic library to run any installed web browser and use it as GUI, which makes your program small, fast, and portable. All it needs is a web browser.
33+
34+
> your program will always run on all machines, as all it needs is an installed web browser.
35+
36+
Sounds compelling right?
37+
38+
The other good news is that Common Lisp was one of the first languages
39+
it got bindings for. How it happened: I was chating in Discord, mentioned WebUI and BAM! @garlic0x1 developed bindings:
40+
41+
- https://github.com/garlic0x1/cl-webui/
42+
43+
thank you so much! (@garlic0x1 has more cool projects on GitHub you can browse. He's also a contributor to Lem)
44+
45+
Here's a simple snippet:
46+
47+
48+
```lisp
49+
(defpackage :webui/examples/minimal
50+
(:use :cl :webui)
51+
(:export :run))
52+
(in-package :webui/examples/minimal)
53+
54+
(defun run ()
55+
(let ((w (webui-new-window)))
56+
(webui-show w "<html>Hello, world!</html>")
57+
(webui-wait)))
58+
```
59+
60+
I would be the happiest lisper in the world if I didn't have an annoying issue. See [#1](https://github.com/garlic0x1/cl-webui/issues/1). I can run my example just fine, but nothing happens the second time :/ I don't know if it's a WebUI thing, the bindings, my system, my build of WebUI… so I'll give this more time.
61+
62+
Fortunately though, the third solution of this blog post is my favourite! o/
63+
64+
## CLOG Frame (webview.h for all)
65+
66+
[CLOG Frame](https://github.com/rabbibotton/clog/tree/main/clogframe)
67+
is part of the CLOG framework. However, it is *not* tied to CLOG… nor
68+
to Common Lisp!
69+
70+
CLOG Frame is a short C++ program that builds an executable that takes
71+
an URL and a PORT as CLI parameters and opens a [webview.h](https://github.com/webview/webview) window.
72+
73+
It's easy to build and works just fine. It's a great approach.
74+
75+
Back to our matter.
76+
77+
This is CLOG Frame: 20 lines!
78+
79+
```Cpp
80+
#include <iostream>
81+
#include <sstream>
82+
#include <string>
83+
#include "webview.h"
84+
85+
int main(int argc,char* argv[]) {
86+
webview::webview w(true, nullptr);
87+
webview::webview *w2 = &w;
88+
w.set_title(argv[1]);
89+
w.set_size(std::stoi(argv[3]), std::stoi(argv[4]), WEBVIEW_HINT_NONE);
90+
w.bind("clogframe_quit", [w2](std::string s) -> std::string {
91+
w2->terminate();
92+
return "";
93+
});
94+
std::ostringstream o;
95+
o << "http://127.0.0.1:" << argv[2];
96+
w.navigate(o.str());
97+
w.run();
98+
return 0;
99+
}
100+
```
101+
102+
Compile it on GNU/Linux like this and don't you worry, it takes a fraction of a second:
103+
104+
c++ clogframe.cpp -ldl `pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0` -o clogframe
105+
106+
(see its repo for other platforms)
107+
108+
this gives you a `clogframe` binary. Put it in your $PATH or remember its location. It's just a short C++ binary, so it weights 197Kb.
109+
110+
Now, back to your web app that you wrote in Common Lisp and that is
111+
waiting to be shipped to users.
112+
113+
Start your web app. Say it is started on port 4284.
114+
115+
From the Lisp side, open a CLOG Frame window like this
116+
117+
118+
```lisp
119+
(uiop:launch-program (list "./clogframe"
120+
"Admin"
121+
(format nil "~A/admin/" 4284)
122+
;; window dimensions (strings)
123+
"1280" "840"))
124+
```
125+
126+
and voilà.
127+
128+
129+
![](https://lisp-journey.gitlab.io/images/clogframe-on-top-emacs.png "A CLOG Frame window showing a WIP Common Lisp web app on top of Emacs.")
130+
131+
Now for the cross-platform part, you'll need to build clogframe and
132+
your web app on the target OS (like with any CL app). Webview.h is cross-platform.
133+
Leave us a comment when you have a good CI setup for the three main OSes (I am studying [40ants/ci](https://github.com/40ants/ci/) and [make-common-lisp-program](https://github.com/melusina-org/make-common-lisp-program/) for now).

0 commit comments

Comments
 (0)