lune v0.1.0
Lune
Build native desktop apps with Crystal and a web frontend.
Lune wraps a native WebView and lets you call Crystal code from JavaScript over a typed bridge — no servers, no IPC boilerplate. Think Wails or Tauri, but for Crystal.
Prerequisites
Getting the CLI
The CLI is not distributed as a pre-built binary. Clone this repo and either install it globally or run it directly:
git clone https://github.com/aristorap/lune
cd lune
make setup # shards install
make deploy # build release binary → /usr/local/bin/lune
# or run without installing (runs relative to your path):
crystal run bin/lune.cr -- <command>
Quick start
With the CLI on your PATH:
lune init my_app
cd my_app
lune dev
lune init scaffolds a Crystal entry point and a Vite frontend. lune dev compiles your Crystal app and starts the Vite dev server together, with hot-reload on source changes. See examples/main.cr for what the generated entry point looks like.
Adding Lune to an existing project
Add it to your shard.yml:
dependencies:
lune:
github: aristorap/lune
version: ~> 0.1
shards install
You still need the CLI for lune dev and lune build — see Getting the CLI.
Crystal API
Lune.run
require "lune"
Lune.run(
title: "My App",
assets: "frontend/dist", # embedded at compile time
width: 1200,
height: 800,
min_width: 800,
min_height: 600,
debug: false,
on_load: -> { puts "page loaded" },
on_navigate: ->(url : String) { puts "navigated to #{url}" },
on_close: -> { puts "window closed" },
) do |app|
# register bindings here
end
Navigation priority (first match wins):
html:— inline HTML stringurl:— explicit URLLUNE_DEV_URLenv var — set automatically bylune devassets:— directory embedded at compile time, served over a local HTTP server
Binding Crystal to JavaScript
bind_typed — single typed argument, return is auto-converted:
app.bind_typed("greet", String) { |msg| "Hello, #{msg}!" }
bind — raw Array(JSON::Any) args, for multiple positional arguments:
app.bind("add") do |args|
a = args[0].as_i
b = args[1].as_i
JSON::Any.new((a + b).to_i64)
end
bind_typed with a struct — named arguments via a JSON-serializable struct:
struct AddArgs
include JSON::Serializable
getter a : Int32
getter b : Int32
end
app.bind_typed("add", AddArgs) { |args| args.a + args.b }
bind_async — same raw signature, runs the block off the main thread:
app.bind_async("slow_echo") do |args|
sleep 1.second
JSON::Any.new("(delayed) #{args[0].as_s}")
end
Namespaces
Group related bindings under a dot-separated prefix:
app.namespace("counter") do |counter|
counter.bind_typed("inc", Int32) { |n| n + 1 }
counter.bind_typed("dec", Int32) { |n| n - 1 }
end
Namespaces compose: math.trig.sin registers as math.trig.sin in JS.
Plugin modules
Extract binding sets into reusable Installable modules:
class GreetModule
include Lune::Installable
def install(app : Lune::App)
app.bind_typed("greet", String) { |msg| "Hello, #{msg}!" }
end
end
Lune.run(title: "My App", assets: "frontend/dist") do |app|
app.install(GreetModule.new)
end
JavaScript API
Lune generates frontend/lunejs/app/App.js from your registered bindings. Import api for a fully dynamic proxy, or import named stubs directly:
import api from "../lunejs/app/App.js";
// dynamic proxy — any registered binding works
const msg = await api.greet("world");
const next = await api.counter.inc(0);
// named stub — same call, IDE-autocompletable
import { greet } from "../lunejs/app/App.js";
const msg = await greet("world");
All bindings return Promise. Exceptions thrown in Crystal reject the promise.
CLI
lune init [APP_NAME] Scaffold a new Lune app (--template vanilla|vue)
lune dev Start Vite + Crystal with hot-reload
lune check Type-check without building
lune build Build frontend + compile Crystal binary
lune build --release Build with Crystal --release optimizations
lune run Launch the previously built artifact
Shared flags (apply to all commands):
--frontend-dir Frontend directory (default: frontend)
--app-entry Crystal entry file (default: src/main.cr)
--debug Enable debug logging
lune build output
lune build
# macOS → build/bin/my_app.app
# Linux → build/bin/my_app
The frontend is compiled with npm run build and embedded in the binary via Crystal macros — the artifact is a single self-contained file.
Development
make setup # shards install + npm install
make test # crystal spec
make deploy # build release binary + copy to /usr/local/bin
Contributing
- Fork it (https://github.com/aristorap/lune/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Add specs for your changes (
crystal spec) - Commit and push (
git commit -am 'Add feature' && git push origin my-new-feature) - Open a Pull Request
Contributors
- Aristotelis Rapai — creator and maintainer
License
MIT
lune
- 1
- 0
- 0
- 0
- 2
- about 3 hours ago
- May 9, 2026
MIT License
Sat, 09 May 2026 20:18:52 GMT