Search

Python SDK

Runtime Dashboard in Python: Progress Bars and Tables

Drive a runtime dashboard from a Python macro with mas.ui in MAS: labels, progress bars, charts, logs, tables with buttons, input fields and event handlers.

  • Windows
  • Mac
  • Emulator
  • Cloud device
  • Python SDK
Intermediate Updated 6 min read
On this page
  1. Names bind the runtime dashboard to your Python script
  2. Text and the macro progress bar
  3. Charts
  4. Text areas
  5. Tables
  6. Interactive widgets
  7. batch
  8. Example: a progress bar plus a log
  9. Example: a table with a button per row
  10. Gotchas

mas.ui is the namespace a Macro Automation Studio (MAS) macro written in Python uses to drive the runtime dashboard shown next to a run. You design the dashboard in the UI Builder as a .uibrt file, give each widget a name, and call mas.ui.set_text("status", "Working") from the script. The app receives the update over JSON-RPC and re-renders that widget. This page explains every function, how names map to widgets, the batching rule and the interactive widgets. Designing the dashboard itself is covered on UI Builder; full signatures are on the API reference. If you describe the task to MAS Agent, it writes this code for you.

Names bind the runtime dashboard to your Python script

Every mas.ui function takes the widget name as its first argument. That name is the name property you set in the UI Builder Inspector, saved in the .uibrt file. The dashboard appears in the Code Editor’s bottom panel and in the Dashboard view of a device card on Device Groups.

A name that does not exist in the file updates nothing, silently. Keep names identifier-safe and unique, and check the file first when a widget stays blank.

WidgetKindFunctions
Labelruntime-labelset_text
Progress barruntime-progressset_progress, set_text for its label
Chartruntime-chartadd_data_point, set_chart_data, clear_chart
Text arearuntime-textareaappend_text, set_textarea, clear_text
Tableruntime-tableset_table_data, append_table_row, clear_table, cell_button, set_cell_button_enabled
Buttonruntime-buttonwait_for_event, on_click, set_text
Inputruntime-inputget_input_value, on_change, wait_for_event

Text and the macro progress bar

set_text(name, text) sets the text of a label, a text area, a button or a progress bar’s label. The value is converted with str(), so numbers are fine.

set_progress(name, value) sets a progress bar. The app clamps the value to the widget’s min and max, so a bar defined as 0 to 100 takes a percentage and a bar defined as 0 to 500 takes a count.

python
import mas

mas.ui.set_text("status", "Collecting")
mas.ui.set_progress("main_bar", 40)

Charts

add_data_point(name, value, label, series=None) appends one point to a streaming chart. label is the text on the x-axis and series groups points for multi-series charts. Points beyond the widget’s maxDataPoints are dropped by the app, oldest first. set_chart_data(name, data) replaces the whole dataset with a list of {"label": ..., "value": ...} dictionaries, each with an optional "series". clear_chart(name) empties it.

python
import mas

mas.ui.add_data_point("gold_chart", value=1250, label="run 7", series="gold")
mas.ui.set_chart_data("totals", [
    {"label": "Gold", "value": 1250},
    {"label": "XP", "value": 45000},
])

Text areas

append_text(name, line) adds a line to a text area, which auto-scrolls and drops lines beyond its maxLines. set_textarea(name, text) replaces the content and clear_text(name) empties it.

The app keeps no memory of a text area’s previous content; it expects the full text on every update. The SDK keeps a local mirror so append_text can build on the last value without a round trip. The mirror starts empty with each script process, so a restarted macro starts its log from a blank line even if the panel still shows old ones. Call clear_text at the top of the run to make that explicit.

Tables

set_table_data(name, rows) replaces all rows. Each row is a dictionary whose keys must match the table’s columns as defined in the UI Builder. append_table_row(name, row) adds one row and clear_table(name) removes them all. Tables use the same local mirror as text areas.

A cell can be a button. cell_button(*, id, label, variant="default", disabled=False) builds the cell value. variant is "default", "outline" or "destructive"; anything else raises ValueError. A click on the cell emits a UI event whose widget_id equals the id you gave, so you handle it like a standalone button. set_cell_button_enabled(table_name, button_id, enabled) flips a button’s state in the mirror and re-pushes the table; it does nothing when no cell has that id.

python
import mas

mas.ui.set_table_data("results", [
    {"Name": "Gold", "Value": "1,250", "Status": "OK"},
])
mas.ui.append_table_row("orders", {
    "Name": "Gold",
    "Action": mas.ui.cell_button(id="refund_gold", label="Refund", variant="destructive"),
})

