Skip to content

Macros: Variables & Logic — User Guide

This guide covers the Variables & Logic features for macros: variable substitution, setting variables (from many sources), and If / Else / End If branching. Everything here is fully usable with a TV remote (D-pad + center + back).

Availability: Macros (and therefore Variables & Logic) are part of the full version. On the free version the macro runner does not execute and the Logic tab is not used.


1. Concepts at a glance

Concept What it is
Variable A named value, e.g. hdmi_state. You read it in text with {hdmi_state}.
Local variable Lives only during one macro run. Shared with macros it calls. Gone when the run ends.
Global variable Saved permanently (survives reboot), shared by all macros. Can be a fixed value or dynamic.
Dynamic global A global defined with a source (ADB, HTTP, setting, built-in, …). Its value is recomputed on demand — the macro waits for the result, then uses it. No "Set variable" action needed.
Built-in variable A read-only value provided by the system (foreground app, time, Wi-Fi, etc.).
Substitution Replacing {name} placeholders in action text with the variable's value at run time.
If / Else / End If A block of actions that only runs when a condition (or a combination of several conditions with AND / OR / parentheses) is true, with an optional Else branch.
Set variable An action that computes a value and stores it into a local or global variable.

Resolution order when you write {name}: local → global → built-in. The first match wins. If a name matches nothing, the text is left exactly as written ({name}) so you can spot the typo.

Single pass, no recursion: substitution runs once. If a variable's value itself contains {x}, that inner {x} is not expanded again. (This prevents loops.)


2. Using variables in text — {name}

Anywhere a macro action has a text field, you can embed {name} placeholders. They are replaced with the variable's current value right before the action runs.

Fields that support substitution:

  • ADB command text (the command stored in an ADB command action)
  • Action target / URL / title fields
  • Key-event command fields (commandDown, commandUp, commandRepeat)
  • TCP/IP field

Example: an ADB command action containing

input text {clipboard_word}
will send whatever the clipboard_word variable currently holds.

