Effects

Effects are native platform operations that require the renderer to interact with the OS on behalf of the host. File dialogs, clipboard access, notifications, and similar features are effects.

Design principle

Effects are simple request/response pairs over the same stdio transport. The host asks, the renderer does, the renderer replies. No capability model, no policy engine, no permission framework. If an effect is requested, the renderer executes it.

If granular permission control is needed later, it can be layered in the Gleam runtime (decide whether to send the request) rather than in the renderer (decide whether to execute it). Keep the renderer dumb.

How effects work

Gleam side

import plushie/effects
import plushie/event.{EffectOk, EffectError, EffectCancelled}

fn update(model, event) {
  case event {
    // User clicked the open button
    event.WidgetClick(id: "open_file", ..) -> {
      let cmd = effects.file_open([
        effects.DialogTitle("Choose a file"),
        effects.Filters([#("Text files", "*.txt"), #("All files", "*")]),
      ])
      #(model, cmd)
    }

    // Effect succeeded
    event.EffectResponse(result: EffectOk(data), ..) -> {
      // data is Dynamic -- decode the "path" key
      #(Model(..model, file_path: decode_path(data)), command.none())
    }

    // User cancelled the dialog
    event.EffectResponse(result: EffectCancelled, ..) -> {
      #(model, command.none())
    }

    _ -> #(model, command.none())
  }
}

Every effect function returns a Command(msg). The command must be returned from update as part of a #(model, command) tuple – discarding it silently does nothing. The effect ID is auto-generated (e.g. "ef_1") and embedded in the command payload.

The result arrives as an EffectResponse event in a subsequent update call. Effects are asynchronous – the model is not blocked waiting for the result.

Transport

MessagePack is the default wire format. JSON shown here for readability (use --json flag for JSONL mode).

-> {"type": "effect", "id": "ef_1", "kind": "file_open", "payload": {"title": "Choose a file", "filters": [["Text files", "*.txt"], ["All files", "*"]]}}
<- {"type": "effect_response", "id": "ef_1", "status": "ok", "result": {"path": "/home/user/notes.txt"}}

The id correlates request to response. The runtime generates unique IDs automatically.

Available effects (v1)

KindDescriptionPayloadResult
file_openOpen file dialogtitle, filters, directory{path} or error
file_open_multipleMulti-file open dialogtitle, filters, directory{paths} or error
file_saveSave file dialogtitle, filters, default_name{path} or error
directory_selectDirectory pickertitle{path} or error
directory_select_multipleMulti-directory pickertitle{paths} or error
clipboard_readRead clipboard{text} or error
clipboard_writeWrite to clipboardtextok or error
clipboard_read_htmlRead HTML from clipboard{html} or error
clipboard_write_htmlWrite HTML to clipboardhtml, alt_textok or error
clipboard_clearClear the clipboardok or error
clipboard_read_primaryRead primary selection (Linux){text} or error
clipboard_write_primaryWrite to primary selection (Linux)textok or error
notificationShow OS notificationtitle, body, icon, timeout, urgency, soundok

All effects can return {"status": "error", "error": "unsupported"} if the renderer is running on a platform that does not support the operation.

Adding new effects

Adding an effect requires changes in two places:

  1. Renderer: handle the new kind in the effect dispatch, execute the platform operation, return the result.
  2. Gleam: add a convenience function in plushie/effects (optional, apps can always send raw requests).

The transport does not need to change. Unknown effect kinds return unsupported.

Effects are not commands

Some frameworks conflate effects (I/O operations) with commands (internal state mutations). In plushie, update handles state mutations synchronously. Effects handle I/O asynchronously. They are separate concerns with separate code paths.

If your app needs something that is purely internal (start a timer, schedule a follow-up event, batch multiple updates), that is handled in the Gleam runtime, not as an effect. Effects always involve the renderer or the OS.

Search Document