Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions WHATS_NEW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# What's New — AutoControl

## What's new (2026-06-25) — Idle Detection + Keep the Machine Awake

Run only when the user has stepped away, and stop an overnight run from sleeping. Full reference: [`docs/source/Eng/doc/new_features/v204_features_doc.rst`](docs/source/Eng/doc/new_features/v204_features_doc.rst).

- **`idle_seconds` / `is_idle` / `keep_awake` / `keep_awake_on` / `allow_sleep` / `plan_keep_awake`** (`AC_idle_seconds`, `AC_is_idle`, `AC_plan_keep_awake`, `AC_keep_awake_on`, `AC_allow_sleep`): long unattended runs get derailed two ways — the screensaver / power policy sleeps the box mid-run, or the run should hold while a human is actively using the machine. The framework had neither signal. `idle_seconds` / `is_idle` report time since the last keyboard / mouse input (`GetLastInputInfo` on Windows) through an injectable `probe`; `keep_awake` (scoped context manager) and `keep_awake_on` / `allow_sleep` (process-global on/off for JSON flows) stop the system and display sleeping, applied through an injectable `driver` (`SetThreadExecutionState` / `caffeinate` / `systemd-inhibit` by default) and restored on release. `plan_keep_awake` is the pure planner. All logic is unit-testable without touching the OS via the injected probe/driver. Second feature of the ROUND-15 cross-app OS lane. No `PySide6`.

## What's new (2026-06-25) — Open Files / URLs with the Default App

Hand a file to its default app, print it, or open a URL in the browser. Full reference: [`docs/source/Eng/doc/new_features/v203_features_doc.rst`](docs/source/Eng/doc/new_features/v203_features_doc.rst).
Expand Down
59 changes: 59 additions & 0 deletions docs/source/Eng/doc/new_features/v204_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
Idle Detection + Keep the Machine Awake
=======================================

Long unattended automation runs get derailed two ways: the screensaver / power
policy sleeps the box mid-run, or the run should hold while a human is actively
using the machine. The framework had neither signal. ``idle_keepawake`` adds
both, behind injectable seams so all logic is testable without touching the OS.

* :func:`idle_seconds` / :func:`is_idle` — seconds since the last user keyboard /
mouse input (``GetLastInputInfo`` on Windows), through an injectable ``probe``.
* :func:`plan_keep_awake` — pure planner describing which wake flags a request
maps to.
* :func:`keep_awake` — scoped context manager that keeps the machine awake for
the duration of a ``with`` block, restoring the prior state on exit.
* :func:`keep_awake_on` / :func:`allow_sleep` — a process-global on / off pair
for JSON action flows.

All three keep-awake entry points apply the plan through an injectable ``driver``
(``SetThreadExecutionState`` on Windows, ``caffeinate`` on macOS,
``systemd-inhibit`` on Linux by default). Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import (
idle_seconds, is_idle, keep_awake, keep_awake_on, allow_sleep,
)

idle_seconds() # e.g. 3.4 — seconds since last input
is_idle(300) # True once nobody has touched the machine for 5 min

# Scoped: keep awake only while a long step runs
with keep_awake():
run_long_batch()

# Flow-style: on at the start, off at the end
keep_awake_on(display=True, system=True)
try:
run_long_batch()
finally:
allow_sleep()

:func:`is_idle` is the gate for "only run when the user has stepped away";
:func:`keep_awake` / :func:`keep_awake_on` stop the display and system sleeping
so an overnight run is not interrupted. ``display=False`` keeps the system awake
but lets the screen blank (battery-friendly for headless boxes).

Executor commands
-----------------

``AC_idle_seconds`` (→ ``{idle_seconds}``), ``AC_is_idle`` (``threshold`` →
``{idle, idle_seconds}``), ``AC_plan_keep_awake`` (``display`` / ``system`` → the
plan), ``AC_keep_awake_on`` (``display`` / ``system`` → the active plan) and
``AC_allow_sleep`` (→ ``{released}``). They are exposed as the matching ``ac_*``
MCP tools (reads read-only, keep-awake on/off side-effect-only) and as Script
Builder commands under **Shell**. The :func:`keep_awake` context manager is the
Python-API surface for scoped use.
53 changes: 53 additions & 0 deletions docs/source/Zh/doc/new_features/v204_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
閒置偵測 + 保持機器清醒
=======================

