Skip to content

Commit 2ed4f86

Browse files
committed
Add held-modifiers context across an action group
1 parent fcee74e commit 2ed4f86

15 files changed

Lines changed: 282 additions & 2 deletions

File tree

‎README/WHATS_NEW_zh-CN.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 本次更新 — AutoControl
22

3+
## 本次更新 (2026-06-23) — 在动作组中持续按住修饰键
4+
5+
在多个动作之间持续按住 ctrl/shift,即使出错也会放开。完整参考:[`docs/source/Zh/doc/new_features/v123_features_doc.rst`](../docs/source/Zh/doc/new_features/v123_features_doc.rst)。
6+
7+
- **`hold_modifiers` / `plan_with_modifiers`**(`AC_with_modifiers`):`hotkey` 会立即放开按键——先前无法在多个独立动作之间持续按住修饰键(shift 连点范围选取、ctrl 连点多选)并保证放开。`hold_modifiers` 是 context manager,进入时按下、离开时(在 `finally`)以反向放开,因此不会泄漏;`plan_with_modifiers` 为纯计划。可注入 sink、确定。
8+
39
## 本次更新 (2026-06-23) — Unicode 文本输入(emoji / CJK)
410

511
输入 `write` 无法处理的任何 Unicode(emoji / CJK / 重音)。完整参考:[`docs/source/Zh/doc/new_features/v122_features_doc.rst`](../docs/source/Zh/doc/new_features/v122_features_doc.rst)。

‎README/WHATS_NEW_zh-TW.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 本次更新 — AutoControl
22

3+
## 本次更新 (2026-06-23) — 在動作群組中持續按住修飾鍵
4+
5+
在多個動作之間持續按住 ctrl/shift,即使出錯也會放開。完整參考:[`docs/source/Zh/doc/new_features/v123_features_doc.rst`](../docs/source/Zh/doc/new_features/v123_features_doc.rst)。
6+
7+
- **`hold_modifiers` / `plan_with_modifiers`**(`AC_with_modifiers`):`hotkey` 會立即放開按鍵——先前無法在多個獨立動作之間持續按住修飾鍵(shift 連點範圍選取、ctrl 連點多選)並保證放開。`hold_modifiers` 是 context manager,進入時按下、離開時(在 `finally`)以反向放開,因此不會外洩;`plan_with_modifiers` 為純計畫。可注入 sink、具決定性。
8+
39
## 本次更新 (2026-06-23) — Unicode 文字輸入(emoji / CJK)
410

511
輸入 `write` 無法處理的任何 Unicode(emoji / CJK / 重音)。完整參考:[`docs/source/Zh/doc/new_features/v122_features_doc.rst`](../docs/source/Zh/doc/new_features/v122_features_doc.rst)。

‎WHATS_NEW.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# What's New — AutoControl
22

3+
## What's new (2026-06-23) — Held Modifiers Across an Action Group
4+
5+
Hold ctrl/shift down across several actions, released even on error. Full reference: [`docs/source/Eng/doc/new_features/v123_features_doc.rst`](docs/source/Eng/doc/new_features/v123_features_doc.rst).
6+
7+
- **`hold_modifiers` / `plan_with_modifiers`** (`AC_with_modifiers`): `hotkey` releases its keys immediately — there was no way to hold a modifier down across several independent actions (shift-click range select, ctrl-click multi-select) with a guaranteed release. `hold_modifiers` is a context manager that presses on enter and releases in reverse on exit (in a `finally`, so nothing leaks); `plan_with_modifiers` is the pure plan. Injectable sink, deterministic.
8+
39
## What's new (2026-06-23) — Unicode Text Entry (Emoji / CJK)
410

