- Status: Beta? I'm not sure what to call it, but it works way better than any alpha ive seen! but hasnt been around long enough to call it stable yet. This is a big release and brings with it support for linux and windows installers for apps built with lotus. This allows you to build apps with lotus and just npx lotus build and it will build the app installer for you for your platform. It is still a bit early so any feedback is appreciated! I plan for this to be a continued project for a while and I'm excited to see what we can do with it.
- Windows: MSI, EXE
- Linux: RPM, DEB, AppImage, Pacman, Flatpak
- Mac: None (yet! please feel free to contribute!)
- Windows: 10, 11
- Linux: Arch, Debian, Fedora, openSUSE, Ubuntu
- Mac: None (yet! please feel free to contribute!)
In general linux, bsd, and windows are supported. Mac is not supported because I don't have a mac. I dont know enough about bsd or suse to automate building installers for them yet but it should fully run there and if not please feel free to open an issue and i will do my best to resolve it!
π "ELECTRON IS AN 80s FORD BRONCO." "Huge. Heavy. Built to survive off-roading, river crossings, and the open internet. Every window spins up a full browser like it's about to get lost in the wilderness."
ποΈ "LOTUS IS... WELL, A LOTUS ELISE." "If a part doesn't make it start faster, use less memory, or render pixels, it's gone. No extra suspension. No spare tires. No browser pretending to be an operating system."
π₯ THE ARCHITECTURE (Or: Why It's Fast) "Most desktop apps are just opening a preferences panel. I didn't think that required a second operating system."
β’ Electron Strategy: Puts the browser in charge and lets Node ride shotgun. "It builds a monster truck because it assumes you're off-roading." β’ Lotus Strategy: The opposite. "Node owns the OS. Servo paints the pixels. No magic. No fake sandboxes. No hidden Chromium instances listening to how you use each piece of chrome for telemetry."
π¨ STATUS: ALPHA (BUT IT WORKS)
We have working Windows and Linux builds available on npm (@lotus-gui/core@0.2.0).
Mac support is missing (because their ecosystem needs an adult, please be its adult!). BSD and SUSE support is planned (because I know the pain points over there, see Roadmap). Tested support for building fully packaged .rpm installers for Linux but should support .deb and basic windows installers.
π§ THE ANALOGY THAT EXPLAINS EVERYTHING: β’ Node.js is the track. β’ Servo is the car. β’ IPC is the steering wheel. "On a track, you don't worry about potholes. You worry about lap times."
TL;DR: Electron assumes you're lost. Lotus assumes you know where you're going. And that's why it's fast.
π‘ THE POINT: "Node.js already does OS integration. We just needed a renderer. That's it. That's the whole project."
-
Speed that actually matters:
- Cold start to interactive window in <300ms. You can't even blink that fast.
- A single window stack (Rust + Node + Servo) runs on ~300MB RAM.
- Adding a second window costs ~80MB. We share the renderer. We don't spawn a new universe for every pop-up.
-
Hybrid Runtime:
- Core: Rust-based Servo engine. It renders HTML/CSS. That's it.
- Controller: Node.js main thread. It does literally everything else.
-
Hybrid Mode (File Serving):
- Custom Protocol:
lotus-resource://serves files from disk. - Why? Because spinning up an HTTP server just to show a JPEG is stupid.
- Security: Directory jailing. You can't
../../your way to/etc/passwd. Nice try.
- Custom Protocol:
-
Advanced IPC (The Steering Wheel):
- WebSocket IPC Server: We use
tokio+axumon127.0.0.1:0with persistentWebSocketconnections. It works. It's pretty dam fast and low latency. - Auto-Adapting: JSON? Binary? Blobs? We don't care. We handle it via WebSockets natively.
- MsgPack Batching (Pipelines): We pack small messages together and unleash them in bursts to avoid starving the Winit rendering thread.
- Zero-Copy Routing: We avoid parsing giant megabytes of JSON simply to route a window event. Copying/allocating data is for people who like waiting.
invoke()/handle()(Promise IPC): The renderer callswindow.lotus.invoke('ch', data)and gets back a realPromise. Node registersipcMain.handle('ch', async fn). No manual reply channels, no leaked listeners, no correlation IDs in your app code.- File Drag-and-Drop: OS-level file drag is intercepted from winit and forwarded to both the renderer (
window.lotus.on('file-drop', ...)) and Node.js (win.on('file-drop', ...)). Zero Servo involvement -- pure winit event forwarding.
- WebSocket IPC Server: We use
-
Window State Persistence:
- It remembers where you put the window (if you give it an ID). Groundbreaking technology, I know.
- Handles maximized state, size, position. You're welcome.
-
Script Injection:
- Execute arbitrary JS in the renderer from the main process. God mode unlocked.
-
Native Look & Feel:
- true OS transparency, and actual working cursors. We don't just emulate a window; we are a window.
- No White Flash: We paint transparently. Your users won't be blinded by a white box while your 5MB of JS loads.
-
Frameless Windows:
- Kill the title bar. Remove the frame. Build whatever crap you want.
- Custom Drag Regions: Mark any element with
-webkit-app-region: dragordata-lotus-drag. Lotus bridges it to the OS -- no JS required. - Custom Resize Borders: 8px invisible resize handles on every edge and corner. They just work.
- Cursor-Aware: Resize cursors show up at the borders. Servo drives all other cursors (grab, pointer, text, etc.) -- no interference.
-
Multi-Window Support:
- Spawn multiple independent windows from a single Node process.
- Shared renderer = ~80MB per extra window.
Lotus is organized as a monorepo with two packages:
lotus/
βββ packages/
β βββ lotus-core/ # @lotus-gui/core -- Runtime engine (Servo + Node bindings)
β β βββ src/ # Rust source (N-API bindings, window management)
β β βββ lotus.js # High-level JS API (ServoWindow, IpcMain, App)
β β βββ index.js # Native binding loader
β β βββ resources/ # IPC bridge scripts, debugger
β β βββ test_app/ # Example application
β β
β βββ lotus-dev/ # @lotus-gui/dev -- CLI toolkit for development & packaging
β βββ bin/lotus.js # CLI entry point (lotus dev, build, clean)
β βββ lib/templates/ # Installer templates (RPM spec, etc.)
β
βββ package.json # Monorepo root (npm workspaces)
βββ README.md # You are here
| Package | npm Name | What It Does |
|---|---|---|
| lotus-core | @lotus-gui/core |
The runtime -- Servo engine, window management, IPC. This is what your app require()s. |
| lotus-dev | @lotus-gui/dev |
CLI toolkit -- dev server with hot-reload, build system, DEB/RPM installer packaging. |
If you want to run this, you need to be on an OS that respects you. (to be fair the plan is to support all platforms, so this is more of a joke to clarify)
This is where development happens. It works here. Fully working .node file for Linux is in the artifacts tab.
-
Node.js: v22+. Don't come at me with v14, we legit require it, we are using N-API 4.
-
System Libraries: You need these or things will scream at you.
Ubuntu/Debian:
sudo apt-get update sudo apt-get install libgl1-mesa-dev libssl-dev python3 libfontconfig1-dev # Required for building .deb installers with `lotus build` sudo apt-get install dpkg-dev fakerootFedora:
sudo dnf install mesa-libGL-devel openssl-devel python3 fontconfig-devel # Required for building .rpm installers with `lotus build` sudo dnf install rpm-build
Note: We auto-fix the
GLIBC_TUNABLESstatic TLS issue. If you seeERR_DLOPEN_FAILEDand the app restarts itself, that's just Lotus fixing your environment for you. Don't panic.
- Status: Alpha ("It Works!")
- NPM: β
Verified -
@lotus-gui/coreincludes the pre-built Windows binary. - Build Requirements (only if building from source): Visual Studio Build Tools +
choco install llvm nasm python311.
- Status: HELP WANTED π
- I removed CI support because I honestly just don't know enough about the Mac app lifecycle to do it right. If you are a Mac developer and want to fix this, PRs are welcome. I just don't have a system to test on. "Here be dragons still." π (Translation: Please save me from Xcode.)
| Platform | Arch | Native Binary (.node) |
Installer Target | Status |
|---|---|---|---|---|
| Linux (Debian/Ubuntu) | x64 | β Verified | .deb (Stable) |
Ready |
| Linux (Fedora/RHEL) | x64 | β Verified | .rpm (Stable) |
Ready |
| Linux (openSUSE) | x64 | π Testing | Planned | Alpha |
| Windows | x64 | β Verified | .msi (testing) |
Beta *1 |
| FreeBSD | x64 | π Testing | Planned | Alpha |
| macOS | arm64 | π Help Wanted | TBD | On Hold |
*1 Windows Beta Status:
-
What Works:
- β
Native Binary: Pre-built
.nodeavailable on npm - β
Build System:
lotus buildcreates.msiinstallers - β Runtime: Angle + Servo + Node.js run correctly
- β VC++ Redist: Auto-embedded and installed silently
- β Icon Handling: PNG/JPG β ICO conversion for installers
- β PE Header Patching: No black console window on launch
- β
Native Binary: Pre-built
-
Known Issues:
β οΈ Installer Signing: Not yet implemented (requires EV cert)β οΈ NSIS Fallback:lotus build --target nsisstill uses old logicβ οΈ Drag-and-Drop: Not yet tested on Windows
Note:
- Installer Target: The packaged distribution format (what users download/install)
- Native Binary: The
.nodefile that powers the runtime (what developersrequire())
For a detailed breakdown of build success, environmental linking, and functional testing per platform, see the master tracking issue: π Multi-Platform Support & Build Targets.
- Build Success: CI/CD produces artifacts without warnings.
- Functionality: Core features operational on clean installs.
- Environment: Proper linking of native dependencies.
The Easy Way: working Windows and Linux builds are on npm. You don't need to build from source.
The fastest way to get started is with the CLI:
npx lotus init my-app
cd my-app
npm install
npx lotus devIf you prefer to set things up yourself:
mkdir my-lotus-app && cd my-lotus-app
npm init -y
npm install @lotus-gui/core @lotus-gui/devThen see the example code below or check
node_modules/@lotus-gui/core/test_appfor a full reference.
This file controls your app's metadata and build settings:
{
"name": "MyApp",
"version": "1.0.0",
"license": "MIT",
"description": "My desktop app, minus the bloat",
"main": "main.js",
"executableName": "my-app",
"icon": "./assets/icon.png",
"build": {
"linux": {
"wmClass": "my-app",
"categories": ["Utility"]
}
}
}const { ServoWindow, ipcMain, app } = require('@lotus-gui/core');
const path = require('path');
app.warmup(); // Wake up the engine
const win = new ServoWindow({
id: 'main-window',
root: path.join(__dirname, 'ui'),
index: 'index.html',
width: 1024,
height: 768,
title: "My Lotus App",
transparent: true,
visible: false
});
// Show only after first frame -- no white flash, ever
win.once('frame-ready', () => win.show());
// IPC: talk to the webpage
ipcMain.on('hello', (data) => {
console.log('Renderer says:', data);
ipcMain.send('reply', { message: 'Hello from Node!' });
});
// Or use invoke() for a clean request/reply pattern
ipcMain.handle('get-time', async () => {
return { ts: Date.now() };
});mkdir uiui/index.html:
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body style="background: transparent;">
<div style="background: rgba(0,0,0,0.9); color: white; padding: 2rem; border-radius: 12px;">
<h1>Hello from Lotus! πͺ·</h1>
<button onclick="window.lotus.send('hello', { from: 'renderer' })">
Talk to Node.js
</button>
<button onclick="getTime()">
Invoke get-time (Promise)
</button>
</div>
<script>
window.lotus.on('reply', (data) => {
console.log('Node says:', data.message);
});
async function getTime() {
const { ts } = await window.lotus.invoke('get-time');
console.log('Server time:', new Date(ts).toISOString());
}
</script>
</body>
</html>npx lotus dev main.jsThe config file lives in your project root and controls both runtime behavior and build output.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Application display name |
version |
string |
Yes | Semver version (e.g., "1.0.0") |
license |
string |
No | SPDX license identifier. Defaults to "Proprietary" |
description |
string |
No | Short description (used in package managers) |
main |
string |
No | Entry point file. Falls back to package.json main, then index.js |
executableName |
string |
No | Binary name (e.g., my-app β /usr/bin/my-app). Defaults to lowercase name |
icon |
string |
No | Path to app icon (relative to project root) |
author |
string |
No | Maintainer name for package metadata |
homepage |
string |
No | Project URL |
build.linux.wmClass |
string |
No | Window manager class (taskbar grouping) |
build.linux.section |
string |
No | Package section (default: "utils") |
build.linux.categories |
string[] |
No | Desktop entry categories |
The @lotus-gui/dev package provides the lotus CLI:
# Start dev server with hot-reload (watches for changes, auto-restarts)
lotus dev [entry]
# Build a distributable installer (DEB or RPM)
lotus build --platform <linux|win32> --target <deb|rpm>
# Clean build artifacts (removes dist/)
lotus cleanSee the full @lotus-gui/dev documentation for details on build output, flags, and project setup.
Stop using Express to serve static files. It's embarrassing.
const { ServoWindow, app } = require('@lotus-gui/core');
app.warmup(); // Wake up the engine
const win = new ServoWindow({
root: '/absolute/path/to/ui', // Jail the renderer here
index: 'index.html', // Start here
width: 1024,
height: 768,
title: "My Hybrid Lotus App"
});
// Now serving at lotus-resource://localhost/index.htmlThe renderer is a webpage. The main process is Node. They talk.
Renderer (The Webpage):
// Fire and forget.
window.lotus.send('channel', { magic: true });
// Binary.
const blob = new Blob(['pure binary fury']);
window.lotus.send('binary-channel', blob);
// Promise-based request/reply -- no manual reply channels needed.
const result = await window.lotus.invoke('get-data', { id: 42 });
console.log(result);Main Process (Node):
const { ipcMain } = require('@lotus-gui/core');
// Fire-and-forget listener.
ipcMain.on('channel', (data) => {
console.log('Renderer said:', data);
ipcMain.send('reply', { status: 'acknowledged' });
});
// Request/reply handler -- pairs with window.lotus.invoke().
ipcMain.handle('get-data', async ({ id }) => {
return await db.find(id); // returned value auto-sent to renderer
});Want a window that keeps the OS vibe? We bridge OS transparency directly to your CSS.
const win = new ServoWindow({
transparent: true, // The magic switch
title: "Ghost Window"
});How it works:
- We set the Servo shell background to
0x00000000(fully transparent). - We tell the OS to make the window transparent.
- Result: The window is invisible. The only thing visible is what you paint.
In your CSS:
/* This makes the whole app see-through to the desktop */
body {
background: transparent;
}
/* This adds a semi-transparent glass effect */
.container {
background: rgba(0, 0, 0, 0.8);
color: white;
}The "White Flash" Killer: Because the default backbone is transparent, there is zero white flash on startup. If your app takes 10ms to load, the user sees their wallpaper for 10ms, not a blinding white rectangle. You're welcome.
Tired of the OS telling you what your title bar looks like? Remove it.
const win = new ServoWindow({
frameless: true, // Kill the native frame
transparent: true, // Optional: go fully borderless
title: "My Borderless App"
});Out of the box you get:
- 8px resize borders on every edge/corner -- just move the mouse to the edge.
- Drag regions driven by CSS -- no JS wiring required.
In your HTML:
<!-- These two approaches both work -->
<div style="-webkit-app-region: drag; cursor: grab;">Drag me to move the window</div>
<div data-lotus-drag="true">Also works</div>Lotus auto-detects elements with -webkit-app-region: drag or data-lotus-drag via injected JS and sends their coordinates to Rust. Mouse down on one of those elements β drag_window(). Mouse down on the border β drag_resize_window(). Everything else β Servo handles it normally.
To exclude an element inside a drag region (like a close button), use -webkit-app-region: no-drag or data-lotus-drag="false".
Full control over the OS window manager directly from JavaScript. You don't need to write native code to build a custom title bar.
// Window manipulation
win.minimize();
win.unminimize();
win.maximize();
win.unmaximize();
win.focus();
win.setMinSize(800, 600); // enforce a minimum, or pass 0,0 to clear
win.setMaxSize(1920, 1080);
// Listen to OS-level events
win.on('moved', ({ x, y }) => console.log('Window moved to', x, y));
win.on('resize', ({ width, height }) => console.log('Resized to', width, height));
win.on('focus', () => console.log('Window gained focus'));
win.on('blur', () => console.log('Window lost focus'));
// File drag-and-drop (OS-level, works everywhere)
win.on('file-hover', ({ path }) => console.log('Hovering:', path));
win.on('file-hover-cancelled', () => console.log('Drag cancelled'));
win.on('file-drop', ({ path }) => console.log('Dropped:', path));
// Renderer-side (same events arrive via window.lotus.on)
// window.lotus.on('file-drop', ({ path }) => showDropOverlay(path));Creating specific windows? Easy. They share the same renderer instance, so it costs ~80MB per extra window instead of ~300MB.
const win1 = new ServoWindow({ title: "Window 1" });
const win2 = new ServoWindow({ title: "Window 2" });
const win3 = new ServoWindow({ title: "Window 3" });
// All three windows share the same renderer process.
// Efficient.By default, windows have the memory span of a goldfish. They forget where they were. If you want them to remember, give them a name.
const win = new ServoWindow({
id: "main-window", // REQUIRED for state saving
title: "I Remember Everything",
restoreState: true // Default is true, obviously
});The Logic:
- No ID? We generate a random UUID. New session, new window, default size.
- With ID? We check
~/.config/app-name/window-state.json. If we've seen "main-window" before, we put it back exactly where you left it. - It snaps back to the last known position faster than you can say "Electron is bloat."
When you're ready to ship your application, Lotus uses a highly optimized Node Single Executable Application (SEA) generation pipeline tied directly into CrabNebula to spit out installer payloads for every OS.
-
Ensure your
lotus.config.jsonis properly configured with your app name, author, icon paths, andresourcesarrays for extra assets. -
Run the build command with your target OS installer format:
# Linux Formats npx lotus build --target appimage npx lotus build --target deb npx lotus build --target pacman # Windows Formats npx lotus build --target exe npx lotus build --target msi
Because Lotus relies on native .node bindings, you must build for the OS you are currently on (e.g., execute --target exe inside a Windows CI/CD runner to get Windows binaries).
Your app is now a real installed application with a binary in /usr/bin/ and everything. Just like a grown-up program.
Pro Tip: You don't actually have to build this yourself. Check the Actions tab on GitHub. Every commit produces working artifacts for Linux and Windows. Download, unzip, use the time saved to beat that level you've been procrastinating on. (expect npm install support without having to build yourself soon -- you can just grab the
.nodefiles from the artifacts tab)
git clone https://github.com/1jamie/project-lotus.git
cd project-lotus
npm installBuild the Native Addon:
cd packages/lotus-core
# Debug Build (Faster compilation, still slow)
npm run build:debug
# Release Build (Optimized, takes eons)
npm run buildAdditional Requirements for Building:
- Rust: Stable toolchain.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- Windows: Visual Studio Build Tools +
choco install llvm nasm python311
Warning: The first build takes forever. You are compiling a browser engine and a Node runtime binding. Go make a coffee. Read a book. Learn a new language. (though we all know you are scrolling TikTok or Reddit, we all know you aren't being productive while the compile runs, none of us ever are) It gets faster after the first time. I promise.
packages/lotus-core/src/lib.rs- The Brain. Main Rust entry point. Handles N-API, Event Loop, IPC.packages/lotus-core/src/window_state.rs- The Memory. Remembers where you put your windows.packages/lotus-core/src/platform.rs- The Politeness. Proper OS integrations.packages/lotus-core/lotus.js- The Body. High-level Node.js wrapper (ServoWindow,IpcMain,App).packages/lotus-core/index.js- The Glue. Native binding loader.packages/lotus-core/test_app/- The Real Demo. Full-featured test app.packages/lotus-dev/bin/lotus.js- The Toolbox. CLI for dev, build, and clean commands.packages/lotus-dev/lib/templates/- The Factory. Installer templates (RPM spec, etc.).
For detailed API documentation, see:
- @lotus-gui/core README -- Full
ServoWindowAPI, IPC reference, architecture - @lotus-gui/dev README -- CLI commands, config reference, build pipeline
PRs are welcome. If you break the winit or glutin version requirements, I will close your PR with extreme prejudice. We need specific embedding traits and I'm already sitting on the edge with winit 0.30.2, don't push me off the edge it has already mentally put me on!
- Fork it.
- Branch it (
git checkout -b feature/cool-stuff). - Commit it (
git commit -m 'Added cool stuff'). - Push it.
- PR it.
- β Frameless Mode: Toggle window decorations off.
- β
CSS Dragging: Bridge for custom CSS drag areas (
-webkit-app-region: drag,data-lotus-drag). - β Resize Borders: Custom 8px resize hit-zones on all edges/corners.
- β
Dev CLI:
lotus initcommand added to create a new Lotus project.
Note: Surprise! I changed my mind and gave you frameless support lol. I was just gonna say deal with it and give you native menu support but then i realized the headache.... so here we are! surprise! Now you can do wtf you want with window decorations!
- Windows Support: Full MSI/EXE distribution (moving beyond just the
.nodebinary). - BSD Support: Bringing the renderer to the BSD community.
- SUSE Support: Expanding
@lotus-gui/devto handle OpenSUSE RPM quirks. - Mac Support? (If someone donates a Mac or a contributor steps up).
- Verify/support build for all supported platforms.: Go through the build process for all supported platforms and verify that it works and fix where it does not.
- Build optimization: See what we can do about the electron builder as it does add deps we may not need and may be able to make it install less deps during the install processes.
- Debugger: Add a debugger to the dev CLI to give access to a dev console for helping with development. Will probably have a seperate package called lotus-core-debug that the dev CLI will use to start the app with a build of lotus with the debug symbols and a developers console in its own window to help with debugging and gui development.
- Open to suggestions: I'm open to suggestions for the future. If you have an idea, let me know. Right now v0.4.0 is just a rough tenative plan for what I might do in the future.
License: MIT. Do whatever you want, just don't blame me if your computer achieves sentience and takes flight.
P.S.
The entire framework core is ~2,500 lines of code.
If that feels suspiciously light, it's because it is. I didn't try to build an OS inside your OS; I just gave Node a window and cut the fat until there was nothing left but speed.
Electron carries the weight of the world. Lotus just carries the pixels.