plushie

Build native desktop apps in Ruby. Pre-1.0

Write your entire application in Ruby (state, events, UI) and get native windows on Linux, macOS, and Windows. Available on RubyGems as plushie. The renderer is built on Iced and ships as a precompiled binary, no Rust toolchain required.

SDKs are also available for Elixir, Gleam, Python, and TypeScript.

Quick start

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: "inc"]
      model.with(count: model.count + 1)
    in Event::Widget[type: :click, id: "dec"]
      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}")
        row(spacing: 8) do
          button("inc", "+")
          button("dec", "-")
        end
      end
    end
  end
end

Plushie.run(Counter)

Add plushie to your Gemfile and download the renderer:

# Gemfile
gem "plushie", "== 0.1.0"
bundle install
rake plushie:download   # download precompiled renderer binary
ruby lib/counter.rb

Requires Ruby 3.2+. The repo includes several other examples you can try. For multi-file projects (custom widgets, native Rust extensions, real project scaffolding), see the plushie-demos repo.

To add Plushie to your own project, see the getting started guide, or browse the docs for all guides and references.

How it works

Your Ruby application and the renderer run as two OS processes that exchange messages. Think of it like talking to a database, except the database is a GPU-accelerated GUI toolkit. The SDK builds UI trees and handles events; the renderer draws native windows and captures input.

The SDK diffs each new tree against the previous one and sends only the changes. If the renderer crashes, Plushie restarts it and re-syncs your state. If your code raises, the SDK reverts to the last good state. Neither process can take the other down.

The same protocol works over a local pipe, an SSH connection, or any bidirectional byte stream. Your code doesn't need to change.

Features

  • Elm architecture - init, update, view. State lives in Ruby, pure functions, predictable updates
  • Block DSL - nested Ruby blocks build the widget tree with natural indentation, no templates or markup
  • 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 Ruby, draw on the canvas, or extend with native Rust
  • Hot reload - Plushie.run(MyApp, dev: true) watches lib/ and reloads on file changes with full state preservation
  • Remote rendering - app on a server or embedded device, renderer on a display machine over SSH or any byte stream
  • Fault-tolerant - renderer crashes auto-recover; app exceptions are caught and state reverted
  • Configuration system - Plushie.configure for binary paths, extensions, test backends, and widget runtime config
  • WASM renderer - rake plushie:download[wasm] for browser targets

Testing and automation

Tests run through the real renderer binary, not mocks. Interact like a user: click, type, find elements, assert on text. All backends support concurrent test execution to keep your suite fast as it grows. 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
class CounterTest < Plushie::Test::Case
  app Counter

  def test_clicking_increment_updates_counter
    click("#inc")
    assert_text "#count", "Count: 1"
  end
end

The same interaction API is available outside Minitest via Plushie::Automation::Session. Attach to any running app and drive it programmatically. Agent-friendly by design.

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