8.18.1.26. Driving from outside

Every boost widget that previous tutorials wrote registers a path in the telemetry tree (DRIVE_WIN/USER, DRIVE_WIN/SPEED, DRIVE_WIN/RUN_BTN …). That path is also an HTTP endpoint: the boost layer ships [live_command] handlers (imgui_snapshot, imgui_force_set, imgui_click, imgui_open, imgui_close, imgui_focus) that look up the target in the registry. imgui_click fires a real synthetic mouse click at the widget’s center; imgui_force_set / imgui_open / imgui_close write the matching field on the widget’s state struct directly. Either way ImGui sees the effect as if a real input device — or an external editor — drove it.

This tutorial flips the point of view: instead of writing the daslang side, write the driver — a curl / Python / Bash script that issues JSON commands at a running daslang-live app. Every interaction the user could perform via mouse/keyboard has a curl equivalent, and the two surfaces use one in-memory model.

Source: modules/dasImgui/examples/tutorial/driving_outside.das — a small target app exposing five widget kinds. The recording is driven entirely by JSON commands: value writes and container toggles mutate state directly (no mouse), while imgui_click fires a real synthetic click.

8.18.1.26.1. Walkthrough

  1options gen2
  2options _comment_hygiene = true
  3
  4require imgui
  5require imgui_app
  6require opengl/opengl_boost
  7require live/glfw_live
  8require live/live_api
  9require live/live_commands
 10require live/live_vars
 11require live_host
 12require imgui/imgui_live
 13require imgui/imgui_boost_runtime
 14require imgui/imgui_boost_v2
 15require imgui/imgui_widgets_builtin
 16require imgui/imgui_containers_builtin
 17require imgui/imgui_visual_aids
 18
 19let PRESETS : array<string> <- ["Calm", "Mellow", "Active", "Frantic"]
 20var PRESET_ROW : table<int; ClickState>
 21
 22// nolint:STYLE014 — file-header tutorial banner (curl driving recipes) displaced below decls
 23// =============================================================================
 24// TUTORIAL: driving_outside — every boost widget is also a JSON endpoint.
 25//
 26// Previous tutorials wrote the daslang side. This one is the inverse view:
 27// what the JSON command surface looks like as a programming model in its
 28// own right. Every widget the boost layer ships registers a path in the
 29// telemetry tree; that path is also addressable from outside via the
 30// [live_command] HTTP endpoints `imgui_snapshot` / `imgui_force_set` /
 31// `imgui_click` / `imgui_open` / `imgui_close` / `imgui_focus`.
 32//
 33// The target app is small — slider + button + input + popup + combo — so
 34// the driving recipes are easy to spot. Every interaction the user could
 35// perform via mouse/keyboard has a curl equivalent.
 36//
 37// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/driving_outside.das
 38// LIVE:       daslang-live modules/dasImgui/examples/tutorial/driving_outside.das
 39//
 40// DRIVE (curl recipes — pair these with the RST walkthrough):
 41//
 42//   # Snapshot the world (always the first read in any driver)
 43//   curl -X POST -d '{"name":"imgui_snapshot"}' localhost:9090/command
 44//
 45//   # imgui_force_set — drive a slider value
 46//   curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/SPEED","value":7}}' \
 47//        localhost:9090/command
 48//
 49//   # imgui_click — fire a button
 50//   curl -X POST -d '{"name":"imgui_click","args":{"target":"DRIVE_WIN/RUN_BTN"}}' \
 51//        localhost:9090/command
 52//
 53//   # imgui_force_set — string into a text input
 54//   curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/USER","value":"Alice"}}' \
 55//        localhost:9090/command
 56//
 57//   # imgui_force_set — combo by selected-index
 58//   curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/PRESET","value":2}}' \
 59//        localhost:9090/command
 60//
 61//   # imgui_open / imgui_close — popups, closable windows, tabs
 62//   curl -X POST -d '{"name":"imgui_open","args":{"target":"DRIVE_WIN/STATUS_POPUP"}}' \
 63//        localhost:9090/command
 64//   curl -X POST -d '{"name":"imgui_close","args":{"target":"DRIVE_WIN/STATUS_POPUP"}}' \
 65//        localhost:9090/command
 66// =============================================================================
 67
 68[export]
 69def init() {
 70    live_create_window("dasImgui driving_outside tutorial", 880, 620)
 71    live_imgui_init(live_window)
 72    // Deterministic FirstUseEver layout for the recording: disable imgui.ini so a prior
 73    // session's window pos/size can't drift the framing. Tutorial-scoped on purpose.
 74    DisableIniPersistence()
 75    let io & = unsafe(GetIO())
 76    GetStyle().FontScaleMain = 1.5
 77}
 78
 79[export]
 80def update() {
 81    if (!live_begin_frame()) return
 82    begin_frame()
 83
 84    ImGui_ImplGlfw_NewFrame()
 85    apply_synth_io_override()
 86    NewFrame()
 87
 88    SetNextWindowPos(ImVec2(30.0f, 30.0f), ImGuiCond.FirstUseEver)
 89    SetNextWindowSize(ImVec2(640.0f, 460.0f), ImGuiCond.FirstUseEver)
 90    window(DRIVE_WIN, (text = "driving_outside", closable = false,
 91                       flags = ImGuiWindowFlags.None)) {
 92
 93        // ---- A text input — driven by `imgui_force_set` with a string value ----
 94        input_text(USER, (text = "User"))
 95        text("USER.value = \"{USER.value}\"")
 96
 97        separator(DR_SEP_1)
 98
 99        // ---- A slider — driven by `imgui_force_set` with a number value ----
100        slider_int(SPEED, (text = "Speed"))
101        text("SPEED.value = {SPEED.value}")
102
103        separator(DR_SEP_2)
104
105        // ---- A combo — driven by `imgui_force_set` with the selected index ----
106        var preset_label = "(none)"
107        if (PRESET.value >= 0 && PRESET.value < length(PRESETS)) {
108            preset_label = PRESETS[PRESET.value]
109        }
110        combo_select(PRESET, (text = "Preset",
111                              preview_value = preset_label,
112                              flags = ImGuiComboFlags.None)) {
113            for (i in range(length(PRESETS))) {
114                let is_sel = (i == PRESET.value)
115                if (selectable_label(PRESET_ROW[i], PRESETS[i], is_sel)) {
116                    PRESET.value = i
117                }
118            }
119        }
120        text("PRESET = {preset_label} (idx {PRESET.value})")
121
122        separator(DR_SEP_3)
123
124        // ---- A button — fired by `imgui_click` ----
125        if (button(RUN_BTN, (text = "Run"))) {}
126        text("RUN_BTN.click_count = {RUN_BTN.click_count}")
127
128        separator(DR_SEP_4)
129
130        // ---- A popup — opened/closed via `imgui_open` / `imgui_close` ----
131        text("STATUS_POPUP - driven by imgui_open / imgui_close.")
132        popup(STATUS_POPUP, (text = "StatusPopup",
133                             flags = ImGuiWindowFlags.None)) {
134            text("Driven from outside via imgui_open.")
135            separator(DR_SEP_5)
136            text("RUN_BTN.click_count = {RUN_BTN.click_count}")
137        }
138    }
139
140    end_of_frame()
141    Render()
142    var w, h : int
143    live_get_framebuffer_size(w, h)
144    glViewport(0, 0, w, h)
145    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
146    glClear(GL_COLOR_BUFFER_BIT)
147    live_imgui_render()
148
149    live_end_frame()
150}
151
152[export]
153def shutdown() {
154    live_imgui_shutdown()
155    live_destroy_window()
156}
157
158[export]
159def main() {
160    init()
161    while (!exit_requested()) {
162        update()
163    }
164    shutdown()
165}

