zgx

Getting started

From nothing to a window, a list, selectable text, a settings window and a shipped app, in Luau.

From nothing to a window, then a list, selectable text, a settings window and a shipped app: first in Rust, then in Luau with zgx-run. Every snippet here is compiled (Rust) or run headless (Luau) by the repository’s tests, so they work as written.

zgx runs on macOS, Windows and Linux. On Linux, install the fontconfig, xkbcommon and D-Bus development packages and pkg-config; on Windows, use the MSVC toolchain (notes).

The same interface as a script, hot reloaded as you save it, without a Rust host of your own. Install the runner and the checker:

LUAU_CXXFLAGS=-DNDEBUG cargo install --git https://github.com/zeronsh/zgx zgx-run
LUAU_CXXFLAGS=-DNDEBUG cargo install --git https://github.com/zeronsh/zgx zgx-script --bin zgx

(zgx-run uses mimalloc as its allocator, for 6–10% less CPU on script, text and layout work at about 2 MB more memory; add --no-default-features to keep the system’s. LUAU_CXXFLAGS=-DNDEBUG builds Luau without its asserts, a fifth faster: the repository’s own builds set it in .cargo/config.toml, which an install doesn’t read. Without it, zgx-run says so as it starts.)

A window

An app is a directory with an app.luau returning its interface, and optionally a zgx.json for its window:

{ "title": "Hello", "width": 480, "height": 320, "app_id": "com.example.hello" }
return ui.column { p = 24, gap = 12, ui.text { "Hello, zgx" } }

zgx-run hello/ opens it; saving app.luau updates the window and keeps its state. zgx check hello/app.luau type checks it (with luau-lsp installed), and zgx lint finds what is slow or wrong though it runs.

State

ui.state holds a value by key: a reload keeps it. Text templates ("{count}") follow it; actions such as ui.add change it without calling the script.

local count = ui.state("count", 0)
return ui.row {
	p = 24,
	gap = 12,
	items = "center",
	ui.button { on_click = ui.add(count, 1), "Add" },
	ui.text { "Clicked {count} times" },
}

A list

ui.list mounts only the rows in view; keyboard = true makes it one Tab stop with arrow keys.

return ui.list {
	count = 10000,
	height = 32,
	keyboard = true,
	w_full = true,
	h_full = true,
	row = function(i: number): Node
		return ui.row { px = 12, items = "center", ui.text { `Row {i}` } }
	end,
}

Selectable text

return ui.column {
	selectable = true,
	p = 24,
	gap = 8,
	ui.text { "Release notes", text_size = 22, heading = 1 },
	ui.text { "Drag across these lines to select them, then copy.", wrap = true },
}

A settings window

ui.window declares another window, open while its state is true; ui.command puts an item in the menu bar, with its shortcut (cmd is Command on macOS and Control elsewhere).

local dark = ui.persist("dark", true)
local settings = ui.state("settings", false)
ui.command {
	id = "settings",
	title = "Settings…",
	menu = "File",
	key = "cmd+,",
	run = function()
		settings:set(true)
	end,
}
ui.window {
	id = "settings",
	title = "Settings",
	width = 360,
	height = 200,
	open = settings,
	ui.column { p = 24, ui.checkbox { label = "Dark appearance", value = dark } },
}
return ui.text {
	function()
		return if dark:get() then "Dark" else "Light"
	end,
	p = 24,
}

Shipping

A shipped app carries its scripts compiled into the binary: a build script bundles the directory, and a small Rust host runs it from the bundle (shipping an app). Build it with the dist profile, as a Rust app (above), and set LUAU_CXXFLAGS=-DNDEBUG for it (cost). The mail example (crates/zgx-script/examples/mail) is a whole app written this way.

Next

Edit on GitHub