長時間無人值守的自動化執行常因兩種情況中斷:螢幕保護 / 電源原則在執行中途讓機器睡眠,或是當有人正在
使用機器時執行應該暫停。框架原本兩種訊號都沒有。``idle_keepawake`` 補上這兩者,並以可注入接縫實作,
所有邏輯都能在不碰作業系統的情況下測試。

* :func:`idle_seconds` / :func:`is_idle` ——距離使用者上次鍵盤 / 滑鼠輸入的秒數(Windows 上用
``GetLastInputInfo``),透過可注入的 ``probe`` 取得。
* :func:`plan_keep_awake` ——純 planner,描述請求對應到哪些清醒旗標。
* :func:`keep_awake` ——具範圍的 context manager,在 ``with`` 區塊期間保持機器清醒,離開時還原先前狀態。
* :func:`keep_awake_on` / :func:`allow_sleep` ——供 JSON 動作流程使用的行程全域開 / 關配對。

三個 keep-awake 入口皆透過可注入的 ``driver`` 套用計畫(預設 Windows 用
``SetThreadExecutionState``、macOS 用 ``caffeinate``、Linux 用 ``systemd-inhibit``)。不匯入
``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import (
idle_seconds, is_idle, keep_awake, keep_awake_on, allow_sleep,
)

idle_seconds() # 例如 3.4 ——距離上次輸入的秒數
is_idle(300) # 沒人碰機器滿 5 分鐘後回傳 True

# 具範圍:只在長步驟執行時保持清醒
with keep_awake():
run_long_batch()

# 流程式:開始時開、結束時關
keep_awake_on(display=True, system=True)
try:
run_long_batch()
finally:
allow_sleep()

:func:`is_idle` 是「只在使用者離開時才執行」的判斷閘;:func:`keep_awake` /
:func:`keep_awake_on` 阻止螢幕與系統睡眠,讓整夜執行不被打斷。``display=False`` 會保持系統清醒但允許
螢幕變黑(對無頭機器較省電)。

執行器指令
----------

``AC_idle_seconds``(→ ``{idle_seconds}``)、``AC_is_idle``(``threshold`` →
``{idle, idle_seconds}``)、``AC_plan_keep_awake``(``display`` / ``system`` → 計畫)、
``AC_keep_awake_on``(``display`` / ``system`` → 生效中的計畫)與 ``AC_allow_sleep``
(→ ``{released}``)。皆以對應的 ``ac_*`` MCP 工具(讀取為唯讀、keep-awake 開 / 關為僅副作用)
及 Script Builder 指令(位於 **Shell** 分類下)形式提供。:func:`keep_awake` context manager
則是具範圍使用的 Python API 介面。
7 changes: 7 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
from je_auto_control.utils.ax_events import wait_for_focus_change
# Open a file with its default app / a URL in the default browser
from je_auto_control.utils.shell_open import open_path, plan_open
# Detect user-idle time and keep the machine awake during unattended runs
from je_auto_control.utils.idle_keepawake import (
allow_sleep, idle_seconds, is_idle, keep_awake, keep_awake_on,
plan_keep_awake,
)
# Rich clipboard formats — RTF + CSV/TSV codecs and Windows get / set
from je_auto_control.utils.clipboard_rich_formats import (
build_rtf, csv_to_rows, get_clipboard_csv, get_clipboard_rtf, rows_to_csv,
Expand Down Expand Up @@ -1703,6 +1708,8 @@ def start_autocontrol_gui(*args, **kwargs):
"get_selection", "list_views", "set_view",
"wait_for_focus_change",
"plan_open", "open_path",
"idle_seconds", "is_idle", "plan_keep_awake",
"keep_awake", "keep_awake_on", "allow_sleep",
"build_rtf", "rtf_to_text", "rows_to_csv", "csv_to_rows",
"set_clipboard_rtf", "get_clipboard_rtf",
"set_clipboard_csv", "get_clipboard_csv",
Expand Down
34 changes: 34 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -4268,6 +4268,40 @@ def _add_work_queue_specs(specs: List[CommandSpec]) -> None:
),
description="Classify how a file/URL would be opened (pure, no launch).",
))
specs.append(CommandSpec(
"AC_idle_seconds", "Shell", "Idle Seconds",
fields=(),
description="Seconds since the last user keyboard / mouse input.",
))
specs.append(CommandSpec(
"AC_is_idle", "Shell", "Is User Idle",
fields=(
FieldSpec("threshold", FieldType.FLOAT, default=300.0,
placeholder="idle seconds threshold"),
),
description="True if the user has been idle for >= threshold seconds.",
))
specs.append(CommandSpec(
"AC_plan_keep_awake", "Shell", "Plan Keep Awake",
fields=(
FieldSpec("display", FieldType.BOOL, optional=True, default=True),
FieldSpec("system", FieldType.BOOL, optional=True, default=True),
),
description="Describe a keep-awake request (pure, no OS call).",
))
specs.append(CommandSpec(
"AC_keep_awake_on", "Shell", "Keep Machine Awake",
fields=(
FieldSpec("display", FieldType.BOOL, optional=True, default=True),
FieldSpec("system", FieldType.BOOL, optional=True, default=True),
),
description="Keep the machine awake until Allow Sleep is run.",
))
specs.append(CommandSpec(
"AC_allow_sleep", "Shell", "Allow Machine to Sleep",
fields=(),
description="Release a previously-started keep-awake.",
))
specs.append(CommandSpec(
"AC_take_golden", "Report", "Capture Golden Image",
fields=(FieldSpec("path", FieldType.FILE_PATH),),
Expand Down
36 changes: 36 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2621,6 +2621,37 @@ def _open_path(target: str, verb: str = "open") -> Dict[str, Any]:
return {"opened": bool(open_path(str(target), verb=str(verb)))}


def _idle_seconds() -> Dict[str, Any]:
"""Adapter: seconds since the last user input."""
from je_auto_control.utils.idle_keepawake import idle_seconds
return {"idle_seconds": float(idle_seconds())}


def _is_idle(threshold: Any) -> Dict[str, Any]:
"""Adapter: whether the user has been idle for >= ``threshold`` seconds."""
from je_auto_control.utils.idle_keepawake import idle_seconds, is_idle
seconds = float(threshold)
return {"idle": bool(is_idle(seconds)), "idle_seconds": idle_seconds()}


def _plan_keep_awake(display: Any = True, system: Any = True) -> Dict[str, Any]:
"""Adapter: describe a keep-awake request (pure, no OS call)."""
from je_auto_control.utils.idle_keepawake import plan_keep_awake
return plan_keep_awake(display=bool(display), system=bool(system))


def _keep_awake_on(display: Any = True, system: Any = True) -> Dict[str, Any]:
"""Adapter: keep the machine awake until ``AC_allow_sleep``."""
from je_auto_control.utils.idle_keepawake import keep_awake_on
return keep_awake_on(display=bool(display), system=bool(system))


def _allow_sleep() -> Dict[str, Any]:
"""Adapter: release a previously-started keep-awake."""
from je_auto_control.utils.idle_keepawake import allow_sleep
return {"released": bool(allow_sleep())}


def _get_control_text(name: Optional[str] = None, role: Optional[str] = None,
app_name: Optional[str] = None,
automation_id: Optional[str] = None) -> Dict[str, Any]:
Expand Down Expand Up @@ -6613,6 +6644,11 @@ def __init__(self):
"AC_wait_for_focus_change": _wait_for_focus_change,
"AC_plan_open": _plan_open,
"AC_open_path": _open_path,
"AC_idle_seconds": _idle_seconds,
"AC_is_idle": _is_idle,
"AC_plan_keep_awake": _plan_keep_awake,
"AC_keep_awake_on": _keep_awake_on,
"AC_allow_sleep": _allow_sleep,
"AC_get_control_text": _get_control_text,
"AC_find_control_text": _find_control_text,
"AC_select_control_text": _select_control_text,
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/idle_keepawake/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Detect user-idle time and keep the machine awake during unattended runs."""
from je_auto_control.utils.idle_keepawake.idle_keepawake import (
allow_sleep, idle_seconds, is_idle, keep_awake, keep_awake_on,
plan_keep_awake,
)

__all__ = [
"idle_seconds", "is_idle", "plan_keep_awake",
"keep_awake", "keep_awake_on", "allow_sleep",
]
Loading
Loading