8.18.1.26.1.1. The command surface

Two kinds of command — faithful input (does what a user does) and bypass (does what a user can’t):

  • Raw synth IO (faithful): imgui_mouse_pos, imgui_mouse_button, imgui_mouse_play, imgui_key_press, imgui_key_type. The driver pretends to be a mouse or keyboard, feeding events into the ImGui input queue. Used by imgui_playwright for cursor-visible recordings.

  • Click a widget by name (faithful): imgui_click, imgui_focus. imgui_click resolves the widget by path (or hex_id), warps to its center, and presses/releases through ImGui’s own input path — a real click, so the widget behaves exactly as a user click would (it errors if the target isn’t rendered this frame). imgui_focus forces keyboard focus. No trajectory to script, but the widget must be on screen.

  • Write a value directly (bypass): imgui_force_set. The framework looks up the widget and queues state.has_pending = true + state.pending_value = ...; the render function submits it next frame. Does what a click can’t — an exact value, an off-screen or inactive widget.

Plus the read side and the container channel:

  • imgui_snapshot — full registry as JSON, the first call in any driver.

  • imgui_open / imgui_close — set state.pending_open / state.pending_close on container widgets (popups, closable windows, tabs, tree nodes).

Prefer imgui_click for clicks and imgui_force_set for values — the first is a faithful click, the second a deterministic value write. Drop to raw synth IO only when there’s no higher-level counterpart (drag along a custom trajectory, paste a long string into a focused input, sustain a chord, …).

