Getting started

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

Prerequisites

  • Ruby 3.2+ (install via ruby-lang.org or a version manager like rbenv/mise)
  • Bundler (ships with Ruby)
  • System libraries for your platform (only needed if building from source):
    • Linux: a C compiler, pkg-config, and display server headers (e.g. libxkbcommon-dev, libwayland-dev on Debian/Ubuntu)
    • macOS: Xcode command-line tools (xcode-select --install)
    • Windows: Visual Studio C++ build tools

Setup

1. Create a new project

mkdir my_app && cd my_app
bundle init

2. Add plushie as a dependency

# Gemfile
gem "plushie", "== 0.1.0"

3. Install and download the renderer

bundle install

Add Rake tasks to your Rakefile so you can download the precompiled renderer binary:

# Rakefile
require "plushie/rake"

Then:

bundle exec rake plushie:download

The precompiled binary requires no Rust toolchain. To build from source instead, install rustup and run bundle exec rake plushie:build.

4. (Optional) Configure the SDK

If you need to override binary paths, set up extensions, or change the test backend, use Plushie.configure:

Plushie.configure do |config|
  config.binary_path = "/opt/plushie/bin/plushie"
  config.source_path = "~/projects/plushie"
  config.test_backend = :headless
end

See Running and Extensions for the full list of configuration options.

Your first app: a counter

Create lib/counter.rb:

require "plushie"

class Counter
  include Plushie::App

  Model = Plushie::Model.define(:count)

  def init(_opts) = Model.new(count: 0)

  def update(model, event)
    case event
    in Event::Widget[type: :click, id: "increment"]
      model.with(count: model.count + 1)
    in Event::Widget[type: :click, id: "decrement"]
      model.with(count: model.count - 1)
    else
      model
    end
  end

  def view(model)
    window("main", title: "Counter") do
      column(padding: 16, spacing: 8) do
        text("count", "Count: #{model.count}", size: 20)

        row(spacing: 8) do
          button("increment", "+")
          button("decrement", "-")
        end
      end
    end
  end
end

Plushie.run(Counter)

Run it:

bundle exec ruby lib/counter.rb

A native window appears with the count and two buttons.

The Elm architecture

Plushie follows the Elm architecture. Your app class includes Plushie::App and implements these callbacks:

  • init(opts) -- returns the initial model (any Ruby object).
  • update(model, event) -- takes the current model and an event, returns the new model. Pure function. To run side effects, return [model, command] instead. See Commands.
  • view(model) -- takes the model and returns a UI tree. Plushie diffs trees and sends only patches to the renderer.
  • subscribe(model) (optional) -- returns a list of active subscriptions (timers, keyboard events).

See App behaviour for the full callback API.

Event types

Events are Data.define structs under Plushie::Event. Pattern match in update:

Event Meaning
Event::Widget[type: :click, id: id] Button click
Event::Widget[type: :input, id: id, value: val] Text input change
Event::Widget[type: :submit, id: id, value: val] Text input Enter
Event::Widget[type: :toggle, id: id, value: val] Checkbox/toggler
Event::Widget[type: :slide, id: id, value: val] Slider moved
Event::Widget[type: :select, id: id, value: val] Pick list/radio
Event::Timer[tag: tag, timestamp: ts] Timer fired

See Events for the full taxonomy.

Rake tasks

Add require "plushie/rake" to your Rakefile, then:

rake plushie:download              # download precompiled binary
rake plushie:download[wasm]        # download WASM renderer
rake plushie:build                 # build from Rust source (with extensions if configured)
rake plushie:run[Counter]          # run an app
rake plushie:run[Counter,dev]      # run an app with live reload
rake plushie:connect[Counter]      # connect to renderer via stdio (for plushie --exec)
rake plushie:inspect[Counter]      # print UI tree as JSON
rake plushie:script                # run .plushie test scripts
rake plushie:replay[path]          # replay a script with real windows
rake plushie:preflight             # run all CI checks

Debugging

Use JSON wire format to see messages between Ruby and the renderer. Pass format: :json to Plushie.run:

Plushie.run(Counter, format: :json)

Enable verbose renderer logging:

RUST_LOG=plushie=debug bundle exec ruby lib/counter.rb

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.

Dev mode

Live code reloading without losing application state. Add the listen gem to your Gemfile:

gem "listen", "~> 3.0", require: false

Then run with dev: true:

Plushie.run(Counter, dev: true)

Edit any .rb file in lib/, save, and the GUI updates in place. The model is preserved -- only view is re-evaluated with the new code.

Extensions

Plushie supports custom Rust-backed widgets via the extension system. You write the widget logic in Rust (implementing the WidgetExtension trait from plushie-ext) and declare the Ruby-side interface with include Plushie::Extension. The build system compiles a custom renderer binary that includes your extensions.

Pure Ruby composite widgets are also supported -- compose existing Plushie widgets into reusable components without touching Rust.

See Writing widget extensions for the full guide.

Next steps