8.19.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.19.26.1. Walkthrough

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

8.19.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.19.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.19.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.19.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.19.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.19.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.19.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.19.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.19.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.