8.18.1.26.1.2. imgui_snapshot — read the world

The first call every driver makes:

curl -X POST -d '{"name":"imgui_snapshot"}' localhost:9090/command

Response shape:

{
  "frame": 412,
  "globals": {
    "DRIVE_WIN": {
      "kind": "window",
      "bbox": [30, 30, 670, 490],
      "hex_id": "0x2c1a8f4b",
      "payload": { "open": true, "size": [640, 460], ... }
    },
    "DRIVE_WIN/SPEED": {
      "kind": "slider_int",
      "bbox": [...],
      "hex_id": "0x...",
      "payload": { "value": 5, "bounds": [0, 10], ... }
    },
    "DRIVE_WIN/RUN_BTN": {
      "kind": "button",
      "bbox": [...],
      "payload": { "click_count": 0 }
    },
    ...
  },
  "io": {
    "mouse_pos": [320, 180],
    "active_widget": "..."
  }
}

Use it to:

  • discover what’s on screen and what kind each widget is;

  • read bbox for L1 mouse synthesis (when needed);

  • check payload for current state (test assertions);

  • read hex_id for fallback dispatch when the path isn’t stable.

8.18.1.26.1.3. imgui_force_set — value writes

imgui_force_set is the universal value-write endpoint — slider, checkbox, combo, color, text input, dock-window position. Type-dispatched on the value’s JSON shape:

# string into a text input
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/USER","value":"Alice"}}' \
     localhost:9090/command

# int into a slider
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/SPEED","value":7}}' \
     localhost:9090/command

# int into a combo (selected index)
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/PRESET","value":2}}' \
     localhost:9090/command

# array-of-floats into a color picker
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/TINT","value":[0.2,0.7,0.4]}}' \
     localhost:9090/command

Under the hood: the registered dispatcher for the widget’s state struct unpacks the JSON, type-checks it against the state’s value field, flips has_pending = true, stores pending_value. The render function picks it up next frame; ImGui submits the new value through its own UpdateValue path.

8.18.1.26.1.4. imgui_click — fire a click

imgui_click is a real synthetic mouse click: it resolves the target to its on-screen bbox, warps the cursor to the center, and presses then releases the button across one frame — through ImGui’s own input path, so the widget can’t tell it apart from a hardware click:

