lucynui 0.1.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/cli/create.ts +25 -126
- package/cli/postinstall.ts +18 -56
- package/cli/proc.ts +6 -8
- package/package.json +1 -1
- package/src/lucyn.ts +18 -6
- package/src/server-worker.ts +15 -0
- package/template/app.ts +1 -1
- package/template/index.html +6 -1
- package/template/package.json +1 -1
- package/template/pages/about.html +39 -0
- package/template/pages/docs.html +57 -0
package/README.md
CHANGED
|
@@ -2,9 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Desktop apps from plain HTML, CSS and JS. Write web files, get a native window.
|
|
4
4
|
|
|
5
|
-
Built on [webview-bun](https://github.com/
|
|
5
|
+
Built on a [fork of webview-bun](https://github.com/LucyNBrowserEngine/webview-bun) and [Elysia](https://elysiajs.com), running on Bun.
|
|
6
6
|
No React. No Rust. No Electron.
|
|
7
7
|
|
|
8
|
+
Part of the [LucyN engine family](https://github.com/LucyNBrowserEngine/LucyNEngine).
|
|
9
|
+
|
|
10
|
+
## Why this exists
|
|
11
|
+
|
|
12
|
+
Electron is 200+ MB before you've written a line of code. Qt was next — building a UI in QPainter was miserable.
|
|
13
|
+
So the question became: can a desktop app just be plain HTML, CSS and JS, no framework underneath? Turned out yes.
|
|
14
|
+
|
|
15
|
+
The search for a lightweight window led to webview.h, the famous single-header C library. The plan was to write a
|
|
16
|
+
Bun wrapper for it — then [webview-bun](https://github.com/tr1ckydev/webview-bun) turned up, already doing exactly
|
|
17
|
+
that. So, like any decent open-source citizen: forked it, merged it in, and LucyN UI was born.
|
|
18
|
+
|
|
8
19
|
## Quick start
|
|
9
20
|
|
|
10
21
|
Scaffold a new app and start it:
|
|
@@ -86,7 +97,8 @@ no `app.ts` in your project root.
|
|
|
86
97
|
bin/lucyn.ts the CLI entry point
|
|
87
98
|
cli/ create-ui, init, dev, build, install hook, terminal styling
|
|
88
99
|
src/ the framework
|
|
89
|
-
lucyn.ts createApp — starts server + window
|
|
100
|
+
lucyn.ts createApp — starts the server (in a Worker) + opens the window
|
|
101
|
+
server-worker.ts runs the Elysia file server, off the main thread
|
|
90
102
|
config.ts reads lucyn.config.ts
|
|
91
103
|
server.ts Elysia file server + hot reload
|
|
92
104
|
build.ts compile step
|
|
@@ -106,6 +118,12 @@ webview-bun/ native window bindings + prebuilt libraries for linux, mac a
|
|
|
106
118
|
|
|
107
119
|
Paths are resolved inside the project root only — anything escaping it gets a 403.
|
|
108
120
|
|
|
121
|
+
## Architecture note
|
|
122
|
+
|
|
123
|
+
`webview.run()` is a blocking native call — once the window opens, it freezes Bun's main thread until the window
|
|
124
|
+
closes. If the file server ran on that same thread it would go dead the instant the window appeared, so `createApp()`
|
|
125
|
+
runs the server in a `Worker` instead, keeping it responsive for the whole life of the window.
|
|
126
|
+
|
|
109
127
|
## Notes
|
|
110
128
|
|
|
111
129
|
- On Windows you get a warning on startup: LucyN UI is built and tested on Linux, and Windows builds differ.
|
|
@@ -113,3 +131,11 @@ Paths are resolved inside the project root only — anything escaping it gets a
|
|
|
113
131
|
- If `index.html` is missing, the window shows a helpful error instead of a blank page.
|
|
114
132
|
- Ports: if the configured port is taken, LucyN UI keeps incrementing until it finds a free one and logs the port it
|
|
115
133
|
actually used.
|
|
134
|
+
|
|
135
|
+
## Note
|
|
136
|
+
|
|
137
|
+
At v1.0.0 this might be done. Elysia isn't updating its API anytime soon, neither is webview, neither is Bun — me
|
|
138
|
+
bolting on more APIs after that is just too much work for too little reason. I made this lib because I wanted an
|
|
139
|
+
easier alternative to Qt's paint-and-draw, and it works for that. So don't come in acting like this is dead — what
|
|
140
|
+
more do you want me to add?
|
|
141
|
+
Lucyn is just a new baby of me being a cpp god but just tired writing ui in cpp forgive me cpp gods i got tired good luck
|
package/cli/create.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import { closeSync, openSync } from "node:fs";
|
|
2
1
|
import { cp } from "node:fs/promises";
|
|
3
2
|
import { basename, join } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import * as proc from "./proc";
|
|
6
5
|
import * as ui from "./ui";
|
|
7
6
|
|
|
8
|
-
/** Matches the line the framework logs once the server is listening. */
|
|
9
7
|
const SERVER_READY = /dev server running on http:\/\/localhost:(\d+)/;
|
|
10
|
-
|
|
11
|
-
/** Files copied from the bundled template into a new project. */
|
|
12
|
-
const TEMPLATE_FILES = ["index.html", "style.css", "app.js", "app.ts", "lucyn.config.ts", "tsconfig.json", ".gitignore"];
|
|
8
|
+
const TEMPLATE_FILES = ["index.html", "style.css", "app.js", "app.ts", "lucyn.config.ts", "tsconfig.json", ".gitignore", "package.json"];
|
|
13
9
|
|
|
14
10
|
const bun = () => proc.findBun() ?? "bun";
|
|
15
11
|
|
|
16
|
-
/** Absolute path to the bundled template directory inside this package. */
|
|
17
12
|
function templateDir(): string {
|
|
18
13
|
return fileURLToPath(new URL("../template", import.meta.url));
|
|
19
14
|
}
|
|
@@ -28,71 +23,51 @@ export async function dev(): Promise<never> {
|
|
|
28
23
|
export async function build(): Promise<never> {
|
|
29
24
|
proc.requireProject();
|
|
30
25
|
ui.info("Building your app...");
|
|
31
|
-
|
|
32
26
|
const code = await proc.run(process.cwd(), [bun(), proc.frameworkPath("build.ts")]);
|
|
33
27
|
if (code !== 0) process.exit(code);
|
|
34
|
-
|
|
35
28
|
ui.ok("Build complete! Check ./dist");
|
|
36
29
|
process.exit(0);
|
|
37
30
|
}
|
|
38
31
|
|
|
39
|
-
/** `lucyn create-ui <appname>`
|
|
32
|
+
/** `lucyn create-ui <appname>` */
|
|
40
33
|
export async function create(appName: string): Promise<never> {
|
|
41
34
|
if (process.platform === "win32") ui.warn(ui.WINDOWS_NOTICE);
|
|
42
|
-
|
|
43
|
-
const bunPath = await proc.ensureBun();
|
|
35
|
+
await proc.ensureBun();
|
|
44
36
|
await proc.ensureSystemDeps();
|
|
45
37
|
|
|
46
|
-
ui.step(1, "
|
|
38
|
+
ui.step(1, "copying template files");
|
|
47
39
|
await scaffoldInto(join(process.cwd(), appName));
|
|
48
|
-
|
|
49
|
-
ui.done("scaffolding complete — fetching the deps and required builds");
|
|
50
|
-
|
|
51
|
-
ui.step(2, "installing modules including lucynui");
|
|
52
|
-
await proc.runStep(appName, [bunPath, "install"]);
|
|
53
|
-
ui.done("modules installed");
|
|
40
|
+
ui.done(`app created in ./${appName}`);
|
|
54
41
|
|
|
55
|
-
ui.
|
|
56
|
-
|
|
57
|
-
ui.
|
|
42
|
+
ui.line();
|
|
43
|
+
ui.info(` cd ${appName}`);
|
|
44
|
+
ui.info(" bun install");
|
|
45
|
+
ui.info(" bun run dev");
|
|
46
|
+
ui.line();
|
|
58
47
|
|
|
59
|
-
|
|
60
|
-
const code = await runDev(appName, (port) => {
|
|
61
|
-
ui.done(`dev is up and running on port ${port}`);
|
|
62
|
-
ui.ready(basename(appName));
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
process.exit(code);
|
|
48
|
+
process.exit(0);
|
|
66
49
|
}
|
|
67
50
|
|
|
68
|
-
/** `lucyn` / `lucyn init`
|
|
51
|
+
/** `lucyn` / `lucyn init` */
|
|
69
52
|
export async function init(): Promise<never> {
|
|
70
|
-
|
|
53
|
+
await proc.ensureBun();
|
|
71
54
|
await proc.ensureSystemDeps();
|
|
72
55
|
|
|
73
56
|
if (await isProject(process.cwd())) {
|
|
74
|
-
ui.info("
|
|
75
|
-
|
|
76
|
-
ui.step(1, "scaffolding the template, hang on");
|
|
77
|
-
await scaffoldInto(process.cwd());
|
|
78
|
-
ui.done("scaffolding complete — fetching the deps and required builds");
|
|
79
|
-
|
|
80
|
-
ui.step(2, "installing modules including lucynui");
|
|
81
|
-
await proc.runStep(process.cwd(), [bunPath, "install"]);
|
|
82
|
-
ui.done("modules installed");
|
|
83
|
-
|
|
84
|
-
ui.step(3, "testing build");
|
|
85
|
-
await testBuild(process.cwd());
|
|
86
|
-
ui.done("build was successful");
|
|
57
|
+
ui.info("Already a LucyN UI app. Run: bun run dev");
|
|
58
|
+
process.exit(0);
|
|
87
59
|
}
|
|
88
60
|
|
|
89
|
-
ui.step(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
ui.ready(basename(process.cwd()));
|
|
93
|
-
});
|
|
61
|
+
ui.step(1, "copying template files");
|
|
62
|
+
await scaffoldInto(process.cwd());
|
|
63
|
+
ui.done("done");
|
|
94
64
|
|
|
95
|
-
|
|
65
|
+
ui.line();
|
|
66
|
+
ui.info(" bun install");
|
|
67
|
+
ui.info(" bun run dev");
|
|
68
|
+
ui.line();
|
|
69
|
+
|
|
70
|
+
process.exit(0);
|
|
96
71
|
}
|
|
97
72
|
|
|
98
73
|
/* ------------------------------------------------------------------- helpers */
|
|
@@ -101,20 +76,6 @@ export function isProject(dir: string) {
|
|
|
101
76
|
return Bun.file(join(dir, proc.CONFIG_FILE)).exists();
|
|
102
77
|
}
|
|
103
78
|
|
|
104
|
-
/** Runs a build and throws away the binary — the point is proving it compiles. */
|
|
105
|
-
export async function testBuild(dir: string) {
|
|
106
|
-
const { rm } = await import("node:fs/promises");
|
|
107
|
-
const code = await proc.run(dir, [bun(), proc.frameworkPath("build.ts")]);
|
|
108
|
-
await rm(join(dir, "dist"), { recursive: true, force: true });
|
|
109
|
-
|
|
110
|
-
if (code !== 0) throw new Error("the test build failed");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Copies the bundled template files into `dir`, never overwriting existing
|
|
115
|
-
* files — except `package.json`: `bun add` creates a bare manifest (no scripts)
|
|
116
|
-
* before postinstall runs, so we always replace it with the full template version.
|
|
117
|
-
*/
|
|
118
79
|
export async function scaffoldInto(dir: string) {
|
|
119
80
|
const { mkdir } = await import("node:fs/promises");
|
|
120
81
|
await mkdir(dir, { recursive: true });
|
|
@@ -123,35 +84,11 @@ export async function scaffoldInto(dir: string) {
|
|
|
123
84
|
|
|
124
85
|
for (const file of TEMPLATE_FILES) {
|
|
125
86
|
const target = join(dir, file);
|
|
126
|
-
if (await Bun.file(target).exists()) continue;
|
|
87
|
+
if (file !== "package.json" && await Bun.file(target).exists()) continue;
|
|
127
88
|
await cp(join(source, file), target);
|
|
128
89
|
}
|
|
129
|
-
|
|
130
|
-
// Always write the template package.json — the bare one bun creates before
|
|
131
|
-
// postinstall (no scripts, no name, no type) is useless and must be replaced.
|
|
132
|
-
await cp(join(source, "package.json"), join(dir, "package.json"));
|
|
133
90
|
}
|
|
134
91
|
|
|
135
|
-
/** Points the app at this CLI's own version, so template drift can never bite. */
|
|
136
|
-
async function pinFramework(dir: string) {
|
|
137
|
-
const manifest = Bun.file(join(dir, "package.json"));
|
|
138
|
-
if (!(await manifest.exists())) return;
|
|
139
|
-
|
|
140
|
-
// Running from a checkout rather than an install? Point the app at this copy
|
|
141
|
-
// so the whole flow can be tried out before the package is published.
|
|
142
|
-
const spec = import.meta.dir.includes("node_modules")
|
|
143
|
-
? `^${await proc.packageVersion()}`
|
|
144
|
-
: `file:${proc.packageRoot()}`;
|
|
145
|
-
|
|
146
|
-
const pkg = await manifest.json();
|
|
147
|
-
pkg.dependencies = { ...(pkg.dependencies ?? {}), lucynui: spec };
|
|
148
|
-
await Bun.write(manifest, JSON.stringify(pkg, null, 2) + "\n");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Starts dev in the foreground, forwarding output, and calls `onReady` with the
|
|
153
|
-
* port the server actually bound to.
|
|
154
|
-
*/
|
|
155
92
|
async function runDev(dir: string, onReady: (port: number) => void): Promise<number> {
|
|
156
93
|
const child = Bun.spawn([bun(), proc.frameworkPath("lucyn.ts"), "--dev"], {
|
|
157
94
|
cwd: dir,
|
|
@@ -167,7 +104,6 @@ async function runDev(dir: string, onReady: (port: number) => void): Promise<num
|
|
|
167
104
|
for await (const chunk of child.stdout) {
|
|
168
105
|
process.stdout.write(chunk);
|
|
169
106
|
if (announced) continue;
|
|
170
|
-
|
|
171
107
|
seen += decoder.decode(chunk, { stream: true });
|
|
172
108
|
const port = SERVER_READY.exec(seen)?.[1];
|
|
173
109
|
if (port) {
|
|
@@ -178,40 +114,3 @@ async function runDev(dir: string, onReady: (port: number) => void): Promise<num
|
|
|
178
114
|
|
|
179
115
|
return await child.exited;
|
|
180
116
|
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Starts dev detached so the caller can exit, writing output to `logPath`.
|
|
184
|
-
* Returns the port, or null when the window never came up.
|
|
185
|
-
*/
|
|
186
|
-
export async function devDetached(dir: string, logPath: string): Promise<number | null> {
|
|
187
|
-
const fd = openSync(logPath, "a");
|
|
188
|
-
|
|
189
|
-
const child = Bun.spawn([bun(), proc.frameworkPath("lucyn.ts"), "--dev"], {
|
|
190
|
-
cwd: dir,
|
|
191
|
-
stdin: "ignore",
|
|
192
|
-
stdout: fd,
|
|
193
|
-
stderr: fd,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
for (let attempt = 0; attempt < 80; attempt++) {
|
|
198
|
-
await Bun.sleep(250);
|
|
199
|
-
|
|
200
|
-
const log = await Bun.file(logPath).text();
|
|
201
|
-
const port = SERVER_READY.exec(log)?.[1];
|
|
202
|
-
|
|
203
|
-
if (port) {
|
|
204
|
-
// Give it a moment to prove it survives the window actually opening.
|
|
205
|
-
await Bun.sleep(500);
|
|
206
|
-
return child.exitCode === null ? Number(port) : null;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (child.exitCode !== null) return null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return null;
|
|
213
|
-
} finally {
|
|
214
|
-
child.unref();
|
|
215
|
-
closeSync(fd);
|
|
216
|
-
}
|
|
217
|
-
}
|
package/cli/postinstall.ts
CHANGED
|
@@ -1,64 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cp } from "node:fs/promises";
|
|
2
2
|
import { join, sep } from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import * as ui from "./ui";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* When the folder is empty it scaffolds the starter app, proves the build works
|
|
10
|
-
* and leaves a dev server running before the install command returns. Any folder
|
|
11
|
-
* that already looks like a project is left completely alone.
|
|
12
|
-
*/
|
|
13
|
-
export async function postinstall(): Promise<void> {
|
|
14
|
-
// Only when installed as a dependency — not while developing this package.
|
|
15
|
-
if (!import.meta.dir.includes("node_modules")) return;
|
|
16
|
-
if (process.env.LUCYN_SKIP_POSTINSTALL) return;
|
|
5
|
+
if (!import.meta.dir.includes("node_modules")) process.exit(0);
|
|
6
|
+
if (process.env.LUCYN_SKIP_POSTINSTALL) process.exit(0);
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
const projectDir = process.env.INIT_CWD
|
|
9
|
+
?? process.env.npm_config_local_prefix
|
|
10
|
+
?? (() => {
|
|
11
|
+
const i = import.meta.dir.lastIndexOf(`${sep}node_modules${sep}`);
|
|
12
|
+
return i === -1 ? null : import.meta.dir.slice(0, i);
|
|
13
|
+
})();
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
if (existsSync(join(dir, "index.html")) || existsSync(join(dir, "src"))) return;
|
|
15
|
+
if (!projectDir) process.exit(0);
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
ui.line();
|
|
17
|
+
const template = fileURLToPath(new URL("../template", import.meta.url));
|
|
18
|
+
const FILES = ["index.html", "style.css", "app.js", "app.ts", "lucyn.config.ts", "tsconfig.json", ".gitignore", "package.json"];
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
ui.step(2, "installing modules including lucynui");
|
|
33
|
-
ui.done("modules installed");
|
|
34
|
-
|
|
35
|
-
ui.step(3, "testing build");
|
|
36
|
-
await create.testBuild(dir);
|
|
37
|
-
ui.done("build was successful");
|
|
38
|
-
|
|
39
|
-
ui.step(4, "starting dev with hmr");
|
|
40
|
-
const logPath = join(dir, ".lucyn-dev.log");
|
|
41
|
-
const port = await create.devDetached(dir, logPath);
|
|
42
|
-
|
|
43
|
-
if (port === null) {
|
|
44
|
-
ui.warn(`The window did not come up. Run lucyn dev yourself — log: ${logPath}`);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
ui.done(`dev is up and running on port ${port}`);
|
|
49
|
-
ui.line();
|
|
50
|
-
ui.line(` ${ui.accent("Log:")} ${ui.text(logPath)}`);
|
|
51
|
-
ui.line();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The folder the user ran `bun add` in — the one holding `node_modules`. */
|
|
55
|
-
function projectDir(): string | null {
|
|
56
|
-
const fromEnv = process.env.INIT_CWD ?? process.env.npm_config_local_prefix;
|
|
57
|
-
if (fromEnv) return fromEnv;
|
|
58
|
-
|
|
59
|
-
const marker = `${sep}node_modules${sep}`;
|
|
60
|
-
const index = import.meta.dir.lastIndexOf(marker);
|
|
61
|
-
return index === -1 ? null : import.meta.dir.slice(0, index);
|
|
20
|
+
for (const file of FILES) {
|
|
21
|
+
const target = join(projectDir, file);
|
|
22
|
+
if (file !== "package.json" && await Bun.file(target).exists()) continue;
|
|
23
|
+
await cp(join(template, file), target);
|
|
62
24
|
}
|
|
63
25
|
|
|
64
|
-
|
|
26
|
+
console.log("\n LucyN UI ready.\n\n bun install\n bun run dev\n");
|
package/cli/proc.ts
CHANGED
|
@@ -7,9 +7,6 @@ import * as ui from "./ui";
|
|
|
7
7
|
/** Marks the root of a LucyN UI project. */
|
|
8
8
|
export const CONFIG_FILE = "lucyn.config.ts";
|
|
9
9
|
|
|
10
|
-
/** Where `create-ui` clones the starter app from. */
|
|
11
|
-
export const TEMPLATE_REPO = "https://github.com/LucyNBrowserEngine/lucynui-template";
|
|
12
|
-
|
|
13
10
|
/** Absolute path to the package root, wherever it is installed. */
|
|
14
11
|
export function packageRoot() {
|
|
15
12
|
return fileURLToPath(new URL("..", import.meta.url));
|
|
@@ -20,6 +17,11 @@ export function frameworkPath(file: string) {
|
|
|
20
17
|
return fileURLToPath(new URL(`../src/${file}`, import.meta.url));
|
|
21
18
|
}
|
|
22
19
|
|
|
20
|
+
/** Absolute path to the package's own `template/` folder, wherever it is installed. */
|
|
21
|
+
export function templatePath() {
|
|
22
|
+
return fileURLToPath(new URL("../template", import.meta.url));
|
|
23
|
+
}
|
|
24
|
+
|
|
23
25
|
export async function packageVersion() {
|
|
24
26
|
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
25
27
|
return String(pkg.version ?? "0.0.0");
|
|
@@ -91,8 +93,4 @@ export async function ensureSystemDeps() {
|
|
|
91
93
|
ui.info("Installing system dependencies...");
|
|
92
94
|
await runStep(process.cwd(), ["sudo", "apt", "install", "-y", "libgtk-4-1", "libwebkitgtk-6.0-4"], "inherit");
|
|
93
95
|
ui.ok("Dependencies installed");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function requireGit() {
|
|
97
|
-
if (!Bun.which("git")) throw new Error("git is required to fetch the template");
|
|
98
|
-
}
|
|
96
|
+
}
|
package/package.json
CHANGED
package/src/lucyn.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { SizeHint, Webview } from "../webview-bun/src/webview";
|
|
3
3
|
import { DEFAULTS, loadConfig, type LucynConfig } from "./config";
|
|
4
|
-
import { errorPage,
|
|
4
|
+
import { errorPage, watchForChanges } from "./server";
|
|
5
5
|
|
|
6
6
|
const WINDOWS_NOTICE =
|
|
7
7
|
"You are on Windows. LucyN UI on Windows is not top grade as Windows builds differ from Linux where the framework was made and tested. If you encounter any bugs or build errors please dont hesitate to open an issue at the LucyN org. Thank you.";
|
|
@@ -9,6 +9,10 @@ const WINDOWS_NOTICE =
|
|
|
9
9
|
/**
|
|
10
10
|
* Starts the file server, opens the native window and blocks until it closes.
|
|
11
11
|
*
|
|
12
|
+
* The server runs in a Worker: `webview.run()` is a blocking FFI call that
|
|
13
|
+
* freezes Bun's main-thread event loop for as long as the window is open, so
|
|
14
|
+
* the server would never be able to answer requests if it lived there too.
|
|
15
|
+
*
|
|
12
16
|
* Reads `lucyn.config.ts` from the project root; anything passed here wins.
|
|
13
17
|
*/
|
|
14
18
|
export async function createApp(config: LucynConfig = {}) {
|
|
@@ -17,15 +21,22 @@ export async function createApp(config: LucynConfig = {}) {
|
|
|
17
21
|
const root = resolve(process.cwd());
|
|
18
22
|
const { title, width, height, port, dev } = { ...DEFAULTS, ...(await loadConfig(root)), ...config };
|
|
19
23
|
|
|
20
|
-
const
|
|
24
|
+
const worker = new Worker(new URL("./server-worker.ts", import.meta.url).href);
|
|
25
|
+
const actualPort = await new Promise<number>((resolvePort, rejectPort) => {
|
|
26
|
+
worker.onmessage = (e: MessageEvent) => {
|
|
27
|
+
if (e.data?.ready) resolvePort(e.data.port);
|
|
28
|
+
};
|
|
29
|
+
worker.onerror = (err) => rejectPort(err);
|
|
30
|
+
worker.postMessage({ root, port });
|
|
31
|
+
});
|
|
21
32
|
|
|
22
|
-
console.log(dev ? `LucyN UI dev server running on http://localhost:${
|
|
33
|
+
console.log(dev ? `LucyN UI dev server running on http://localhost:${actualPort}` : `LucyN UI running on http://localhost:${actualPort}`);
|
|
23
34
|
|
|
24
35
|
const webview = new Webview(dev, { width, height, hint: SizeHint.NONE });
|
|
25
36
|
webview.title = title;
|
|
26
37
|
|
|
27
38
|
if (await Bun.file(resolve(root, "index.html")).exists()) {
|
|
28
|
-
webview.navigate(`http://localhost:${
|
|
39
|
+
webview.navigate(`http://localhost:${actualPort}`);
|
|
29
40
|
} else {
|
|
30
41
|
webview.setHTML(
|
|
31
42
|
errorPage(
|
|
@@ -42,9 +53,10 @@ export async function createApp(config: LucynConfig = {}) {
|
|
|
42
53
|
webview.run();
|
|
43
54
|
|
|
44
55
|
watcher?.close();
|
|
45
|
-
|
|
56
|
+
worker.postMessage({ stop: true });
|
|
57
|
+
worker.terminate();
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
if (import.meta.main) {
|
|
49
61
|
await createApp(process.argv.includes("--dev") ? { dev: true } : {});
|
|
50
|
-
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { startServer } from "./server";
|
|
2
|
+
|
|
3
|
+
declare const self: Worker;
|
|
4
|
+
|
|
5
|
+
self.onmessage = async (e: MessageEvent) => {
|
|
6
|
+
const { root, port } = e.data as { root: string; port: number };
|
|
7
|
+
const server = await startServer(root, port);
|
|
8
|
+
self.postMessage({ ready: true, port: server.port });
|
|
9
|
+
|
|
10
|
+
self.onmessage = (stop: MessageEvent) => {
|
|
11
|
+
if (stop.data?.stop) {
|
|
12
|
+
server.stop().then(() => process.exit(0));
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
};
|
package/template/app.ts
CHANGED
package/template/index.html
CHANGED
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
Edit <code>index.html</code>, <code>style.css</code> or <code>app.js</code> and the window reloads by itself.
|
|
15
15
|
</p>
|
|
16
16
|
<button id="ping">Say hello</button>
|
|
17
|
+
|
|
18
|
+
<nav class="links">
|
|
19
|
+
<a href="/pages/about.html">About</a>
|
|
20
|
+
<a href="/pages/docs.html">Docs</a>
|
|
21
|
+
</nav>
|
|
17
22
|
</main>
|
|
18
23
|
<script src="/app.js"></script>
|
|
19
24
|
</body>
|
|
20
|
-
</html>
|
|
25
|
+
</html>
|
package/template/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>About — LucyN UI</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="app">
|
|
11
|
+
<p class="brand">LucyN UI</p>
|
|
12
|
+
<h1>Why this exists</h1>
|
|
13
|
+
|
|
14
|
+
<p>
|
|
15
|
+
LucyN UI is part of the LucyN engine family. It was mainly made because I didn't like
|
|
16
|
+
Electron being big, and also hated how hard Qt made UI to build.
|
|
17
|
+
</p>
|
|
18
|
+
<p>
|
|
19
|
+
So I asked myself: is it possible to build a desktop app in plain, simple HTML, CSS and JS?
|
|
20
|
+
Turned out it is. I came across the likes of
|
|
21
|
+
<a href="https://tauri.app">Tauri</a> and
|
|
22
|
+
<a href="https://www.electronjs.org">Electron</a>, but learning they were 200+ MB for a
|
|
23
|
+
start, I stopped there.
|
|
24
|
+
</p>
|
|
25
|
+
<p>
|
|
26
|
+
Then I found a webview library — the infamous single <code>webview.h</code> file. I
|
|
27
|
+
thought about writing a Bun wrapper for it myself, then discovered
|
|
28
|
+
<a href="https://github.com/tr1ckydev/webview-bun">webview-bun</a>, which had already
|
|
29
|
+
written that exact wrapper.
|
|
30
|
+
</p>
|
|
31
|
+
<p>
|
|
32
|
+
So what did I do? Like a noble citizen of open source: forked it, merged it in. And well,
|
|
33
|
+
here we are — <strong>LucyN UI</strong>.
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<button id="back" onclick="window.location.href='/index.html'">Back home</button>
|
|
37
|
+
</main>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Docs — LucyN UI</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="app">
|
|
11
|
+
<p class="brand">LucyN UI</p>
|
|
12
|
+
<h1>Docs</h1>
|
|
13
|
+
|
|
14
|
+
<p>Your app is just files plus a dependency on <code>lucynui</code>:</p>
|
|
15
|
+
<p>
|
|
16
|
+
<code>app.ts</code> — entry point, calls <code>createApp()</code><br />
|
|
17
|
+
<code>index.html</code> — your UI<br />
|
|
18
|
+
<code>style.css</code><br />
|
|
19
|
+
<code>app.js</code><br />
|
|
20
|
+
<code>lucyn.config.ts</code> — title / size / port
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<h1>Commands</h1>
|
|
24
|
+
<p>
|
|
25
|
+
<code>bun run app.ts</code> — start the app<br />
|
|
26
|
+
<code>lucyn dev</code> — dev server + window, hot reload on html/css/js changes<br />
|
|
27
|
+
<code>lucyn build</code> — single binary in <code>./dist</code>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<h1>Config</h1>
|
|
31
|
+
<p>
|
|
32
|
+
<code>lucyn.config.ts</code> in your project root, every field optional:
|
|
33
|
+
<code>title</code>, <code>width</code>, <code>height</code>, <code>port</code>,
|
|
34
|
+
<code>dev</code>.
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<h1>API Reference</h1>
|
|
38
|
+
<p>
|
|
39
|
+
I mean — do you know HTML? Yes. Do you know CSS? Yes. Do you know JS? Yes. Then what the
|
|
40
|
+
hell are you doing here, just go edit the template.
|
|
41
|
+
</p>
|
|
42
|
+
<p>
|
|
43
|
+
From <code><createdappfolder>/app.ts</code>, you've got two ways to run it:
|
|
44
|
+
</p>
|
|
45
|
+
<p>
|
|
46
|
+
<code>bun run app.ts</code> or <code>lucynui dev</code> — start the app<br />
|
|
47
|
+
<code>bun run build</code> or <code>lucynui build</code> — ship a binary
|
|
48
|
+
</p>
|
|
49
|
+
<p>That's the whole API. You already know the rest.</p>
|
|
50
|
+
|
|
51
|
+
<nav class="links">
|
|
52
|
+
<a href="/index.html">Back home</a>
|
|
53
|
+
<a href="/pages/about.html">About</a>
|
|
54
|
+
</nav>
|
|
55
|
+
</main>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|