Getting started

Build native desktop GUIs from Gleam. Plushie handles rendering via iced (Rust) while you own state, logic, and UI trees in pure Gleam.

Prerequisites

Setup

1. Create a new Gleam project

gleam new my_app
cd my_app

2. Add plushie as a dependency

gleam add plushie_gleam

3. Fetch dependencies and build the renderer

gleam run -m plushie/build

The build step compiles the Rust renderer binary. First build takes a few minutes; subsequent builds are fast.

Your first app: a counter

Create src/my_app.gleam:

import gleam/int
import plushie/app
import plushie/gui
import plushie/command
import plushie/event.{type Event, WidgetClick}
import plushie/node.{type Node}
import plushie/prop/padding
import plushie/ui

type Model {
  Model(count: Int)
}

fn init() {
  #(Model(count: 0), command.none())
}

fn update(model: Model, event: Event) {
  case event {
    WidgetClick(id: "increment", ..) -> #(
      Model(count: model.count + 1),
      command.none(),
    )
    WidgetClick(id: "decrement", ..) -> #(
      Model(count: model.count - 1),
      command.none(),
    )
    _ -> #(model, command.none())
  }
}

fn view(model: Model) -> Node {
  ui.window("main", [ui.title("Counter")], [
    ui.column("content", [ui.padding(padding.all(16.0)), ui.spacing(8)], [
      ui.text("count", "Count: " <> int.to_string(model.count), [
        ui.font_size(20.0),
      ]),
      ui.row("buttons", [ui.spacing(8)], [
        ui.button_("increment", "+"),
        ui.button_("decrement", "-"),
      ]),
    ]),
  ])
}

pub fn main() {
  gui.run(app.simple(init, update, view), gui.default_opts())
}

Run it:

gleam run -m my_app

A native window appears with the count and two buttons.

The Elm architecture

Plushie follows the Elm architecture. Your app is built from three functions passed to app.simple:

For apps that need subscriptions, use app.with_subscriptions to add a subscribe callback that returns a list of active subscriptions (timers, keyboard events).

See App behaviour for the full API.

Event types

Events are constructors of the Event type in plushie/event. Pattern match in update:

EventMeaning
WidgetClick(id: id, ..)Button click
WidgetInput(id: id, value: val, ..)Text input change
WidgetSubmit(id: id, value: val, ..)Text input Enter
WidgetToggle(id: id, value: val, ..)Checkbox/toggler
WidgetSlide(id: id, value: val, ..)Slider moved
WidgetSelect(id: id, value: val, ..)Pick list/radio
TimerTick(tag: tag, timestamp: ts)Timer fired

See Events for the full taxonomy.

CLI helpers

Plushie provides CLI modules for common tasks:

// src/my_app.gleam -- build and run
import plushie/gui

pub fn main() {
  gui.run(my_app(), gui.default_opts())
}
// src/inspect_app.gleam -- print UI tree as JSON
import plushie/inspect

pub fn main() {
  inspect.run(my_app())
}
gleam run -m plushie/build               # build renderer only
gleam run -m plushie/build -- --release  # release build
gleam run -m plushie/download            # download precompiled binary

Use GuiOpts to configure the runner:

gui.run(my_app(), GuiOpts(..gui.default_opts(), json: True))   // JSON wire format
gui.run(my_app(), GuiOpts(..gui.default_opts(), dev: True))    // live reload

Debugging

Use JSON wire format to see messages between Gleam and the renderer:

gui.run(my_app(), GuiOpts(..gui.default_opts(), json: True))

Enable verbose renderer logging:

RUST_LOG=plushie=debug gleam run -m my_app

Error handling

If update or view raises, the runtime catches the exception, logs it, and continues with the previous state. The GUI does not crash. Fix the code and the next event works normally. See the crash-lab demo for all three failure paths (Gleam panics and Rust extension panics) in action.

Dev mode

Live code reloading without losing application state. Enable it by setting dev: True in your GuiOpts:

gui.run(my_app(), GuiOpts(..gui.default_opts(), dev: True))

In dev mode, the dev server watches src/ for changes, recompiles, hot-reloads BEAM modules, and triggers a re-render without losing app state. Edit any .gleam file, save, and the GUI updates in place. The model is preserved – only view is re-evaluated with the new code.

Try it with the counter example – run with dev: True, then edit your view function and save. The window updates instantly.

Next steps

Search Document