Current section
Files
Jump to
Current section
Files
src/plushie/command_encode.gleam
//// Command-to-wire classification.
////
//// Converts a `Command(msg)` into a `WireOp(msg)`, a tagged union that
//// separates the payload construction (shared between BEAM and JS runtimes)
//// from the actual send mechanism (runtime-specific). Both runtimes
//// pattern-match on `WireOp` and dispatch to their own transport layer.
import gleam/bit_array
import gleam/dict.{type Dict}
import gleam/dynamic.{type Dynamic}
import gleam/list
import gleam/option
import plushie/command.{type Command}
import plushie/node.{
type PropValue, BinaryVal, BoolVal, FloatVal, IntVal, StringVal,
}
/// Wire operations that both runtimes handle identically on the encoding
/// side; only the send mechanism differs.
pub type WireOp(msg) {
/// No side effect.
NoOp
/// Shut down the runtime.
Exit
/// Execute sub-commands in list order.
RunBatch(commands: List(Command(msg)))
/// Inject an already-resolved msg into the update loop.
DoneImmediate(value: Dynamic, mapper: fn(Dynamic) -> msg)
/// Schedule a msg delivery after a delay.
ScheduleTimer(delay_ms: Int, msg: msg)
/// Run a function on a background process / Promise.
SpawnAsync(tag: String, work: fn() -> Dynamic)
/// Run a streaming function that can emit multiple values.
SpawnStream(tag: String, work: fn(fn(Dynamic) -> Nil) -> Dynamic)
/// Cancel a running async/stream task.
CancelTask(tag: String)
/// Unified widget-targeted command (focus, scroll, text, pane, custom).
/// Wire format: {type: "command", id, family, value}
Command(id: String, family: String, value: Dict(String, PropValue))
/// Batch of widget-targeted commands.
/// Wire format: {type: "commands", commands: [{id, family, value}, ...]}
CommandBatch(commands: List(#(String, String, Dict(String, PropValue))))
/// Global widget operation (no target ID: focus_next, announce, etc.).
WidgetOp(op: String, payload: List(#(String, PropValue)))
/// Window lifecycle operation (resize, move, maximize, etc.).
WindowOp(op: String, window_id: String, settings: List(#(String, PropValue)))
/// Window query (get_size, get_position, etc.): a window_op with a tag.
WindowQuery(op: String, window_id: String, tag: String)
/// System-wide operation not tied to a specific window.
SystemOp(op: String, settings: List(#(String, PropValue)))
/// System-wide query.
SystemQuery(op: String, tag: String)
/// Image operation (create, update, delete).
ImageOp(op: String, payload: List(#(String, PropValue)))
/// Load a custom font family for use in subsequent renders.
/// Wire format: {type: "load_font", family, data}, where `data` is a
/// base64 string in JSON and a native binary in MessagePack.
LoadFontMessage(family: String, data: BitArray)
/// Platform effect request (file dialog, clipboard, notification).
EffectRequest(
id: String,
tag: String,
kind: String,
payload: Dict(String, PropValue),
)
/// Advance one frame in test/headless mode.
AdvanceFrame(timestamp: Int)
}
/// Classify a command into a wire operation. All payload construction
/// happens here; the caller only needs to route the result to the
/// appropriate transport.
pub fn classify(cmd: Command(msg)) -> WireOp(msg) {
case cmd {
command.None -> NoOp
command.Exit -> Exit
command.Batch(commands:) -> RunBatch(commands)
command.Done(value:, mapper:) -> DoneImmediate(value, mapper)
command.SendAfter(delay_ms:, msg:) -> ScheduleTimer(delay_ms, msg)
command.Async(work:, tag:) -> SpawnAsync(tag, work)
command.Stream(work:, tag:) -> SpawnStream(tag, work)
command.Cancel(tag:) -> CancelTask(tag)
command.Renderer(renderer_cmd) -> classify_renderer(renderer_cmd)
}
}
fn classify_renderer(cmd: command.RendererCommand) -> WireOp(msg) {
case cmd {
// -- Widget-targeted commands (unified format) --
command.Focus(widget_id:) -> Command(widget_id, "focus", dict.new())
command.FocusNext -> WidgetOp("focus_next", [])
command.FocusPrevious -> WidgetOp("focus_previous", [])
command.FocusNextWithin(scope:) ->
WidgetOp("focus_next_within", [#("scope", StringVal(scope))])
command.FocusPreviousWithin(scope:) ->
WidgetOp("focus_previous_within", [#("scope", StringVal(scope))])
command.SelectAll(widget_id:) ->
Command(widget_id, "select_all", dict.new())
command.MoveCursorToFront(widget_id:) ->
Command(widget_id, "move_cursor_to_front", dict.new())
command.MoveCursorToEnd(widget_id:) ->
Command(widget_id, "move_cursor_to_end", dict.new())
command.MoveCursorTo(widget_id:, position:) ->
Command(
widget_id,
"move_cursor_to",
dict.from_list([#("position", IntVal(position))]),
)
command.SelectRange(widget_id:, start_pos:, end_pos:) ->
Command(
widget_id,
"select_range",
dict.from_list([
#("start_pos", IntVal(start_pos)),
#("end_pos", IntVal(end_pos)),
]),
)
command.ScrollTo(widget_id:, x:, y:) ->
Command(
widget_id,
"scroll_to",
dict.from_list([#("x", FloatVal(x)), #("y", FloatVal(y))]),
)
command.SnapTo(widget_id:, x:, y:) ->
Command(
widget_id,
"snap_to",
dict.from_list([#("x", FloatVal(x)), #("y", FloatVal(y))]),
)
command.SnapToEnd(widget_id:) ->
Command(widget_id, "snap_to_end", dict.new())
command.ScrollBy(widget_id:, x:, y:) ->
Command(
widget_id,
"scroll_by",
dict.from_list([#("x", FloatVal(x)), #("y", FloatVal(y))]),
)
// -- Global ops (no target ID, stay as widget_op) --
command.Announce(text:, politeness:) ->
WidgetOp("announce", [
#("text", StringVal(text)),
#("politeness", StringVal(politeness_to_wire(politeness))),
])
command.TreeHashQuery(tag:) ->
WidgetOp("tree_hash", [#("tag", StringVal(tag))])
command.FindFocused(tag:) ->
WidgetOp("find_focused", [#("tag", StringVal(tag))])
command.LoadFont(family:, data:) -> LoadFontMessage(family:, data:)
// -- Pane grid commands (widget-targeted) --
command.PaneSplit(pane_grid_id:, pane_id:, axis:, new_pane_id:) ->
Command(
pane_grid_id,
"pane_split",
dict.from_list([
#("pane", StringVal(pane_id)),
#("axis", StringVal(axis)),
#("new_pane_id", StringVal(new_pane_id)),
]),
)
command.PaneClose(pane_grid_id:, pane_id:) ->
Command(
pane_grid_id,
"pane_close",
dict.from_list([#("pane", StringVal(pane_id))]),
)
command.PaneSwap(pane_grid_id:, pane_a:, pane_b:) ->
Command(
pane_grid_id,
"pane_swap",
dict.from_list([#("a", StringVal(pane_a)), #("b", StringVal(pane_b))]),
)
command.PaneMaximize(pane_grid_id:, pane_id:) ->
Command(
pane_grid_id,
"pane_maximize",
dict.from_list([#("pane", StringVal(pane_id))]),
)
command.PaneRestore(pane_grid_id:) ->
Command(pane_grid_id, "pane_restore", dict.new())
// -- Window, system, image (delegated) --
command.Window(window_cmd) -> classify_window(window_cmd)
command.System(system_cmd) -> classify_system(system_cmd)
command.Image(image_cmd) -> classify_image(image_cmd)
// -- Effect, widget command, advance frame --
command.Effect(id:, tag:, kind:, payload:) ->
EffectRequest(id, tag, kind, payload)
command.NativeCommand(node_id:, op:, payload:) ->
case command.is_valid_native_op(op) {
True -> Command(node_id, op, payload)
False -> NoOp
}
command.NativeCommands(commands:) ->
case list.all(commands, fn(cmd) { command.is_valid_native_op(cmd.1) }) {
True -> CommandBatch(commands)
False -> NoOp
}
command.AdvanceFrame(timestamp:) -> AdvanceFrame(timestamp)
}
}
fn classify_window(cmd: command.WindowCommand) -> WireOp(msg) {
case cmd {
command.CloseWindow(window_id:) -> WindowOp("close", window_id, [])
command.ResizeWindow(window_id:, width:, height:) ->
WindowOp("resize", window_id, [
#("width", FloatVal(width)),
#("height", FloatVal(height)),
])
command.MoveWindow(window_id:, x:, y:) ->
WindowOp("move", window_id, [
#("x", FloatVal(x)),
#("y", FloatVal(y)),
])
command.MaximizeWindow(window_id:, maximized:) ->
WindowOp("maximize", window_id, [#("maximized", BoolVal(maximized))])
command.MinimizeWindow(window_id:, minimized:) ->
WindowOp("minimize", window_id, [#("minimized", BoolVal(minimized))])
command.SetWindowMode(window_id:, mode:) ->
WindowOp("set_mode", window_id, [#("mode", StringVal(mode))])
command.ToggleMaximize(window_id:) ->
WindowOp("toggle_maximize", window_id, [])
command.ToggleDecorations(window_id:) ->
WindowOp("toggle_decorations", window_id, [])
command.FocusWindow(window_id:) -> WindowOp("gain_focus", window_id, [])
command.SetWindowLevel(window_id:, level:) ->
WindowOp("set_level", window_id, [#("level", StringVal(level))])
command.DragWindow(window_id:) -> WindowOp("drag", window_id, [])
command.DragResizeWindow(window_id:, direction:) ->
WindowOp("drag_resize", window_id, [
#("direction", StringVal(direction)),
])
command.RequestUserAttention(window_id:, urgency:) -> {
let payload = case urgency {
option.Some(u) -> [#("urgency", StringVal(u))]
option.None -> []
}
WindowOp("request_attention", window_id, payload)
}
command.Screenshot(window_id:, tag:) ->
WindowOp("screenshot", window_id, [#("tag", StringVal(tag))])
command.SetResizable(window_id:, resizable:) ->
WindowOp("set_resizable", window_id, [
#("resizable", BoolVal(resizable)),
])
command.SetMinSize(window_id:, width:, height:) ->
WindowOp("set_min_size", window_id, [
#("width", FloatVal(width)),
#("height", FloatVal(height)),
])
command.SetMaxSize(window_id:, width:, height:) ->
WindowOp("set_max_size", window_id, [
#("width", FloatVal(width)),
#("height", FloatVal(height)),
])
command.EnableMousePassthrough(window_id:) ->
WindowOp("mouse_passthrough", window_id, [#("enabled", BoolVal(True))])
command.DisableMousePassthrough(window_id:) ->
WindowOp("mouse_passthrough", window_id, [#("enabled", BoolVal(False))])
command.ShowSystemMenu(window_id:) ->
WindowOp("show_system_menu", window_id, [])
command.SetResizeIncrements(window_id:, width:, height:) -> {
let payload = case width, height {
option.Some(w), option.Some(h) -> [
#("width", FloatVal(w)),
#("height", FloatVal(h)),
]
option.Some(w), option.None -> [#("width", FloatVal(w))]
option.None, option.Some(h) -> [#("height", FloatVal(h))]
option.None, option.None -> []
}
WindowOp("set_resize_increments", window_id, payload)
}
command.SetIcon(window_id:, rgba_data:, width:, height:) ->
WindowOp("set_icon", window_id, [
#("data", StringVal(bit_array.base64_encode(rgba_data, True))),
#("width", IntVal(width)),
#("height", IntVal(height)),
])
// -- Window queries --
command.GetWindowSize(window_id:, tag:) ->
WindowQuery("get_size", window_id, tag)
command.GetWindowPosition(window_id:, tag:) ->
WindowQuery("get_position", window_id, tag)
command.IsMaximized(window_id:, tag:) ->
WindowQuery("is_maximized", window_id, tag)
command.IsMinimized(window_id:, tag:) ->
WindowQuery("is_minimized", window_id, tag)
command.GetMode(window_id:, tag:) -> WindowQuery("get_mode", window_id, tag)
command.GetScaleFactor(window_id:, tag:) ->
WindowQuery("get_scale_factor", window_id, tag)
command.RawWindowId(window_id:, tag:) ->
WindowQuery("raw_id", window_id, tag)
command.MonitorSize(window_id:, tag:) ->
WindowQuery("monitor_size", window_id, tag)
}
}
fn classify_system(cmd: command.SystemCommand) -> WireOp(msg) {
case cmd {
command.AllowAutomaticTabbing(enabled:) ->
SystemOp("allow_automatic_tabbing", [
#("enabled", BoolVal(enabled)),
])
command.GetSystemTheme(tag:) -> SystemQuery("get_system_theme", tag)
command.GetSystemInfo(tag:) -> SystemQuery("get_system_info", tag)
}
}
fn classify_image(cmd: command.ImageCommand) -> WireOp(msg) {
case cmd {
command.CreateImage(handle:, data:) ->
ImageOp("create_image", [
#("handle", StringVal(handle)),
#("data", BinaryVal(data)),
])
command.CreateImageRgba(handle:, width:, height:, pixels:) ->
ImageOp("create_image", [
#("handle", StringVal(handle)),
#("width", IntVal(width)),
#("height", IntVal(height)),
#("pixels", BinaryVal(pixels)),
])
command.UpdateImage(handle:, data:) ->
ImageOp("update_image", [
#("handle", StringVal(handle)),
#("data", BinaryVal(data)),
])
command.UpdateImageRgba(handle:, width:, height:, pixels:) ->
ImageOp("update_image", [
#("handle", StringVal(handle)),
#("width", IntVal(width)),
#("height", IntVal(height)),
#("pixels", BinaryVal(pixels)),
])
command.DeleteImage(handle:) ->
ImageOp("delete_image", [#("handle", StringVal(handle))])
command.ListImages(tag:) -> ImageOp("list", [#("tag", StringVal(tag))])
command.ClearImages -> ImageOp("clear", [])
}
}
fn politeness_to_wire(politeness: command.Politeness) -> String {
case politeness {
command.Polite -> "polite"
command.Assertive -> "assertive"
}
}