Fast path: if a field contains no {, nothing is processed — there is zero overhead.


3. The "Set variable" action

Open the macro editor → Add → Variables & Logic tab → Set variable…

A form opens. Each row is focusable; pressing center opens a remote-friendly dialog.

Field Meaning
Name Variable name. Only letters, digits and _ are kept (others are stripped).
Scope Local (this run only) or Global (saved, shared everywhere).
Source Where the value comes from (see below).
(source-specific rows) Shown/hidden depending on the chosen source.
Test now Computes the value immediately and shows the result, so you can verify before saving.
Save Stores the action into the macro.

Sources

Source What it does Extra fields
Static value A literal string. Supports {var} substitution. Value
ADB command result Runs a shell command and uses its output. Command, Filter output + Keyword (optional), Extraction regex (optional), Timeout (ms)
System setting Reads Settings.Global/System/Secure. No permission needed to read. Namespace, Key
Built-in variable Snapshots a built-in (see §6) into your variable. Built-in token
Arithmetic Either Incremental (current value (or 0) + increment, great for counters) or Logic Expression (a formula such as ({count} + 1) % 2). Mode, Increment by or Expression
HTTP request result Performs an HTTP request and uses the response body. URL, Method (GET/POST), Headers, Body, Extraction regex (optional), Timeout (ms)

Output filter (ADB only)

Before any regex runs, you can narrow a multi-line command output down to one line:

  • Filter output — None, Contains, Starts with, Ends with or Equals (all case-insensitive).
  • Keyword — the text to look for.

The first line that matches is used (reading stops there). If no line matches, the result is an empty string. With None (or an empty keyword) the whole output is used (up to the last 1000 lines).

Example: dumpsys input_method + filter Contains mInputShown returns just that line.

Regex extraction (ADB & HTTP)

If you provide an Extraction regex, it is applied to the (filtered) command/response text:

  • If the pattern has a capture group, group 1 is used.
  • Otherwise the whole match is used.
  • Only the first match is taken.
  • If the pattern doesn't match, the result is an empty string.
  • If the pattern itself is invalid, the text is used unchanged (a warning is logged).

Example — extract the HDMI power status:

Command: dumpsys hdmi_control
Regex:   power_status:\s*(\w+)

Timeout behaviour (ADB & HTTP)

The request is wrapped in a timeout (default 3000 ms, minimum 50 ms). If it times out or fails (ADB not connected, network error, etc.), the variable becomes an empty string and the macro continues. You can then test it with the is empty operator.

Arithmetic

Incremental mode. Increment by 1 with a global variable named runs makes a persistent counter: each run it reads the current number (or 0 if unset), adds 1, and stores it back. Integral results are stored without a trailing .0 (e.g. 5, not 5.0).

Logic Expression mode. Type a formula that can use {variables}, numbers, operators and functions, for example:

({count} + 1) % 2
{volume.music} * 100 / {volume.max.music}
IF({time.hour} >= 22, 1, 0)
{a} > 5 && {b} < 10
  • Operators: + - * / % ^, comparisons (= == != > >= < <=), &&, ||, !, parentheses.
  • Functions come from the EvalEx library (for example IF, MIN, MAX, ABS, ROUND, FLOOR, CEILING, SQRT) - see its documentation for the full list.
  • Variable values are inserted by type: numbers and true/false are used as-is, other text becomes a quoted string, and an empty value becomes an empty string. If you wrap a token in quotes yourself ('{name}'), it is always treated as text.
  • A variable that references itself and has no value yet counts as 0 (so {n} + 1 works on the first run).
  • The result can be a number, true/false or text. Integral numbers have no .0.
  • Save is refused if the expression is not valid ("Invalid logic expression"). If it still fails at run time (e.g. text used in a calculation), the variable receives the expression text unchanged.

HTTP headers format

One header per line, Key: Value:

Authorization: Bearer abc123
Accept: application/json
For POST, the Body is sent as the request body. URL, headers and body all support {var}.


4. If / Else / End If

Open the macro editor → Add → Variables & Logic tab → If…

A condition has three parts:

[ Left side ]   [ Operator ]   [ Right side ]

Left and Right sides are text fields that support {var} (e.g. {active_package}).

Several conditions: AND / OR / parentheses

An If can hold more than one condition. The numbered chips across the top of the dialog (1 2 3 ...) are the conditions; the selected chip is the one you edit below.

  • Press the + chip to add another condition.
  • Between two chips is an AND / OR toggle: press it to switch. It joins the condition on its left with the one on its right. Evaluation is short-circuit: once the result is known, the remaining conditions are skipped (their variables are not even refreshed).
  • Precedence: AND binds tighter than OR (normal logic): 1 AND 2 OR 3 means (1 AND 2) OR 3.
  • Parentheses appear automatically when you have 3 or more conditions that mix AND and OR. Each ( / ) is focusable; press it to move that bracket to the next valid position. Pairs always stay balanced and properly nested. Use them to override precedence, e.g. 1 AND (2 OR 3).
  • Reorder conditions: long-press a chip, then press the chip you want to swap it with. Press Back to cancel the swap.
  • Delete condition (n) removes the selected condition (shown only when there are 2 or more).

Operators

Operator True when… Notes
equals / does not equal sides are (not) equal honors Ignore case
contains / does not contain left (does not) contain right honors Ignore case
starts with / ends with left starts/ends with right honors Ignore case
matches regex left matches the regex in right invalid regex → false
> >= < <= numeric comparison both sides must be numbers, else false
is empty / is not empty left side is (not) empty right side ignored
  • Ignore case (a toggle row) applies to the string operators.
  • Test condition evaluates the whole If against current live values. The result lists every condition with its resolved left side, operator and right side: green ✔ = true, red ✘ = false, orange = skipped (short-circuit logic did not need it). The last line is the overall result (CONDITION MET / NOT MET). Press the button again while it runs to stop it. Local variables: the test first runs the Set variable actions placed above this If in the macro (including their ADB/HTTP calls) so locals get realistic values; a local that is never set earlier in the macro stays unresolved.

Else branch

The Include Else branch toggle adds an Else. The structure becomes:

If <condition>
    …actions when TRUE…
Else
    …actions when FALSE…
End If

Without Else:

If <condition>
    …actions when TRUE…
End If

When the condition is false, execution jumps to the Else branch (if present) or to just after End If.

How blocks are created and managed

  • Adding an If automatically inserts the matching End If (and an Else if you enabled it). You never add Else / End If manually — they are managed for you.
  • The three marker rows are paired by a hidden block id, so they stay matched even if you reorder actions.
  • To put actions inside a branch, just move normal actions between the markers (see §5).

5. Editing blocks in the macro list

In the macro editor the rows render with indentation and a colored accent so blocks are easy to read:

▶ Launch Netflix
▶ Set local hdmi = setting.global:hdmi_control_enabled
▶ If {hdmi} equals 1                ← accent color
│   ▶ ADB: input keyevent 23        ← indented (inside the block)
│   ▶ Delay 500 ms
▶ Else                              ← accent color
│   ▶ Show panel "Inputs"
▶ End If                            ← accent color
▶ Run macro "Cleanup"

Press center / long-press a row to open its menu:

  • Edit (on If and Set variable rows) reopens the config form pre-filled. For an If, the Include Else branch toggle in that form will add/remove the Else row.
  • Move lets you reposition with the D-pad.
  • Moving a normal action moves just that row — and it can cross block markers, which is how you pull an action into or out of a branch.
  • Moving a block marker (If/Else/End If) moves the whole block as a unit (its markers and everything inside, including nested blocks). The block always stays balanced.
  • Delete on a normal row removes just that row. Delete on a marker asks to confirm and removes the whole block's markers while keeping the inner actions.

Self-repair: if a macro somehow ends up with an orphaned marker (a marker with no partner), it is removed automatically when the macro is opened.

Nesting If blocks

You can put an If inside another If's branch — no need to split it into separate macros. Two ways:

  1. Add the inner If (it appears at the bottom), then Move it (grab its If marker) and slide it up into the outer block's body. As it passes the outer If/Else marker it moves inside, and the indentation deepens to show the new level.
  2. Or build the inner block where it already sits and move the normal actions in/out around it.

Indentation shows the nesting depth, and execution honors it (an inner block runs only when the outer branch runs). There is no fixed depth limit.

▶ If {active_package} equals com.netflix.ninja
│   ▶ If {hdmi} equals 1                ← nested
│   │   ▶ ADB: input keyevent 23
│   ▶ End If
▶ Else
│   ▶ Show panel "Inputs"
▶ End If

6. Built-in variables (reference)

Use these as {token}. Tokens marked (param) take an argument after a colon, e.g. {setting.global:adb_enabled}. All values are strings; boolean-ish values are true/false unless noted.

App / media

Token Value
active_package Foreground app package name
active_activity Foreground activity class name
playback_state Overall state: playing / paused / stopped
playback_state:<pkg> (param) State of a specific app
media_playing_package Package currently playing media (empty if none)
app.installed:<pkg> (param) true / false

Time / date

Token Value
time.hour Hour 0–23
time.minute Minute 0–59
time.hhmm Zero-padded HHMM, e.g. 0930
date.day_of_week 1=Monday … 7=Sunday
date.iso YYYY-MM-DD

System / network

Token Value
screen.state on / off
wifi.ssid Connected Wi-Fi SSID (may be empty if not permitted)
wifi.connected true / false
network.ip First non-loopback IPv4 address
bt.connected true if any Bluetooth device is connected
bt.connected:<address-or-name> (param) true/false for a specific bonded device (match by MAC or name)
volume.music / volume.max.music Current / maximum media volume
volume.system / volume.ring / volume.notification / volume.alarm / volume.voice_call Current volume for that stream (append to volume.max.* for the maximum)
volume.muted Whether media sound is muted (true/false)
brightness Settings.System screen brightness (0–255)
display.refresh_rate Current display refresh rate (e.g. 60)
display.orientation portrait / landscape
device.model Device model
android.sdk Android SDK level
locale Device locale tag (e.g. en-US)
previous_package Previously foreground app package

Settings

Token Value
setting.global:<key> (param) Settings.Global value
setting.system:<key> (param) Settings.System value
setting.secure:<key> (param) Settings.Secure value

tvQuickActions overlays & flags

Token Value
overlay.cursor Mouse cursor overlay: on / off
overlay.night_mode Night mode overlay: on / off
overlay.clock Clock widget overlay: on / off
overlay.screen_off Black-screen (screen-off) overlay: on / off
overlay.menu_open A tvQA menu/panel is open: true / false
overlay.dock_open The apps dock is open: true / false
overlay.dialpad_open The dialpad is open: true / false
overlay.web_widget Any web widget overlay is shown: true / false
overlay.web_widget:<uid> (param) A specific web widget is shown: true / false
lock.active App-lock overlay is showing: true / false
recorder.active Screen recording in progress: true / false
tvqa.mapping_enabled Whether key remapping is enabled (true/false)
tvqa.macros_enabled Whether macros are enabled (true/false)
tvqa.service_running Accessibility service running: true / false
tvqa.ignoring Remapping currently ignored by a per-app rule: true / false
mapping_enabled:<uid> (param) Whether a specific mapping is enabled
macros_enabled:<uid> (param) Whether a specific macro is enabled

ADB / services / system features

Token Value
adb.connected ADB/Shizuku backend usable: true / false
adb.backend adblib / shizuku / root
sleep_timer.active A sleep timer is running: true / false
sleep_timer.remaining Milliseconds until sleep (0 if none)
fps.current Measured FPS (empty if not measuring)
afr.active AFR currently active: true / false
afr.package App that AFR is active for
gamepad.service_running Gamepad service running: true / false
gamepad.mode Gamepad service mode flag (true/false)
app.locked:<pkg> (param) Whether an app is parental-locked

Weather (last cached by the clock/weather widget)

Token Value
weather.temp Current temperature in °C
weather.temp_f Current temperature in °F
weather.condition Current condition (e.g. cloudy)

Weather values reflect what the clock/weather widget last cached — they are read-only and not fetched on demand. The widget must have run at least once (with location permission) to populate them.

System Info Overlay metrics

These come from the System Info Overlay's latest snapshot. They are populated while that overlay is showing, or while background collection is enabled (see below); otherwise they read empty.

Enable without showing the overlay: turn on System Info Overlay settings → "Collect data in background". While enabled, the app keeps gathering these metrics (CPU, RAM, network, now-playing, …) even when the overlay is hidden, so sys.* / media.* variables stay fresh. It survives reboot. It costs extra battery/CPU (the collector samples ~once per second), so it is opt-in.

Token Value
sys.cpu_load CPU load
sys.cpu_clock CPU clock
sys.cpu_temp CPU temperature
sys.cpu_governor CPU governor
sys.ram_used_mb / sys.ram_total_mb RAM used / total (MB)
sys.ram_percent RAM used percent
sys.disk_used_gb / sys.disk_total_gb Internal storage used / total (GB)
sys.download_speed / sys.upload_speed Network speeds
sys.connection_type Connection type (Wi-Fi / Ethernet / Cellular / None)
sys.vpn VPN active (true/false)
sys.process_count Active process count
sys.app_memory / sys.app_storage Foreground app memory / storage usage
media.title / media.artist Now-playing title / artist
media.app / media.app_package Now-playing app name / package
media.position / media.duration Now-playing position / duration (ms)
media.is_live Whether the now-playing stream is live (true/false)

Other

Token Value
hdmi.state:<input id> (param) connected / standby / disconnected
random:<min>-<max> (param) Random integer in the inclusive range, e.g. random:1-6

Note on bt.connected:<…>: matching is case-insensitive and works with either the device's MAC address (e.g. AA:BB:CC:DD:EE:FF) or its display name (e.g. My Headphones).


7. The variable picker (value editor)

Any value field (condition sides, static value, HTTP URL/body, etc.) opens a picker with these choices:

  • Enter text — type a literal value (it can include {tokens} you type yourself).
  • Insert variable — choose from a list showing live current values:
  • Local variables defined earlier in this macro
  • Global variables (with their stored value)
  • All built-ins (with their current value)

Selecting one inserts {token} at the end of the current text. - Choose value (when the field's meaning is known) — pick from predefined values instead of typing. For example, when one side of an If is {playback_state}, the other side offers playing / paused / stopped; for an on/off token it offers on / off; for a true/false token, true / false, and so on. - Choose app (when comparing to a package) — when one side is {active_package} (or another package-valued token), pick the app from a list instead of typing the package name.

Choosing parameters from a list

When you insert a parameterized token, you don't have to know the raw uid/id — you pick from a list and the picker fills in the parameter for you:

Token You pick from…
playback_state:<pkg>, app.installed:<pkg> installed apps
macros_enabled:<uid> your macros
mapping_enabled:<uid> your mappings
overlay.web_widget:<uid> your web widgets
hdmi.state:<id> detected HDMI inputs
bt.connected:<device> bonded Bluetooth devices
setting.*:<key>, random:<min-max> typed manually

This makes it easy to build expressions like {active_package} or {macros_enabled:…} without remembering token names or ids.

In the action list, these tokens are shown with friendly names rather than raw ids — e.g. {macros_enabled:Movie night}, {mapping_enabled:Power button}, {overlay.web_widget:Weather}, {bt.connected:My Headphones}, {playback_state:Netflix} — so a row stays readable. (The stored value is still the uid/id; only the display is friendly.)


8. Global variables manager

Open the Variables & Logic tab → Manage global variables. From here you can:

  • See every global variable with its current value. Dynamic globals also show their source (e.g. · adb: getprop …).
  • Add global variable — opens the same form as "Set variable", with the full set of sources (Static, ADB command, System setting, Built-in, Arithmetic, HTTP request). The scope is fixed to Global. Pick a source and save — the global is created immediately, everywhere.
  • Edit an existing global (reopens the source form) or Delete it.
  • Toggle the Built-in variables inspector to see all built-in tokens with their live values — a handy debugging screen on a TV.

Dynamic globals — define once, no "Set variable" needed

A global created with a dynamic source (anything other than Static) is recomputed from its source on demand. You do not add a Set variable action in each macro — just reference {name} and the engine fetches a fresh value.

How and when it refreshes:

  • When a macro first references a dynamic global in a run (in an If condition, an ADB command, a URL, any substituted field…), the macro waits for its source to produce a value (ADB/HTTP run with their timeout), stores it, and then continues — so your If branches on the live value.
  • It is refreshed once per run and reused for the rest of that run. So an ADB/HTTP source runs at most once per macro execution, and an Arithmetic global increments once per run (a perfect cross-run counter).
  • An explicit Set variable action that writes the same global wins for the rest of that run (the dynamic source won't overwrite it again).

Example — a dynamic global, used directly:

(once, in Manage global variables)
Add global  hdmi  =  System setting  global:hdmi_control_enabled

(in any macro, no Set variable needed)
If {hdmi} equals 1
    ADB: input keyevent 23
End If

Static vs dynamic: a global created with the Static source is just a constant value. A Set variable action with Global scope still works too (imperative: it computes and stores when the macro reaches that line) — and its name appears in pickers as soon as you save it.


9. Local vs global, and nested macros

  • Local variables are scoped to a single top-level macro run. If that macro runs another macro (a "Run macro" action), the called macro shares the same local variables. This lets you write a reusable "subroutine" macro that sets a result the caller reads afterward.
  • Global variables are shared by every macro and persist across reboots.
  • On a name clash, local wins over global at resolution time. The picker shows a scope label so you can tell them apart.

10. Worked examples

A. Branch on the foreground app

If {active_package} equals com.netflix.ninja
    ADB: input keyevent 23
Else
    Show panel "Inputs"
End If

B. Read an ADB value, then branch on it

Set local model = ADB `getprop ro.product.model`   (Regex: (.*) )
If {model} contains SHIELD
    Toast "Running on SHIELD"
Else
    Toast "Other device"
End If

C. Persistent run counter

Set global runs = Arithmetic +1
Toast "This macro ran {runs} times"
runs survives reboot; view/reset it in Manage global variables.

D. HTTP request → variable

Set local weather = HTTP GET https://example.com/api/temp   (Regex: "temp":(\d+) )
If {weather} >= 30
    Toast "It's hot: {weather}°"
End If

E. React to overlay / Bluetooth state

If {overlay.night_mode} equals on
    Toast "Night mode is on"
End If

If {bt.connected:My Headphones} equals true
    ADB: media volume up
End If

F. Several conditions with AND / OR / parentheses

If  1: {active_package} equals com.netflix.ninja
    AND (2: {time.hour} >= 22   OR   3: {overlay.night_mode} equals on)
    Toast "Late-night Netflix"
End If
Chips 1, 2, 3, toggle the joins to AND / OR and move the brackets around chips 2 and 3.

G. Toggle with an expression

Set global tv_mode = Arithmetic (Logic Expression)   ({tv_mode} + 1) % 2
If {tv_mode} equals 1
    Toast "Mode A"
Else
    Toast "Mode B"
End If

H. Pick one line from a long ADB output

Set local ime = ADB `dumpsys input_method`
                Filter output: Contains   Keyword: mInputShown
                Extraction regex: mInputShown=(\w+)
If {ime} equals true
    ...
End If

11. Edge cases & troubleshooting

  • Unknown {token} → left in the text literally and logged. Check spelling / scope.
  • ADB not connected / command timed out → variable is empty; macro continues. Test with is empty.
  • HTTP failed / timed out → empty string; macro continues.
  • Invalid regex - in matches regex the condition is false; in an extraction regex the text is used unchanged. A valid regex that finds nothing gives an empty result.
  • Invalid arithmetic expression → refused on Save; if it fails at run time the variable gets the expression text unchanged.
  • Numeric compare on non-numbers (>, <, …) → condition is false.
  • No value yet for a global → it reads as empty until first written (or set in the manager).
  • Block looks broken → orphaned markers are auto-removed when the macro is opened; deleting a marker keeps the inner actions.
  • Backups — macros with blocks and variable actions export/import correctly; block pairing and order are preserved.

12. Quick reference

Substitution: {name} → local → global → built-in; single pass; unknown left literal.

Set-variable sources: Static · ADB command (+ output filter) · System setting · Built-in · Arithmetic (incremental or expression) · HTTP request.

Operators: equals, not equals, contains, not contains, starts with, ends with, matches regex, >, >=, <, <=, is empty, is not empty.

Combining conditions: AND / OR toggles between conditions; AND binds before OR; optional parentheses (offered for 3+ mixed conditions); short-circuit evaluation.

Regex extraction: group 1 if present, else whole match, first match only; no match → empty; invalid pattern → text unchanged.

Timeouts (ADB/HTTP): failure → empty string, macro continues.

Scopes: Local (per run, shared with nested macros) · Global (persisted, shared, survives reboot).

Dynamic globals: define a global with a source once (in Manage global variables); referencing {name} refreshes it from that source — the macro waits for the result, then continues. Refreshed once per run.