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
20 changes: 20 additions & 0 deletions WHATS_NEW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# What's New — AutoControl

## What's new (2026-06-26)

### Live IME State for Safe CJK Entry

Wait for the input method to commit before reading a Japanese/Chinese/Korean field. Full reference: [`docs/source/Eng/doc/new_features/v208_features_doc.rst`](docs/source/Eng/doc/new_features/v208_features_doc.rst).

- **`ime_state` / `is_composing` / `wait_for_composition_commit` / `decode_conversion_mode`** (`AC_ime_state`, `AC_is_composing`, `AC_wait_for_composition_commit`, `AC_decode_conversion_mode`): typing into a CJK field is unsafe while an IME is *composing* — the candidate text isn't committed, so reading the field back returns half-entered glyphs and the next keystroke edits the composition. `text_unicode` (`VK_PACKET`) is blind to this. `ime_state` exposes the focused window's live `{open, composing, composition, conversion}` (Windows IMM32, read-only) through an injectable `reader`; `is_composing` is the boolean gate; `wait_for_composition_commit` blocks until the IME commits (injectable `clock`/`sleep`/`reader`); `decode_conversion_mode` is the pure `IME_CMODE_*` bitmask decoder. All decode/wait logic is unit-tested without an IME. Sixth feature of the ROUND-15 cross-app OS lane. No `PySide6`.

### Lock the Workstation + Wait for Unlock

Lock the box at the end of a run, and block until a human unlocks it before resuming. Full reference: [`docs/source/Eng/doc/new_features/v207_features_doc.rst`](docs/source/Eng/doc/new_features/v207_features_doc.rst).

- **`lock_session` / `plan_lock_session` / `wait_for_unlock` / `wait_for_lock` / `classify_lock_transitions`** (`AC_lock_session`, `AC_plan_lock_session`, `AC_wait_for_unlock`, `AC_classify_lock_transitions`): `session_guard` could *detect* a locked session and raise; this adds *acting* on it. `lock_session` locks the workstation now (`LockWorkStation` on Windows, `loginctl lock-session` / `CGSession -suspend` elsewhere) through an injectable `driver`; `wait_for_unlock` / `wait_for_lock` poll `session_guard.is_session_locked` (reusing its real Windows `OpenInputDesktop` probe) until the state flips or a timeout, with injectable `clock` / `sleep` / `probe`; `plan_lock_session` is the pure per-OS planner and `classify_lock_transitions` reduces a lock-state sample stream to `{event, locked}` lock/unlock events. `wait_for_unlock` is the blocking companion to `ensure_interactive_session`. Fifth feature of the ROUND-15 cross-app OS lane. No `PySide6`.

### Read and Control the System Volume

Set a known audio baseline before a run — mute, set 30%, or assert the level. Full reference: [`docs/source/Eng/doc/new_features/v206_features_doc.rst`](docs/source/Eng/doc/new_features/v206_features_doc.rst).

- **`get_volume` / `set_volume` / `change_volume` / `is_muted` / `set_mute` / `mute` / `unmute` / `toggle_mute`** (`AC_get_volume`, `AC_set_volume`, `AC_change_volume`, `AC_set_mute`, `AC_toggle_mute`): the framework only had the blind media-key steps (`volume up` / `down` nudge by an unknown amount with no read-back). This adds absolute, read-backable control of the default output device — read or set the master level as an integer percent `0..100`, and read / set / toggle the mute flag. All logic (clamping, percent↔scalar conversion, toggle) is pure and runs through an injectable `VolumeDriver` seam, so it is fully unit-tested without an audio device; the default driver uses the Windows Core Audio `IAudioEndpointVolume` interface through the optional `pycaw` dependency (`pip install je_auto_control[audio]`), degrading with a clear error when absent. Fourth feature of the ROUND-15 cross-app OS lane. No `PySide6`.

## What's new (2026-06-25)

### Resolve the App Registered for a File Type
Expand Down
67 changes: 67 additions & 0 deletions docs/source/Eng/doc/new_features/v206_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Read and Control the System Volume
==================================

