plushie

Native desktop GUIs from Gleam, powered by iced.

Plushie implements the Elm architecture (init/update/view) with commands and subscriptions. It communicates with a Rust binary over stdin/stdout using MessagePack, driving native windows via iced.

Quick start

import plushie
import plushie/app
import plushie/command
import plushie/event.{type Event, WidgetClick}
import plushie/ui
import plushie/widget/window
import gleam/int

type Model { Model(count: Int) }

pub fn main() {
  let counter = app.simple(
    fn() { #(Model(0), command.none()) },
    fn(model, event) {
      case event {
        WidgetClick(id: "inc", ..) ->
          #(Model(model.count + 1), command.none())
        _ -> #(model, command.none())
      }
    },
    fn(model) {
      ui.window("main", [window.Title("Counter")], [
        ui.text_("count", "Count: " <> int.to_string(model.count)),
        ui.button_("inc", "+"),
      ])
    },
  )
  let assert Ok(_) = plushie.start(counter, plushie.default_start_opts())
}

Types

A running plushie application instance, parameterized over the application’s model type.

The model type flows from the App(model, msg) passed to start. Use get_model to query state with full type safety, stop to shut down, or wait to block until exit.

pub opaque type Instance(model)

Errors that can occur when starting a plushie application.

pub type StartError {
  BinaryNotFound(binary.BinaryError)
  SupervisorStartFailed(actor.StartError)
}

Constructors

Options for starting a plushie application.

pub type StartOpts {
  StartOpts(
    binary_path: option.Option(String),
    format: protocol.Format,
    daemon: Bool,
    session: String,
    app_opts: dynamic.Dynamic,
    required_native_widgets: List(String),
    renderer_args: List(String),
    transport: Transport,
    dev: Bool,
    token: option.Option(String),
  )
}

Constructors

  • StartOpts(
      binary_path: option.Option(String),
      format: protocol.Format,
      daemon: Bool,
      session: String,
      app_opts: dynamic.Dynamic,
      required_native_widgets: List(String),
      renderer_args: List(String),
      transport: Transport,
      dev: Bool,
      token: option.Option(String),
    )

    Arguments

    binary_path

    Path to the plushie binary. None = auto-resolve.

    format

    Wire format. Default: MessagePack.

    daemon

    Keep running after all windows close. Default: False.

    session

    Session identifier. Default: “” (single-session).

    app_opts

    Application options passed to init/1. Default: dynamic.nil().

    required_native_widgets

    Native widgets this app expects the renderer to have loaded.

    renderer_args

    Extra CLI arguments prepended to the renderer command.

    transport

    Transport mode. Default: Spawn.

    dev

    Enable dev-mode live reload. Default: False. When True, starts a file watcher that recompiles on source changes and triggers a force re-render without losing state. Requires the file_system dep and Elixir installed; see the Getting Started guide.

    token

    Authentication token for socket transport. Sent to the renderer as a digest in the settings message. Default: None.

Transport mode for communicating with the renderer.

  • Spawn (default): spawns the renderer binary as a child process using an Erlang Port.
  • Stdio: reads/writes the BEAM’s own stdin/stdout. Used when the renderer spawns the Gleam process (e.g. plushie-renderer --exec).
  • Iostream: sends and receives protocol messages via an external process. Used for custom transports like SSH channels, TCP sockets, or WebSockets where an adapter process handles the underlying I/O.
pub type Transport {
  Spawn
  Stdio
  Iostream(adapter: process.Subject(bridge.IoStreamMessage))
}

Constructors

Values

pub fn await_async(
  instance: Instance(model),
  tag: String,
  timeout: Int,
) -> Result(Nil, Nil)

Wait for an async task with the given tag to complete.

If the task has already completed, returns immediately. Otherwise blocks until the task finishes and its result has been processed through update.

pub fn default_start_opts() -> StartOpts

Default start options.

pub fn dispatch_event(
  instance: Instance(model),
  event: event.Event,
) -> Nil

Dispatch an event directly to the runtime’s message loop.

Bypasses the bridge/renderer. The event is processed through the normal handle_event -> update -> view -> diff -> patch cycle as if it came from the renderer.

Useful for integration tests that need to trigger state changes (clicks, toggles, etc.) in a running application.

pub fn get_focused(
  instance: Instance(model),
) -> Result(option.Option(String), Nil)

Get the ID of the currently focused widget, or None.

Focus is tracked automatically from renderer status events. This is a synchronous query to the runtime.

pub fn get_health(
  instance: Instance(model),
) -> Result(runtime.HealthStatus, Nil)

Query the health status of a running application.

Returns a snapshot of error counters and view desync state. Useful for monitoring, testing, and dev tooling.

pub fn get_model(instance: Instance(model)) -> Result(model, Nil)

Query the current model from a running application.

Returns the model with full type safety: the type parameter flows from the App(model, msg) passed to start.

The first call may block briefly if the runtime is still completing its init sequence (settings, snapshot, subscriptions). The reply always reflects the post-init model state.

pub fn get_prop_warnings(
  instance: Instance(model),
) -> Result(List(runtime.PropWarning), Nil)

Query and clear accumulated prop validation warnings.

Returns a list of PropWarning records for each node that had validation issues since the last query. Warnings are cleared after retrieval. Renderer restarts do not clear these warnings: they describe SDK-generated props, not renderer process health.

pub fn get_tree(
  instance: Instance(model),
) -> Result(option.Option(node.Node), Nil)

Query the current normalized tree from a running application.

Returns None if the runtime hasn’t rendered yet (shouldn’t happen in practice; the initial render runs before start returns).

pub fn interact(
  instance: Instance(model),
  action: String,
  selector: dict.Dict(String, node.PropValue),
  payload: dict.Dict(String, node.PropValue),
  timeout: Int,
) -> Result(Nil, String)

Perform a synchronous interaction on the running app.

Sends an interact request to the renderer which executes the action against the widget tree. Used by scripting engines and testing infrastructure for programmatic UI interactions.

Actions: “click”, “type_text”, “toggle”, “select”, “slide”, “submit”, “press”, “release”, “type_key”, “canvas_press”, etc.

Selector format (Dict keys):

  • By ID: {"by": "id", "value": "widget_id"}
  • By role: {"by": "role", "value": "button"}
  • By label: {"by": "label", "value": "Email"}
  • Focused: {"by": "focused"}

Payload is action-specific (e.g., {"text": "hello"} for type_text, {"value": "3.5"} for slide).

pub fn is_view_desynced(
  instance: Instance(model),
) -> Result(Bool, Nil)

Check if the view is desynced (tree stale due to view errors).

Returns True when the view function has failed consecutively and the tree no longer reflects the current model. Useful in tests to detect that the UI is frozen.

pub fn register_effect_stub(
  instance: Instance(model),
  kind: String,
  response: node.PropValue,
) -> Result(Nil, String)

Register an effect stub with the renderer.

When the renderer receives an effect request of this kind, it returns the given response immediately without executing the real effect. Used for testing (controlled effect responses) and scripting (no user interaction required). The kind must be one of the platform effect kinds from plushie/effect, such as "file_open", "clipboard_read", or "notification".

The response value is returned as-is in an EffectOk result. To simulate cancellation, use dispatch_event with an EffectResponse(result: EffectCancelled) instead.

pub fn start(
  app: app.App(model, msg),
  opts: StartOpts,
) -> Result(Instance(model), StartError)

Start a plushie application under an OTP supervisor.

Creates a RestForOne supervisor with Bridge and Runtime as children. Bridge starts first and opens the port to the renderer binary. Runtime starts second, registers with the bridge, and enters the Elm update loop. If dev mode is enabled, a DevServer child is added.

Returns an Instance(model) that can be used with get_model, dispatch_event, wait, and stop.

pub fn start_error_to_string(err: StartError) -> String

Format a start error as a human-readable string.

pub fn stop(instance: Instance(model)) -> Nil

Stop a running plushie application.

Sends a shutdown exit to the supervisor, which terminates all children (bridge, runtime, dev server) in reverse start order.

pub fn supervisor_pid(instance: Instance(model)) -> process.Pid

Get the supervisor pid from a running instance. Useful for linking, monitoring, or integration with other OTP code.

pub fn unregister_effect_stub(
  instance: Instance(model),
  kind: String,
) -> Result(Nil, String)

Remove a previously registered effect stub.

Blocks until the renderer confirms the stub is removed. Subsequent effects of this kind will be handled normally by the renderer (or return EffectUnsupported if the backend doesn’t support it). The kind must be one of the platform effect kinds from plushie/effect.

pub fn wait(instance: Instance(model)) -> Nil

Block the caller until the plushie application exits.

Monitors the supervisor process and returns when it stops. Use this instead of process.sleep_forever() so that the caller exits cleanly when the user closes all windows.

case plushie.start(app(), plushie.default_start_opts()) {
  Ok(instance) -> plushie.wait(instance)
  Error(err) -> io.println_error(plushie.start_error_to_string(err))
}
Search Document