Interactive widgets

There are two ways to receive input: block for it, or register a callback.

wait_for_event(timeout=30.0) blocks until the user clicks a button or changes an input. It returns a UIEvent with widget_id, event_type ("click" or "change") and value, or None when the timeout passes. Use it when the script has nothing else to do, such as a start gate at the top of a run.

python
import mas

mas.ui.set_text("status", "Press Start when the game is on the lobby screen")
event = mas.ui.wait_for_event(timeout=120)
if event is None or event.widget_id != "start_btn":
    mas.log("no start within two minutes, exiting")
    raise SystemExit(0)

on_click(name, handler) and on_change(name, handler) register callbacks. A click handler takes no arguments; a change handler receives the new value. They fire only while the listener runs, so call start_listener() once, after registering every handler. The listener is a daemon thread with its own connection to the app, so it never blocks the main thread’s mas.* calls and it does not keep the process alive after your script returns. Calling it twice is a no-op. An exception inside a handler is printed and does not stop the listener. stop_listener() shuts it down, which is mostly useful in tests.

get_input_value(name) returns the most recent text in an input widget, or None. Use it to read a setting at the moment you need it instead of tracking every change.

batch

batch() is a context manager that collects every update inside the with block and sends them as one message. Use it whenever a loop iteration touches more than one widget; it is the difference between a smooth dashboard and one that flickers. Batches cannot be nested: entering a second one raises RuntimeError. The buffer is process-wide, not per thread, so updates made by a handler while a batch is open are pulled into that batch. Finish the batch before you let handlers fire.

Example: a progress bar plus a log

The dashboard has a label status, a progress bar main_bar (0 to 100) and a text area log.

python
import mas

images = mas.images({"chest": 412})
steps = ["open lobby", "collect chests", "claim daily", "close pop-ups"]

mas.ui.set_text("status", "Starting")
mas.ui.clear_text("log")

for i, step in enumerate(steps, start=1):
    chest = mas.find_object_retry(images.chest, total_tries=3, time_sleep=2.0)
    found = chest is not None
    if found:
        mas.click(chest.x, chest.y)
    with mas.ui.batch():
        mas.ui.set_text("status", f"Step {i} of {len(steps)}: {step}")
        mas.ui.set_progress("main_bar", i * 100 // len(steps))
        mas.ui.append_text("log", f"{step}: {'ok' if found else 'skipped'}")

mas.ui.set_text("status", "Done")

Example: a table with a button per row

The dashboard has a table accounts with the columns Account, Gold and Action, and a label status. Each row gets a button whose id encodes the account. One handler per row reads that id, and the button is disabled once used. The main thread waits until every row is handled or a deadline passes, so the macro still ends if nobody clicks.

python
import time
import mas

accounts = {"alt-1": 1250, "alt-2": 980, "alt-3": 2210}
pending = set(accounts)

def build_rows():
    return [
        {
            "Account": name,
            "Gold": str(gold),
            "Action": mas.ui.cell_button(id=f"collect_{name}", label="Collect"),
        }
        for name, gold in accounts.items()
    ]

def make_handler(name):
    def collect():
        mas.ui.set_text("status", f"collecting for {name}")
        # ... switch to the account and collect ...
        mas.ui.set_cell_button_enabled("accounts", f"collect_{name}", False)
        pending.discard(name)
    return collect

mas.ui.set_table_data("accounts", build_rows())
for name in accounts:
    mas.ui.on_click(f"collect_{name}", make_handler(name))
mas.ui.start_listener()
mas.ui.set_text("status", "Click Collect on a row")

deadline = time.time() + 300
while pending and time.time() < deadline:
    time.sleep(0.5)
mas.ui.set_text("status", "Done" if not pending else "Timed out")

Gotchas

  • A wrong name is the most common bug. The call succeeds and nothing changes.
  • Row keys must match the table’s columns exactly, including case.
  • set_progress is clamped to the widget’s range, so a value of 150 on a 0 to 100 bar shows as full.
  • The local mirrors for text areas and tables reset when the script process restarts.
  • batch() cannot be nested, and it collects updates from every thread while it is open.
  • Register handlers before start_listener(); a handler registered afterwards still works, but clicks before registration are lost.
  • Runtime UI calls need no device, but they need the app to be showing the dashboard for the run. On a cloud device the updates travel with the run’s log stream.

Next steps

Related pages

Was this page helpful?

Questions? Ask in Discord