511
Type any Unicode (emoji / CJK / accented) that `write` can't. Full reference: [`docs/source/Eng/doc/new_features/v122_features_doc.rst`](docs/source/Eng/doc/new_features/v122_features_doc.rst).
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
Held Modifiers Across an Action Group
2+
=====================================
3+
4+
``hotkey`` presses a set of keys and releases them immediately — fine for a
5+
one-shot chord, but there was no way to hold ``ctrl`` (or ``shift``) *down across
6+
several independent actions* (range-select with shift-clicks, ctrl-clicks to
7+
multi-select) and be sure the modifiers are released even if one of those actions
8+
raises.
9+
10+
:func:`plan_with_modifiers` wraps an op-step list with press / release steps and
11+
is pure / unit-testable; :func:`hold_modifiers` is a context manager that presses
12+
on enter and releases (in reverse) on exit — including on exception —
13+
dispatching through an injectable ``sink``. Imports no ``PySide6``.
14+
15+
Headless API
16+
------------
17+
18+
.. code-block:: python
19+
20+
from je_auto_control import hold_modifiers, plan_with_modifiers
21+
from je_auto_control import click_mouse
22+
23+
# shift-held range select: every click happens with shift down
24+
with hold_modifiers(["shift"]):
25+
click_mouse("mouse_left", 100, 100)
26+
click_mouse("mouse_left", 100, 300)
27+
# shift is released here — even if a click raised
28+
29+
plan_with_modifiers([{"op": "click"}], ["ctrl", "shift"])
30+
# press ctrl, press shift, click, release shift, release ctrl
31+
32+
Modifiers are pressed in order on entry and released in *reverse* order on exit,
33+
in a ``finally`` block, so a stuck modifier can never leak. ``plan_with_modifiers``
34+
is the pure plan for any op-step list.
35+
36+
Executor commands
37+
-----------------
38+
39+
``AC_with_modifiers`` runs a nested JSON action list while ``modifiers`` (e.g.
40+
``["ctrl"]`` or ``"ctrl+shift"``) are held, releasing them even if an action
41+
fails. It is exposed as the MCP tool ``ac_with_modifiers`` and as a Script
42+
Builder command under **Keyboard**.

‎docs/source/Eng/eng_index.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ Comprehensive guides for all AutoControl features.
145145
doc/new_features/v120_features_doc
146146
doc/new_features/v121_features_doc
147147
doc/new_features/v122_features_doc
148+
doc/new_features/v123_features_doc
148149
doc/ocr_backends/ocr_backends_doc
149150
doc/observability/observability_doc
150151
doc/operations_layer/operations_layer_doc
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
在動作群組中持續按住修飾鍵
2+
==========================
3+
4+
``hotkey`` 按下一組鍵後立即放開——適合一次性的組合鍵,但先前無法在*多個獨立動作之間持續按住* ``ctrl``
5+
(或 ``shift``)(以 shift 連點做範圍選取、以 ctrl 連點做多選),也無法確保即使其中某個動作拋例外時修飾鍵仍會
6+
被放開。
7+
8+
:func:`plan_with_modifiers` 以 press / release 步驟包覆操作步驟清單,為純函式、可單元測試;:func:`hold_modifiers`
9+
是一個 context manager,進入時按下、離開時(含例外情況)以反向順序放開,並透過可注入的 ``sink`` 派發。
10+
不匯入 ``PySide6``。
11+
12+
無頭 API
13+
--------
14+
15+
.. code-block:: python
16+
17+
from je_auto_control import hold_modifiers, plan_with_modifiers
18+
from je_auto_control import click_mouse
19+
20+
# 按住 shift 做範圍選取:每次點擊都在 shift 按下狀態進行
21+
with hold_modifiers(["shift"]):
22+
click_mouse("mouse_left", 100, 100)
23+
click_mouse("mouse_left", 100, 300)
24+
# shift 在此放開——即使某次點擊拋例外也是
25+
26+
plan_with_modifiers([{"op": "click"}], ["ctrl", "shift"])
27+
# 按 ctrl、按 shift、click、放 shift、放 ctrl
28+
29+
修飾鍵進入時依序按下、離開時在 ``finally`` 區塊以*反向*順序放開,因此卡住的修飾鍵絕不會外洩。
30+
``plan_with_modifiers`` 是任意操作步驟清單的純計畫。
31+
32+
執行器命令
33+
----------
34+
35+
``AC_with_modifiers`` 在按住 ``modifiers``(如 ``["ctrl"]`` 或 ``"ctrl+shift"``)時執行巢狀 JSON 動作清單,
36+
即使某動作失敗也會放開修飾鍵。它以 MCP 工具 ``ac_with_modifiers`` 以及 Script Builder 中 **Keyboard** 分類下
37+
的命令提供。

‎docs/source/Zh/zh_index.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ AutoControl 所有功能的完整使用指南。
145145
doc/new_features/v120_features_doc
146146
doc/new_features/v121_features_doc
147147
doc/new_features/v122_features_doc
148+
doc/new_features/v123_features_doc
148149
doc/ocr_backends/ocr_backends_doc
149150
doc/observability/observability_doc
150151
doc/operations_layer/operations_layer_doc

‎je_auto_control/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@
267267
from je_auto_control.utils.text_unicode import (
268268
plan_paste, type_unicode, unicode_code_units,
269269
)
270+
# Hold modifier keys across a group of actions (release-on-error)
271+
from je_auto_control.utils.modifier_state import (
272+
hold_modifiers, plan_with_modifiers,
273+
)
270274
# CI workflow annotations (GitHub Actions)
271275
from je_auto_control.utils.ci_annotations import (
272276
emit_annotations, format_annotation,
@@ -1052,6 +1056,8 @@ def start_autocontrol_gui(*args, **kwargs):
10521056
"type_unicode",
10531057
"plan_paste",
10541058
"unicode_code_units",
1059+
"hold_modifiers",
1060+
"plan_with_modifiers",
10551061
"emit_annotations", "format_annotation",
10561062
"ClipboardHistory", "default_clipboard_history",
10571063
"analyze_heal_log", "heal_stats", "scan_secrets",

‎je_auto_control/gui/script_builder/command_schema.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ def _add_keyboard_specs(specs: List[CommandSpec]) -> None:
194194
),
195195
description="Enter any Unicode text via clipboard paste (write can't).",
196196
))
197+
specs.append(CommandSpec(
198+
"AC_with_modifiers", "Keyboard", "With Modifiers Held",
199+
fields=(
200+
FieldSpec("modifiers", FieldType.STRING, placeholder="ctrl+shift"),
201+
FieldSpec("actions", FieldType.STRING,
202+
placeholder='[["AC_click_mouse", {...}], ...]'),
203+
),
204+
description="Run nested actions while modifiers are held (release-safe).",
205+
))
197206
specs.append(CommandSpec(
198207
"AC_hotkey", "Keyboard", "Hotkey",
199208
fields=(

‎je_auto_control/utils/executor/action_executor.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3161,6 +3161,20 @@ def _type_unicode(text: str, modifier: str = "ctrl") -> Dict[str, Any]:
31613161
return type_unicode(text, modifier=modifier)
31623162

31633163

3164+
def _with_modifiers(modifiers: Any, actions: Any) -> Dict[str, Any]:
3165+
"""Adapter: run nested actions while modifier keys are held down."""
3166+
import json
3167+
from je_auto_control.utils.modifier_state import hold_modifiers
3168+
if isinstance(modifiers, str):
3169+
modifiers = (json.loads(modifiers) if modifiers.strip().startswith("[")
3170+
else [part.strip() for part in modifiers.split("+")])
3171+
if isinstance(actions, str):
3172+
actions = json.loads(actions)
3173+
with hold_modifiers(list(modifiers)):
3174+
record = executor.execute_action(list(actions), raise_on_error=True)
3175+
return {"modifiers": list(modifiers), "record": record}
3176+
3177+
31643178
def _cas_put(name: str, key: str, value: Any,
31653179
expected_version: Any = None) -> Dict[str, Any]:
31663180
"""Adapter: optimistic put into a named versioned store."""
@@ -4862,6 +4876,7 @@ def __init__(self):
48624876
"AC_hold_key": _hold_key,
48634877
"AC_move_mouse_relative": _move_mouse_relative,
48644878
"AC_type_unicode": _type_unicode,
4879+
"AC_with_modifiers": _with_modifiers,
48654880
"AC_detect_drift": _detect_drift,
48664881
"AC_categorical_drift": _categorical_drift,
48674882
"AC_diff_rows": _diff_rows,

0 commit comments

Comments
 (0)