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}
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 withorEquals(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/falseare 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} + 1works on the first run). - The result can be a number,
true/falseor 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
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:
ANDbinds tighter thanOR(normal logic):1 AND 2 OR 3means(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
Ifagainst 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 thisIfin 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
IfandSet variablerows) reopens the config form pre-filled. For anIf, 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:
- Add the inner If (it appears at the bottom), then Move it (grab its
Ifmarker) and slide it up into the outer block's body. As it passes the outerIf/Elsemarker it moves inside, and the indentation deepens to show the new level. - 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
Ifcondition, 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 yourIfbranches 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
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 regexthe 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.