Skip to content

Add while_eq macro: variable-driven loop stoppable from another mapping - #1352

Open
timcharper wants to merge 1 commit into
sezanzeb:mainfrom
timcharper:macro-while-eq
Open

timcharper wants to merge 1 commit into
sezanzeb:mainfrom
timcharper:macro-while-eq

Conversation

@timcharper

Copy link
Copy Markdown

Problem

I wanted an autoclicker toggled on by one key and stopped by a different
key (e.g. Shift+F4 starts clicking, F4 stops it and also releases a
held button from a third mapping). toggle() doesn't fit: it only
starts/stops on a second press of the same trigger, so an unrelated
key has no way to reach it.

The natural workaround is repeat(N, if_eq($var, 1, click, wait)) driven
by a shared set()/if_eq() variable — but this runs into a sharper
problem than just "clicking never stops immediately". RepeatTask.run()
reads its count once and loops a plain for _ in range(repeats), with no
way to exit early:

https://github.com/sezanzeb/input-remapper/blob/2.2.1/inputremapper/injection/macros/tasks/repeat.py#L44-L49

Meanwhile Macro.run() refuses to start a second execution while the
first is still "running":

https://github.com/sezanzeb/input-remapper/blob/2.2.1/inputremapper/injection/macros/macro.py#L83-L87

So even after the shared variable flips to stop the clicking, the macro
instance keeps occupying that mapping's "running" slot until the fixed
repeat count exhausts — which can be minutes depending on how the count
was sized — silently swallowing every re-press of the trigger key in the
meantime. From the user's perspective: works once, then appears to just
stop responding.

Fix

while_eq(variable, value, macro) re-reads the variable every iteration
(like if_eq does) instead of deciding a count up front (like repeat
does), so run() actually returns as soon as the condition no longer
holds:

set(clicking, 1).while_eq(clicking, 1, key(BTN_LEFT).wait(50))
# a different mapping: set(clicking, 0) stops it, and the macro is
# immediately retriggerable — it isn't still "running" a leftover budget

This required a genuinely new task rather than composing existing ones —
none of repeat/hold/toggle/if_tap/if_single support an
externally-driven, unbounded, promptly-exiting loop; hold/toggle's
early-exit mechanism (press_trigger()/release_trigger()) is scoped to
the same trigger that started them, and repeat's bound is fixed at
start with no way to shorten it once running.

Changes

  • inputremapper/injection/macros/tasks/while_eq.py — new task
  • inputremapper/injection/macros/parse.py — registers while_eq in TASK_CLASSES
  • readme/macros.md — docs entry alongside toggle
  • tests/unit/test_macros/test_while_eq.py — covers: no-op when the
    condition starts false, stopping promptly when the variable is changed
    by other code mid-loop, and immediate re-triggerability after stopping

Ran the full tests/unit/test_macros suite locally (134 tests, all
passing) plus the new file on its own.

Happy to adjust naming/placement/docs wording if you'd prefer something
different — while_eq was chosen to mirror if_eq's naming rather than
introduce an unrelated verb (I'd originally called it until, which is
backwards — until conventionally means "loop while false", and this
loops while true).

…her mapping

Motivation: an autoclicker toggled on by one key and off by a different
key. `toggle()` only stops via a second press of the *same* trigger, so
a separate 'clear everything' key can't reach it. The natural workaround,
`repeat(N, if_eq($var, 1, ..., wait(...)))`, has a sharper problem:
`RepeatTask.run()` reads its count once and loops a plain `for _ in
range(repeats)` with no way to exit early, while `Macro.run()` refuses
to start a second execution while the first is still "running" (see the
'Tried to run already running macro' guard). So even after the shared
variable is flipped to stop the clicking, the macro instance keeps
occupying that slot until the fixed count exhausts — anywhere up to the
full repeat budget — silently swallowing every re-press of the trigger
key in the meantime.

`while_eq(variable, value, macro)` closes that gap: it re-reads the
variable every iteration (like `if_eq`) instead of deciding a count up
front (like `repeat`), so `run()` actually returns as soon as the
condition no longer holds. That both lets a different mapping stop the
loop via `set()`/`add()`, and frees the mapping to be retriggered
immediately rather than waiting out a stale budget.

Includes docs (readme/macros.md, alongside toggle/if_eq) and tests
modeled on test_repeat.py/test_set.py covering: no-op when the condition
starts false, stopping promptly when a variable written by other code
changes mid-loop, and immediate re-triggerability after stopping.
@sezanzeb

sezanzeb commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Thanks! Your implementation of while_eq mirrors the deprecated ifeq macro (variable and value arguments), and instead should probably be changed to mirror if_eq (value_1, value_2).

I fixed the lint pipeline yesterday, just merge main into your branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants