Plushie mascot

Have fun with Plushie...

Build native desktop apps in Elixir, Gleam, Python, Ruby, and TypeScript

One shared renderer built on Iced. Real native GUI, not Electron.

defmodule Counter do
  use Plushie.App
  alias Plushie.Event.WidgetEvent

  def init(_opts), do: %{count: 0}

  def update(model, %WidgetEvent{type: :click, id: "inc"}),
    do: %{model | count: model.count + 1}
  def update(model, %WidgetEvent{type: :click, id: "dec"}),
    do: %{model | count: model.count - 1}
  def update(model, _event), do: model

  def view(model) do
    import Plushie.UI

    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
defmodule CounterTest do
  use Plushie.Test.Case, app: Counter

  test "count to two and screenshot" do
    click("#inc")
    click("#inc")

    assert_text("#count", "Count: 2")
    assert_screenshot("counter")
  end
end
import gleam/int
import plushie/command
import plushie/event.{type Event, WidgetClick}
import plushie/node.{type Node}
import plushie/prop/padding
import plushie/ui
import plushie/widget/column
import plushie/widget/row
import plushie/widget/window

pub type Model { Model(count: Int) }

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

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

fn view(model: Model) -> Node {
  ui.window("main", [window.Title("Counter")], [
    ui.column("content",
      [column.Padding(padding.all(16.0)), column.Spacing(8)], [
      ui.text_("count",
        "Count: " <> int.to_string(model.count)),
      ui.row("buttons", [row.Spacing(8)], [
        ui.button_("inc", "+"),
        ui.button_("dec", "-"),
      ]),
    ]),
  ])
}
import gleam/option
import gleeunit/should
import plushie/testing
import plushie/testing/element

pub fn count_to_two_test() {
  let ctx = testing.start(counter.app())
  let ctx = testing.click(ctx, "inc")
  let ctx = testing.click(ctx, "inc")

  let assert option.Some(el) = testing.find(ctx, "count")
  should.equal(element.text(el), option.Some("Count: 2"))
  testing.screenshot(ctx, "counter")
}
from dataclasses import dataclass, replace

import plushie
from plushie import ui
from plushie.events import Click

@dataclass(frozen=True, slots=True)
class Model:
    count: int = 0

class Counter(plushie.App[Model]):
    def init(self) -> Model:
        return Model()

    def update(self, model, event):
        match event:
            case Click(id="inc"):
                return replace(model, count=model.count + 1)
            case Click(id="dec"):
                return replace(model, count=model.count - 1)
            case _:
                return model

    def view(self, model):
        return ui.window("main",
            ui.column(
                ui.text("count", f"Count: {model.count}"),
                ui.row(
                    ui.button("inc", "+"),
                    ui.button("dec", "-"),
                    spacing=8,
                ),
                padding=16, spacing=8,
            ),
            title="Counter",
        )
from plushie.testing import AppFixture

def test_count_to_two(plushie_pool):
    with AppFixture(Counter, plushie_pool) as app:
        app.click("#inc")
        app.click("#inc")

        assert app.text("#count") == "Count: 2"
        app.save_screenshot("counter")
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: "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
class CounterTest < Plushie::Test::Case
  app Counter

  test "count to two and screenshot" do
    click("#inc")
    click("#inc")

    assert_text("#count", "Count: 2")
    screenshot("counter")
  end
end
import { app } from "plushie"
import { window, column, row, text, button } from "plushie/ui"

type Model = { count: number }

const inc = (s: Model): Model => ({ ...s, count: s.count + 1 })
const dec = (s: Model): Model => ({ ...s, count: s.count - 1 })

export default app<Model>({
  init: { count: 0 },

  view: (s) =>
    window("main", { title: "Counter" }, [
      column({ padding: 16, spacing: 8 }, [
        text("count", `Count: ${s.count}`),
        row({ spacing: 8 }, [
          button("inc", "+", { onClick: inc }),
          button("dec", "-", { onClick: dec }),
        ]),
      ]),
    ]),
})
import { testWith } from "plushie/testing"
import Counter from "./counter"

testWith(Counter)("count to two", async ({ session }) => {
  await session.click("#inc")
  await session.click("#inc")

  await session.assertText("#count", "Count: 2")
  await session.screenshot("counter")
})

How it works

APPLICATION Desktop app, server dashboard, debug panel, embedded, ... Your Code Whatever you want it to be Plushie SDK Declarative UI, minimal deps
Inter-Process SSH WebSocket In-Process
PLUSHIE RENDERER Rust-based, language-agnostic wire protocol (MessagePack or JSON) Native Desktop windows, no browser needed WASM Same codebase, runs in the browser

Your app describes the UI; the renderer draws it. No Rust toolchain required — the renderer is a pre-compiled binary you download, like a database driver for native GUI.

Multiple users can connect to the same server-side app for shared dashboards, collaborative sessions, and live mirroring — no synchronization code required.

Why Plushie

Native platform

Developer experience

Testing and automation

Deployment

Plushie mascot biting a checkbox