Plushie for Gleam
Build native desktop apps in Gleam. Pre-1.0
Write your entire application in Gleam (state, events, UI) and get native windows on Linux, macOS, and Windows. The renderer is built on Iced and ships as a precompiled binary, no Rust toolchain required.
SDKs are also available for Elixir, Python, Ruby, and TypeScript.
Quick start
import gleam/int
import plushie/app
import plushie/gui
import plushie/command
import plushie/event.{type Event, Click, EventTarget, Widget}
import plushie/node.{type Node}
import plushie/prop/padding
import plushie/ui
import plushie/widget/column
import plushie/widget/row
import plushie/widget/window
type Model {
Model(count: Int)
}
fn init() {
#(Model(count: 0), command.none())
}
fn update(model: Model, event: Event) {
case event {
Widget(Click(target: EventTarget(id: "inc", ..))) ->
#(Model(count: model.count + 1), command.none())
Widget(Click(target: EventTarget(id: "dec", ..))) ->
#(Model(count: model.count - 1), command.none())
_ -> #(model, command.none())
}
}
fn view(model: Model) -> List(Node) {
[
ui.window("main", [window.Title("Counter")], [
ui.column(
"content",
[column.Padding(padding.all(16.0)), column.Spacing(8.0)],
[
ui.text_("count", "Count: " <> int.to_string(model.count)),
ui.row("buttons", [row.Spacing(8.0)], [
ui.button_("inc", "+"),
ui.button_("dec", "-"),
]),
],
),
]),
]
}
pub fn main() {
gui.run(app.simple(init, update, view), gui.default_opts())
}
Add plushie to your dependencies and run:
gleam add plushie_gleam
gleam run -m plushie/download # download precompiled binary
gleam run -m my_app # run your app
Pin to an exact version and read the CHANGELOG carefully when upgrading.
The precompiled binary requires no Rust toolchain. To build from
source instead, install rustup and
cargo-plushie (see the
installation hints printed by plushie/build if it’s not yet on
PATH), then run gleam run -m plushie/build.
The repo includes several examples you can try. Edit them while the GUI is running and see changes instantly. See the getting started guide for the full walkthrough, or browse the docs for all guides and references.
How it works
Under the hood, a renderer built on iced handles window drawing and platform integration. Your Gleam code sends widget trees to the renderer over stdin; the renderer draws native windows and sends user events back over stdout.
You don’t need Rust to use plushie. The renderer is a precompiled binary, similar to how your app talks to a database without you writing C. If you ever need custom native rendering, the custom widgets guide shows how to compose widgets in Gleam and when to drop to Rust for native widgets.
The same protocol works over a local pipe, an SSH connection, or any bidirectional byte stream - your code doesn’t need to change. See the shared state guide for deployment and remote rendering options.
Features
- Elm architecture - init, update, view. State lives in Gleam, pure functions, predictable updates
- Built-in widgets - layout, input, display, and interactive widgets out of the box
- Canvas - shapes, paths, gradients, transforms, and interactive elements for custom 2D drawing
- Themes - dark, light, nord, catppuccin, tokyo night, and more, with custom palettes and per-widget style overrides
- Animation - renderer-side transitions, springs, and sequences with no wire traffic per frame
- Multi-window - declare windows in your view; the framework manages the rest
- Platform effects - native file dialogs, clipboard, OS notifications
- Accessibility - keyboard navigation, screen readers, and focus management via AccessKit
- Custom widgets - compose existing widgets in pure Gleam, draw on the canvas, or extend with native Rust
- Hot reload - edit code, see changes instantly with full
state preservation (requires
file_systemdep and Elixir; see Getting Started) - Remote rendering - app on a server or embedded device, renderer on a display machine over SSH or any byte stream
- Multi-target - runs on BEAM and JavaScript, same codebase
Testing and automation
Tests run through the real renderer binary, not mocks. Interact like a user: click, type, find elements, assert on text. Three interchangeable backends:
- Mock - millisecond tests, no display server
- Headless - real rendering via tiny-skia, supports screenshots for pixel regression in CI
- Windowed - real windows with GPU rendering, platform effects, real input
import gleeunit/should
import gleam/option
import plushie/testing
import plushie/testing/element
pub fn add_and_complete_a_todo_test() {
let session = testing.start(todo_app)
let session = testing.type_text(session, "new_todo", "Buy milk")
let session = testing.submit(session, "new_todo")
let assert option.Some(el) = testing.find(session, "todo_count")
let assert option.Some(txt) = element.text(el)
should.equal(txt, "1 item")
should.be_true(option.is_some(testing.find(session, "todo:1")))
let session = testing.toggle(session, "todo:1")
let session = testing.click(session, "filter_completed")
let assert option.Some(el) = testing.find(session, "todo_count")
let assert option.Some(txt) = element.text(el)
should.equal(txt, "0 items")
should.be_true(option.is_none(testing.find(session, "todo:1")))
}
See the testing reference for the full API, backend details, and CI configuration.
Status
Pre-1.0. The core works (built-in widgets, event system, themes, multi-window, testing framework, accessibility) but the API is still evolving. Pin to an exact version and read the CHANGELOG when upgrading.
License
MIT