Unattended runs often need a known audio baseline — mute before a noisy batch,
restore a level afterwards, or assert the current volume — but the framework
only had the blind media-key steps (``volume up`` / ``down`` nudge by an unknown
amount with no read-back). ``system_volume`` adds absolute, read-backable
control of the default output device.

* :func:`get_volume` / :func:`set_volume` / :func:`change_volume` — read and
write the master level as an integer percent ``0..100`` (``set_volume`` and
``change_volume`` clamp to that range).
* :func:`is_muted` / :func:`set_mute` / :func:`mute` / :func:`unmute` /
:func:`toggle_mute` — read and write the mute flag.

All logic (clamping, percent <-> scalar conversion, toggle) is pure and runs
through an injectable :class:`VolumeDriver` seam, so it is fully testable without
an audio device. The default driver drives the Windows Core Audio
``IAudioEndpointVolume`` interface through the optional ``pycaw`` dependency
(``pip install je_auto_control[audio]``); on a platform / install without it the
default driver raises a clear error telling the caller to pass ``driver=``.
Imports no ``PySide6``.

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

.. code-block:: python

from je_auto_control import (
get_volume, set_volume, change_volume, is_muted, mute, unmute,
toggle_mute,
)

get_volume() # e.g. 65 — current master volume percent
set_volume(30) # set to 30 %, returns 30
change_volume(-10) # lower by 10 %, returns the applied percent
is_muted() # False
mute() # True — silence the output
unmute() # False — restore it
toggle_mute() # flip and return the new state

For tests (or any non-Windows host) pass a ``driver`` — any object exposing
``get_scalar`` / ``set_scalar`` / ``get_mute`` / ``set_mute`` over a ``0.0..1.0``
scalar:

.. code-block:: python

class FakeVolume:
def __init__(self, scalar=0.5, muted=False):
self.scalar, self.muted = scalar, muted
def get_scalar(self): return self.scalar
def set_scalar(self, s): self.scalar = s
def get_mute(self): return self.muted
def set_mute(self, m): self.muted = m

drv = FakeVolume()
set_volume(73, driver=drv) # 73, drv.scalar == 0.73

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

``AC_get_volume`` (→ ``{volume, muted}``), ``AC_set_volume`` (``level`` →
``{volume}``), ``AC_change_volume`` (``delta`` → ``{volume}``), ``AC_set_mute``
(``muted`` → ``{muted}``) and ``AC_toggle_mute`` (→ ``{muted}``). They are
exposed as the matching ``ac_*`` MCP tools (the read is read-only, the writes
side-effect-only) and as Script Builder commands under **Shell**. The executor
and MCP layers use the default OS driver, so they require ``pycaw`` on Windows.
62 changes: 62 additions & 0 deletions docs/source/Eng/doc/new_features/v207_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Lock the Workstation + Wait for Unlock
======================================

:mod:`session_guard` answers "is the session locked right now?" and raises if it
is. The missing half is *acting* on the lock state: lock the machine at the end
of an unattended run, block until a human unlocks it before resuming, or reduce
a stream of lock-state samples to lock / unlock events. ``lock_session`` adds
that, behind injectable seams so the logic is testable without touching the OS.

* :func:`lock_session` — lock the workstation now (``LockWorkStation`` on
Windows, ``loginctl lock-session`` on Linux, ``CGSession -suspend`` on macOS)
through an injectable ``driver``.
* :func:`plan_lock_session` — pure planner: how the lock would be performed on
this OS and whether a default is available (``{backend, argv, available}``).
* :func:`wait_for_unlock` / :func:`wait_for_lock` — poll
:func:`is_session_locked` until the state flips or a timeout, with injectable
``clock`` / ``sleep`` / ``probe`` for deterministic tests.
* :func:`classify_lock_transitions` — pure: a list of lock-state samples to a
list of ``{event, locked}`` lock / unlock transitions.

The lock probe reused by the wait helpers is :mod:`session_guard`'s — the
Windows ``OpenInputDesktop`` check — so ``wait_for_unlock`` is the blocking
companion to ``ensure_interactive_session`` (which only raises). Imports no
``PySide6``.

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

