Skip to content

Commit 69fee0c

Browse files
committed
Add hold-key / auto-repeat key input
1 parent 40a4733 commit 69fee0c

15 files changed

Lines changed: 293 additions & 1 deletion

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+
按住一个键一段时间,或以固定频率自动重复。完整参考:[`docs/source/Zh/doc/new_features/v119_features_doc.rst`](../docs/source/Zh/doc/new_features/v119_features_doc.rst)
6+
7+
- **`hold_key` / `plan_key_hold`**(`AC_hold_key`):`type_keyboard` 是瞬间按下+放开——先前没有「按住此键 N 秒」(游戏移动、按住滚动)或「每秒送 R 次」(自动重复)。`plan_key_hold` 建立确定性操作计划(按下/等待/放开,或为 `rate_hz` 产生 N 个间隔按键事件);`hold_key` 将等待导向可注入的 `sleep`、按键导向可注入的 `sink`。纯计划、确定。
8+
39
## 本次更新 (2026-06-23) — 等待消失(阻塞式 vanish 等待)
410

511
阻塞直到转圈圈 / toast / 对话框消失。完整参考:[`docs/source/Zh/doc/new_features/v118_features_doc.rst`](../docs/source/Zh/doc/new_features/v118_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+
按住一個鍵一段時間,或以固定頻率自動重複。完整參考:[`docs/source/Zh/doc/new_features/v119_features_doc.rst`](../docs/source/Zh/doc/new_features/v119_features_doc.rst)
6+
7+
- **`hold_key` / `plan_key_hold`**(`AC_hold_key`):`type_keyboard` 是瞬間按下+放開——先前沒有「按住此鍵 N 秒」(遊戲移動、按住捲動)或「每秒送 R 次」(自動重複)。`plan_key_hold` 建立決定性操作計畫(按下/等待/放開,或為 `rate_hz` 產生 N 個間隔按鍵事件);`hold_key` 將等待導向可注入的 `sleep`、按鍵導向可注入的 `sink`。純計畫、具決定性。
8+
39
## 本次更新 (2026-06-23) — 等待消失(阻塞式 vanish 等待)
410

511
阻塞直到轉圈圈 / toast / 對話框消失。完整參考:[`docs/source/Zh/doc/new_features/v118_features_doc.rst`](../docs/source/Zh/doc/new_features/v118_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) — Hold Key / Auto-Repeat
4+
5+
Hold a key for a duration, or auto-repeat it at a fixed rate. Full reference: [`docs/source/Eng/doc/new_features/v119_features_doc.rst`](docs/source/Eng/doc/new_features/v119_features_doc.rst).
6+
7+
- **`hold_key` / `plan_key_hold`** (`AC_hold_key`): `type_keyboard` is an instant down+up — there was no "hold this key for N seconds" (game movement, hold-to-scroll) or "send it at R presses/second" (auto-repeat). `plan_key_hold` builds the deterministic op-plan (press/wait/release, or N spaced key events for `rate_hz`); `hold_key` routes waits to an injectable `sleep` and keys to an injectable `sink`. Pure-planning, deterministic.
8+
39
## What's new (2026-06-23) — Wait Until Gone (Blocking Vanish Waits)
410

511
Block until a spinner / toast / dialog disappears. Full reference: [`docs/source/Eng/doc/new_features/v118_features_doc.rst`](docs/source/Eng/doc/new_features/v118_features_doc.rst).
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
Hold Key / Auto-Repeat
2+
======================
3+
4+
``type_keyboard`` is an instant down+up and ``input_macro.run_sequence`` can
5+
hand-roll a press / wait / release, but there was no primitive for "hold this key
6+
for N seconds" (game movement, hold-to-scroll) or "send it at R presses per
7+
second" (auto-repeat).
8+
9+
:func:`plan_key_hold` builds the deterministic op-plan (pure, unit-testable);
10+
:func:`hold_key` dispatches it through an injectable ``sink`` and ``sleep`` so it
11+
is tested without real input or real waiting. Imports no ``PySide6``.
12+
13+
Headless API
14+
------------
15+
16+
.. code-block:: python
17+
18+
from je_auto_control import hold_key, plan_key_hold
19+
20+
hold_key("key_d", duration_s=1.5) # press, hold 1.5s, release
21+
hold_key("key_down", duration_s=2.0, rate_hz=20) # 40 key events @ 50ms
22+
23+
plan_key_hold("space", 1.0)
24+
# [{'op': 'press', 'key': 'space'},
25+
# {'op': 'wait', 'seconds': 1.0},
26+
# {'op': 'release', 'key': 'space'}]
27+
28+
With ``rate_hz`` unset the key is pressed, held for ``duration_s``, then released.
29+
With ``rate_hz`` set it is sent as ``round(duration_s * rate_hz)`` discrete key
30+
events spaced ``1 / rate_hz`` apart — simulated auto-repeat for movement / scroll
31+
loops. A non-positive duration or rate raises ``ValueError``. ``hold_key`` routes
32+
the ``wait`` steps to ``sleep`` and the key steps to ``sink``, both injectable.
33+
34+
Executor commands
35+
-----------------
36+
37+
``AC_hold_key`` takes ``key`` plus ``duration_s`` and an optional ``rate_hz`` and
38+
returns ``{ops, plan}``. It is exposed as the MCP tool ``ac_hold_key`` and as a
39+
Script Builder command under **Keyboard**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ Comprehensive guides for all AutoControl features.
141141
doc/new_features/v116_features_doc
142142
doc/new_features/v117_features_doc
143143
doc/new_features/v118_features_doc
144+
doc/new_features/v119_features_doc
144145
doc/ocr_backends/ocr_backends_doc
145146
doc/observability/observability_doc
146147
doc/operations_layer/operations_layer_doc
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
按住按鍵 / 自動重複
2+
==================
3+
4+
``type_keyboard`` 是瞬間的按下+放開,``input_macro.run_sequence`` 雖可手動拼出按下 / 等待 / 放開,但先前沒有
5+
「按住此鍵 N 秒」(遊戲移動、按住捲動)或「每秒送 R 次」(自動重複)的基本元件。
6+
7+
:func:`plan_key_hold` 建立決定性的操作計畫(純函式、可單元測試);:func:`hold_key` 透過可注入的 ``sink``
8+
與 ``sleep`` 派發,因此可在無真實輸入、無真實等待下測試。不匯入 ``PySide6``。
9+
10+
無頭 API
11+
--------
12+
13+
.. code-block:: python
14+
15+
from je_auto_control import hold_key, plan_key_hold
16+
17+
hold_key("key_d", duration_s=1.5) # 按下、按住 1.5 秒、放開
18+
hold_key("key_down", duration_s=2.0, rate_hz=20) # 40 個按鍵事件 @ 50ms
19+
20+
plan_key_hold("space", 1.0)
21+
# [{'op': 'press', 'key': 'space'},
22+
# {'op': 'wait', 'seconds': 1.0},
23+
# {'op': 'release', 'key': 'space'}]
24+
25+
未設定 ``rate_hz`` 時,鍵會被按下、按住 ``duration_s``、再放開。設定 ``rate_hz`` 時,會送出
26+
``round(duration_s * rate_hz)`` 個相隔 ``1 / rate_hz`` 的離散按鍵事件——用於移動 / 捲動迴圈的模擬自動重複。
27+
非正數的時長或頻率會拋出 ``ValueError``。``hold_key`` 將 ``wait`` 步驟導向 ``sleep``、按鍵步驟導向 ``sink``,
28+
兩者皆可注入。
29+
30+
執行器命令
31+
----------
32+
33+
``AC_hold_key`` 接受 ``key`` 以及 ``duration_s`` 與選用的 ``rate_hz``,並回傳 ``{ops, plan}``。它以 MCP 工具
34+
``ac_hold_key`` 以及 Script Builder 中 **Keyboard** 分類下的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ AutoControl 所有功能的完整使用指南。
141141
doc/new_features/v116_features_doc
142142
doc/new_features/v117_features_doc
143143
doc/new_features/v118_features_doc
144+
doc/new_features/v119_features_doc
144145
doc/ocr_backends/ocr_backends_doc
145146
doc/observability/observability_doc
146147
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@
257257
)
258258
# Clear-then-type a text field (Playwright `fill` idiom; paste for Unicode)
259259
from je_auto_control.utils.field_entry import plan_field_set, set_field_text
260+
# Hold a key for a duration / auto-repeat at a fixed rate
261+
from je_auto_control.utils.key_hold import hold_key, plan_key_hold
260262
# CI workflow annotations (GitHub Actions)
261263
from je_auto_control.utils.ci_annotations import (
262264
emit_annotations, format_annotation,
@@ -1034,6 +1036,8 @@ def start_autocontrol_gui(*args, **kwargs):
10341036
"path_easings",
10351037
"plan_field_set",
10361038
"set_field_text",
1039+
"plan_key_hold",
1040+
"hold_key",
10371041
"emit_annotations", "format_annotation",
10381042
"ClipboardHistory", "default_clipboard_history",
10391043
"analyze_heal_log", "heal_stats", "scan_secrets",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,17 @@ def _add_keyboard_specs(specs: List[CommandSpec]) -> None:
174174
),
175175
description="Clear the focused field then enter text (paste for Unicode).",
176176
))
177+
specs.append(CommandSpec(
178+
"AC_hold_key", "Keyboard", "Hold Key",
179+
fields=(
180+
FieldSpec("key", FieldType.STRING, placeholder="e.g. key_d, space"),
181+
FieldSpec("duration_s", FieldType.FLOAT, default=1.0,
182+
min_value=0.01),
183+
FieldSpec("rate_hz", FieldType.FLOAT, optional=True,
184+
placeholder="auto-repeat presses/sec (blank = hold)"),
185+
),
186+
description="Hold a key for a duration, or auto-repeat it at rate_hz.",
187+
))
177188
specs.append(CommandSpec(
178189
"AC_hotkey", "Keyboard", "Hotkey",
179190
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3123,6 +3123,14 @@ def _set_field_text(text: str, clear: str = "select_all", paste: Any = False,
31233123
modifier=modifier)
31243124

31253125

3126+
def _hold_key(key: str, duration_s: Any = 1.0,
3127+
rate_hz: Any = None) -> Dict[str, Any]:
3128+
"""Adapter: hold a key for a duration (or auto-repeat at rate_hz)."""
3129+
from je_auto_control.utils.key_hold import hold_key
3130+
rate = float(rate_hz) if rate_hz not in (None, "") else None
3131+
return hold_key(key, float(duration_s), rate_hz=rate)
3132+
3133+
31263134
def _cas_put(name: str, key: str, value: Any,
31273135
expected_version: Any = None) -> Dict[str, Any]:
31283136
"""Adapter: optimistic put into a named versioned store."""
@@ -4821,6 +4829,7 @@ def __init__(self):
48214829
"AC_move_along_path": _move_along_path,
48224830
"AC_drag_path": _drag_path,
48234831
"AC_set_field_text": _set_field_text,
4832+
"AC_hold_key": _hold_key,
48244833
"AC_detect_drift": _detect_drift,
48254834
"AC_categorical_drift": _categorical_drift,
48264835
"AC_diff_rows": _diff_rows,

0 commit comments

Comments
 (0)