curl -X POST -d '{"name":"imgui_click","args":{"target":"DRIVE_WIN/RUN_BTN"}}' \
     localhost:9090/command

The button’s render function returns true, click_count increments, and the daslang side sees both the inline if (button(...)) and RUN_BTN.clicked / RUN_BTN.click_count as expected. Pass "button": 1 for a right-click (context menus), 2 for middle. Because it’s a real click, the target must be rendered this frame — clicking an unrendered widget returns an error (use imgui_force_set to drive a widget that isn’t on screen).

8.18.1.26.1.5. imgui_open / imgui_close — containers

Containers expose an open-state channel through state.pending_open and state.pending_close. imgui_open flips pending_open; imgui_close flips pending_close. The next frame’s render function applies the change:

curl -X POST -d '{"name":"imgui_open","args":{"target":"DRIVE_WIN/STATUS_POPUP"}}' \
     localhost:9090/command
curl -X POST -d '{"name":"imgui_close","args":{"target":"DRIVE_WIN/STATUS_POPUP"}}' \
     localhost:9090/command

The same channel handles popups, closable windows, tabs (closable-tab visibility specifically — see tutorial_containers for the tab-item caveat), tree nodes, and collapsing headers.

8.18.1.26.1.6. The flow on a single command

Every command runs the same path:

  1. daslang-live HTTP server receives POST /command.

  2. Routes by name to the registered [live_command] handler.

  3. Handler looks up target in the registry’s path map (or hex_id reverse map).

  4. Either spawns a synthetic-input coroutine (imgui_click — warp + press/release over a frame) or mutates the matching state struct’s pending field (imgui_force_set / imgui_open / imgui_close); returns {"ok": true, ...} on the HTTP response.

  5. Over the next frame(s) the script’s update() runs; ImGui processes the synthetic input, or the render function applies the pending field, and the updated state is observable from the next imgui_snapshot.

So commands settle over the next frame or two by design — there’s no ambiguity about which frame’s state corresponds to a given response. For test harnesses that need to read the result, the canonical pattern is: command, then await_quiescent (waits a frame), then imgui_snapshot.

8.18.1.26.1.7. Standalone vs live

The HTTP server only exists under daslang-live. Standalone daslang.exe runs the same script but the live-command endpoints aren’t bound — drive-from-outside scenarios require the live host.

8.18.1.26.1.8. Driving from outside (recap)

A complete drive sequence for this tutorial’s app:

# Read the world
curl -X POST -d '{"name":"imgui_snapshot"}' localhost:9090/command

# Write each widget kind
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/USER","value":"Alice"}}' \
     localhost:9090/command
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/SPEED","value":7}}' \
     localhost:9090/command
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DRIVE_WIN/PRESET","value":2}}' \
     localhost:9090/command
curl -X POST -d '{"name":"imgui_click","args":{"target":"DRIVE_WIN/RUN_BTN"}}' \
     localhost:9090/command
curl -X POST -d '{"name":"imgui_open","args":{"target":"DRIVE_WIN/STATUS_POPUP"}}' \
     localhost:9090/command

The recording at the top of this page runs this exact sequence — just JSON commands. The value writes and container toggles flow through the state-struct pending channel with no mouse motion; the imgui_click is a real synthetic click at the button’s center.

8.18.1.26.1.9. Next steps

Now that the JSON-driven view is explicit, the visual aids tour walks through every overlay the recordings used: highlight, mouse trail, cursor sprite, narrate, key HUD, focus rect — all [live_command]-wrapped so the same curl pattern reaches them.

See also

Full source: modules/dasImgui/examples/tutorial/driving_outside.das

Richer reference: modules/dasImgui/examples/features/io_synth_text.dasimgui_key_type streams text as synthetic key + char events through the key timeline; the synthetic keyboard layer in action.

Snapshot contract: imgui_boost_runtime.das’s g_serializers per-kind payload definitions.

Previous tutorial: Live reload

Boost macros — the macro layer.