.. code-block:: python

from je_auto_control import (
lock_session, wait_for_unlock, classify_lock_transitions,
)

# ... unattended run finishes ...
lock_session() # secure the machine

# Resume only once a human has unlocked the box
if wait_for_unlock(timeout_s=600):
run_next_stage()

# Reduce a sampled lock-state log to events
classify_lock_transitions([False, True, True, False])
# -> [{'event': 'lock', 'locked': True},
# {'event': 'unlock', 'locked': False}]

For tests (or any host) pass a ``driver`` / ``probe``:

.. code-block:: python

locked = lock_session(driver=lambda: True) # no real lock
wait_for_unlock(probe=lambda: False) # already unlocked

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

``AC_lock_session`` (→ ``{locked}``), ``AC_plan_lock_session`` (→ the plan),
``AC_wait_for_unlock`` (``timeout`` / ``interval`` → ``{unlocked}``) and
``AC_classify_lock_transitions`` (``states`` JSON list → ``{events}``). They are
exposed as the matching ``ac_*`` MCP tools (``ac_lock_session`` is destructive —
it interrupts the session; the rest are read-only) and as Script Builder
commands under **Shell**.
57 changes: 57 additions & 0 deletions docs/source/Eng/doc/new_features/v208_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Live IME State for Safe CJK Entry
=================================

Typing into a CJK / Japanese / Korean field is unsafe while an IME (input method
editor) is *composing*: the candidate text has not been committed yet, so
reading the field back returns half-entered glyphs and the next keystroke edits
the composition instead of the field. ``text_unicode`` (``VK_PACKET``) is blind
to this. ``ime_state`` exposes the live composition and conversion state so a
flow can wait for the IME to commit before it reads or acts.

* :func:`ime_state` — ``{open, composing, composition, conversion,
conversion_flags}`` for the focused window's IME, through an injectable
``reader``.
* :func:`is_composing` — ``True`` while the IME has an uncommitted composition.
* :func:`wait_for_composition_commit` — block until composition ends (or a
timeout), with injectable ``clock`` / ``sleep`` / ``reader``.
* :func:`decode_conversion_mode` — pure: the IMM32 ``IME_CMODE_*`` conversion
bitmask to ``{native, katakana, full_shape, roman, char_code}``.

The default ``reader`` queries Windows IMM32 (``ImmGetContext`` /
``ImmGetOpenStatus`` / ``ImmGetConversionStatus`` / ``ImmGetCompositionStringW``)
read-only; all decoding / waiting logic runs through the injectable seam, so it
is fully testable without an IME. Imports no ``PySide6``.

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

.. code-block:: python

from je_auto_control import (
ime_state, is_composing, wait_for_composition_commit,
)

# Before reading a CJK field, make sure the IME has committed
if wait_for_composition_commit(timeout_s=3):
value = read_field()

is_composing() # True while candidate text is still on screen
ime_state() # {'open': True, 'composing': True, 'composition': 'あ', ...}

For tests (or any non-Windows host) pass a ``reader`` — a
``() -> {open, conversion, composition}``:

.. code-block:: python

busy = lambda: {"open": True, "conversion": 0, "composition": "あ"}
is_composing(reader=busy) # True
ime_state(reader=busy)["composition"] # 'あ'

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

``AC_ime_state`` (→ the full state), ``AC_is_composing`` (→ ``{composing}``),
``AC_wait_for_composition_commit`` (``timeout`` / ``interval`` →
``{committed}``) and ``AC_decode_conversion_mode`` (``flags`` → the decoded
modes). They are exposed as the matching read-only ``ac_*`` MCP tools and as
Script Builder commands under **Shell**.
60 changes: 60 additions & 0 deletions docs/source/Zh/doc/new_features/v206_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
讀取與控制系統音量
==================

無人值守的執行常需要一個已知的音訊基準——在吵雜的批次前靜音、結束後還原音量,或斷言目前音量——但框架
原本只有盲目的媒體鍵步驟(``volume up`` / ``down`` 以未知幅度推移,且無法讀回)。``system_volume``
補上對預設輸出裝置的絕對、可讀回控制。

* :func:`get_volume` / :func:`set_volume` / :func:`change_volume` ——以整數百分比 ``0..100``
讀寫主音量(``set_volume`` 與 ``change_volume`` 會夾到該範圍)。
* :func:`is_muted` / :func:`set_mute` / :func:`mute` / :func:`unmute` /
:func:`toggle_mute` ——讀寫靜音旗標。

所有邏輯(夾值、百分比 <-> 純量轉換、切換)皆為純函式,並透過可注入的 :class:`VolumeDriver` 接縫執行,
故能在不需音訊裝置的情況下完整測試。預設 driver 透過選用相依套件 ``pycaw``
(``pip install je_auto_control[audio]``)驅動 Windows Core Audio 的
``IAudioEndpointVolume`` 介面;在沒有該套件 / 非 Windows 平台上,預設 driver 會丟出清楚的錯誤,
提示呼叫端傳入 ``driver=``。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import (
get_volume, set_volume, change_volume, is_muted, mute, unmute,
toggle_mute,
)

get_volume() # 例如 65 ——目前主音量百分比
set_volume(30) # 設為 30 %,回傳 30
change_volume(-10) # 降低 10 %,回傳套用後的百分比
is_muted() # False
mute() # True ——使輸出靜音
unmute() # False ——還原
toggle_mute() # 切換並回傳新狀態

測試時(或任何非 Windows 主機)可傳入 ``driver`` ——任何以 ``0.0..1.0`` 純量提供
``get_scalar`` / ``set_scalar`` / ``get_mute`` / ``set_mute`` 的物件:

.. code-block:: python

class FakeVolume:
def __init__(self, scalar=0.5, muted=False):
self.scalar, self.muted = scalar, muted
def get_scalar(self): return self.scalar
def set_scalar(self, s): self.scalar = s
def get_mute(self): return self.muted
def set_mute(self, m): self.muted = m

drv = FakeVolume()
set_volume(73, driver=drv) # 73,drv.scalar == 0.73

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

``AC_get_volume``(→ ``{volume, muted}``)、``AC_set_volume``(``level`` →
``{volume}``)、``AC_change_volume``(``delta`` → ``{volume}``)、``AC_set_mute``
(``muted`` → ``{muted}``)與 ``AC_toggle_mute``(→ ``{muted}``)。皆以對應的 ``ac_*``
MCP 工具(讀取為唯讀、寫入為僅副作用)及 Script Builder 指令(位於 **Shell** 分類下)形式提供。
執行器與 MCP 層使用預設 OS driver,故在 Windows 上需要 ``pycaw``。
55 changes: 55 additions & 0 deletions docs/source/Zh/doc/new_features/v207_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
鎖定工作站 + 等待解鎖
====================

:mod:`session_guard` 回答「目前 session 是否鎖定?」並在鎖定時丟出例外。缺少的另一半是對鎖定狀態
*採取行動*:在無人值守執行結束時鎖定機器、在恢復前阻塞直到有人解鎖,或把一連串鎖定狀態取樣化約為
鎖定 / 解鎖事件。``lock_session`` 補上這些,並以可注入接縫實作,故邏輯能在不碰作業系統的情況下測試。

* :func:`lock_session` ——立即鎖定工作站(Windows 用 ``LockWorkStation``、Linux 用
``loginctl lock-session``、macOS 用 ``CGSession -suspend``),透過可注入的 ``driver``。
* :func:`plan_lock_session` ——純 planner:此 OS 上會如何執行鎖定,以及是否有預設可用
(``{backend, argv, available}``)。
* :func:`wait_for_unlock` / :func:`wait_for_lock` ——輪詢 :func:`is_session_locked`
直到狀態翻轉或逾時,``clock`` / ``sleep`` / ``probe`` 皆可注入以利確定性測試。
* :func:`classify_lock_transitions` ——純函式:把一連串鎖定狀態取樣化約為
``{event, locked}`` 鎖定 / 解鎖轉變的清單。

wait 系列重用的鎖定 probe 即 :mod:`session_guard` 的——Windows 的 ``OpenInputDesktop`` 檢查——
故 ``wait_for_unlock`` 是 ``ensure_interactive_session``(只會丟例外)的阻塞式搭檔。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import (
lock_session, wait_for_unlock, classify_lock_transitions,
)

# ... 無人值守執行結束 ...
lock_session() # 鎖住機器

# 等有人解鎖後才繼續
if wait_for_unlock(timeout_s=600):
run_next_stage()

# 把取樣的鎖定狀態紀錄化約為事件
classify_lock_transitions([False, True, True, False])
# -> [{'event': 'lock', 'locked': True},
# {'event': 'unlock', 'locked': False}]

測試時(或任何主機)可傳入 ``driver`` / ``probe``:

.. code-block:: python

locked = lock_session(driver=lambda: True) # 不真正鎖定
wait_for_unlock(probe=lambda: False) # 已解鎖

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

``AC_lock_session``(→ ``{locked}``)、``AC_plan_lock_session``(→ 計畫)、
``AC_wait_for_unlock``(``timeout`` / ``interval`` → ``{unlocked}``)與
``AC_classify_lock_transitions``(``states`` JSON 清單 → ``{events}``)。皆以對應的 ``ac_*``
MCP 工具(``ac_lock_session`` 為破壞性——會中斷 session;其餘為唯讀)及 Script Builder 指令
(位於 **Shell** 分類下)形式提供。
51 changes: 51 additions & 0 deletions docs/source/Zh/doc/new_features/v208_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
即時 IME 狀態以利安全的 CJK 輸入
================================

在 IME(輸入法)*組字中*對 CJK / 日文 / 韓文欄位輸入並不安全:候選字尚未送出,故讀回欄位會得到
半成形的字,而下一個按鍵會編輯組字而非欄位。``text_unicode``(``VK_PACKET``)對此一無所知。
``ime_state`` 暴露即時的組字與轉換狀態,讓流程能在讀取或操作前等待 IME 送出。

* :func:`ime_state` ——聚焦視窗 IME 的 ``{open, composing, composition, conversion,
conversion_flags}``,透過可注入的 ``reader``。
* :func:`is_composing` ——當 IME 有尚未送出的組字時回傳 ``True``。
* :func:`wait_for_composition_commit` ——阻塞直到組字結束(或逾時),``clock`` / ``sleep`` /
``reader`` 皆可注入。
* :func:`decode_conversion_mode` ——純函式:把 IMM32 ``IME_CMODE_*`` 轉換位元遮罩解碼為
``{native, katakana, full_shape, roman, char_code}``。

預設 ``reader`` 以唯讀方式查詢 Windows IMM32(``ImmGetContext`` / ``ImmGetOpenStatus`` /
``ImmGetConversionStatus`` / ``ImmGetCompositionStringW``);所有解碼 / 等待邏輯都透過可注入接縫
執行,故能在沒有 IME 的情況下完整測試。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import (
ime_state, is_composing, wait_for_composition_commit,
)

# 讀取 CJK 欄位前,先確認 IME 已送出
if wait_for_composition_commit(timeout_s=3):
value = read_field()

is_composing() # 候選字仍在畫面上時為 True
ime_state() # {'open': True, 'composing': True, 'composition': 'あ', ...}

測試時(或任何非 Windows 主機)可傳入 ``reader`` ——一個
``() -> {open, conversion, composition}``:

.. code-block:: python

busy = lambda: {"open": True, "conversion": 0, "composition": "あ"}
is_composing(reader=busy) # True
ime_state(reader=busy)["composition"] # 'あ'

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

``AC_ime_state``(→ 完整狀態)、``AC_is_composing``(→ ``{composing}``)、
``AC_wait_for_composition_commit``(``timeout`` / ``interval`` → ``{committed}``)
與 ``AC_decode_conversion_mode``(``flags`` → 解碼後的模式)。皆以對應的唯讀 ``ac_*`` MCP 工具
及 Script Builder 指令(位於 **Shell** 分類下)形式提供。
Loading
Loading