Metadata-Version: 2.5
Name: uictl
Version: 0.1.4
Summary: Linux desktop UI automation control CLI based on dogtail / AT-SPI
Project-URL: Repository, https://github.com/lin-calvin/uictl
Project-URL: Issues, https://github.com/lin-calvin/uictl/issues
Author-email: calvinweb <calvinchaochao@outlook.com>
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Keywords: accessibility,at-spi,automation,cli,dogtail,gui,testing,wayland,x11
Classifier: Environment :: X11 Applications
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Requires-Dist: dasbus>=1.7
Requires-Dist: dogtail>=2.0.0
Description-Content-Type: text/markdown

# uictl

Linux 桌面 UI 自动化控制 CLI，基于 [dogtail](https://gitlab.com/dogtail/dogtail) / AT-SPI
无障碍树。搜索窗口、浏览控件树、点击控件、输入文本、等待/断言，一条命令搞定。

```bash
uictl apps                              # 列出所有应用
uictl windows gnome-text-editor         # 列出应用的窗口
uictl tree gnome-text-editor --depth 4  # 打印窗口控件树
uictl find gnome-text-editor "role=button"
uictl inspect gnome-text-editor "name=保存"
uictl read Chromium                        # 提取页面干净文本（自动定位 document）
uictl read Chromium 'role=paragraph'       # 只读 selector 子树下所有文本
uictl click gnome-text-editor "role=button[name=保存]"
uictl type gnome-text-editor "role=text" --text "hello"
uictl wait  gnome-text-editor "name=保存" --visible
uictl expect gnome-text-editor "name=保存" --visible --sensitive
```

## 安装

```bash
# 轻量命令（不碰无障碍树）：直接跑
uvx uictl --version
uvx uictl install-skill        # 注入 agent 技能到 ~/.agents/skills/uictl/

# 完整 UI 命令：用系统 Python 跑（gi 无 PyPI wheel，只能来自系统包；
# uictl 在 gi 缺失时自动注入系统 site-packages 到 sys.path，无需 PYTHONPATH）
uvx --python /usr/bin/python3 uictl apps

# 或持久安装
uv tool install uictl --python /usr/bin/python3
```

依赖系统包（AT-SPI typelib，无法 pip 安装）：
`gir1.2-atspi-2.0 python3-gi python3-pyatspi`
（Wayland 下如需输入注入：`gnome-ponytail-daemon`，GNOME 46+ 已封禁注入，见下）。

本地开发（本仓库）：

```bash
cd ~/projects/uictl
uv venv .venv --python $(which python3) --system-site-packages
uv pip install -e .
```

## 子命令

| 子命令 | 作用 | Wayland 可用 |
|---|---|---|
| `apps` | 列出应用（`--json`） | ✅ |
| `windows [APP]` | 列出窗口 | ✅ |
| `tree APP` | 控件树（`--depth` `--path` `--json`） | ✅ |
| `find APP SEL [--context N]` | 搜索控件，输出 path；`--context N` 打印目标周边局部树 | ✅ |
| `inspect APP SEL` | 控件详情（状态/文本/坐标/Action 列表） | ✅ |
| `click APP SEL` | 点击：**AT-SPI Action 自动链**（click→press→activate→toggle→check/uncheck），全无才回退注入（`--action NAME`） | ✅ Action / ⚠️ 注入 |
| `dblclick APP SEL` | 双击 | ⚠️ 需注入 |
| `hover APP SEL` | 悬停 | ⚠️ 需注入 |
| `type APP SEL --text X` | 输入：fill 后**自动验证**，失败自动降级 `insert_text`，仍失败清晰报错（`--keystrokes` 逐键 ⚠️ 需注入） | ✅ / ⚠️ |
| `key COMBO` | 组合键，如 `"<Control>a"` | ⚠️ 需注入 |
| `read APP [SEL] [--win] [--min-length N] [--skip-role a,b] [--max-depth N]` | 提取干净文本：默认读窗口内所有 document 节点，无则整窗；传 SEL 则读其子树 | ✅ |
| `search [APP] KEYWORD [--win] [--scope] [--name-only] [--context N] [--limit N]` | 全文发现（name/text/desc 子串，不区分大小写）；APP 省略搜所有应用 | ✅ |
| `snapshot APP [--win]` | 捕获 tree-json 精度基线，打印短 id，存 `/tmp/uictl-UID/<id>.json` | ✅ |
| `delta ID [--keep] [--json]` | 对比快照，输出 `+`/`-`/`~`（树节点级差异），默认 diff 后删除 | ✅ |
| `snapshots [--clear]` | 列出/清理存活快照 | ✅ |
| `wait APP SEL [--visible] [--sensitive] [--focused] [--gone]` | 轮询等待条件成立 | ✅ |
| `expect APP SEL [--text S] [--role R] ...` | 断言，失败 exit 1 | ✅ |
| `install-skill [--dir DIR] [--force]` | 注入 agent 技能到 `~/.agents/skills/uictl/` | ✅ |

> ⚠️ 需注入 = 需要原始输入注入（X11 会话原生支持；Wayland 需 RemoteDesktop
> 后端）。在只读环境（GNOME 46+ Wayland）下这些命令会**报清晰错误**而不是
> 抛 traceback。

## 选择器语法（Playwright 风格）

```
role=button[name=保存]         # 引擎 + [属性...]
name=保存                      # 单属性
text=请输入                    # 匹配 node.text 子串
path=0:2:3                     # 从应用根开始的索引路径（find 输出的 path 可直接用）
保存                           # 裸字符串 = name
```

支持属性：`name` `role`(role_name) `desc`(description) `label` `id`(accessible_id) `text`。
值含空格需加引号：`name="保存 文档"`。

**strict 模式**：`inspect`/`click` 等要求唯一匹配，命中多个会报错并列出全部匹配，
用 `--index N` 指定；`--scope` 限定在父节点内搜索；`--win` 指定多窗口应用的窗口。

## `read`：提取干净的页面文本

`uictl read APP` 把页面内容抽成干净的纯文本行（每段/每标题一行），适合让 agent
直接"读网页"。行为：

- **默认根**：在窗口里递归找所有 document 类角色（`document` / `document web` /
  `document frame` / `document picture` / `web area`），逐个读取拼接；一个都没有
  （如 GTK 文本编辑器）就回退读整个窗口子树。
- **去重**：容器节点（heading/paragraph/link）的 `node.text` 已聚合子孙文本，
  重复的 static 子节点会被自动丢弃，标题/链接不会出现两遍。
- **内联替换**：Chromium 用 `\ufffc`(U+FFFC) 占位内联子节点，如
  `"You can ￼."` 会还原成 `"You can index it here."`。
- `image` 无文本时用其 name（alt 文本）兜底。

常用参数：`--min-length N` 丢弃过短行、`--skip-role button,panel` 整类跳过、
`--max-depth N` 限深、`--win` 指定窗口。传 `SEL` 时改为 strict 解析单个节点、只读
该子树（如 `uictl read Chromium 'role=paragraph'`）。

## `search` / `snapshot` / `delta`：给 LLM Agent 的"发现 + 验证"配套

uictl 面向 LLM Agent 使用做了几处容错设计：

- **`type` 不再静默撒谎**：fill（`set_text_contents`）后自动读回验证，失败自动降级
  `delete_text + insert_text`（Chromium 网页输入框吃这一套），仍失败才报清晰错误。
- **`click` 自动选 Action**：按 `click → press → activate → toggle → check/uncheck`
  依次尝试，Wayland 下 radio/checkbox 无需手动 `--action`。
- **`search` 代替 `tree | head`**：禁止对 `tree` 全量输出用 `head` 截断（会漏掉
  document 里的内容）。发现用 `search APP "关键词"`（覆盖整页、一次拿全），看结构用
  `find APP SEL --context N`（目标周边局部树，命中行标 `◀ target`）。
- **`snapshot` + `delta` 验证操作生效**：

  ```
  $ uictl snapshot Chromium
  snap: e3f2a91
  $ uictl type Chromium "name=令牌名称" --text tea-cli
  $ uictl delta e3f2a91
  ~ [entry] 令牌名称: text '' → 'tea-cli'      # 填进去了
  [snapshot e3f2a91 removed]
  ```

  - `snapshot` 捕获**与 `tree --json` 同精度**的树（`role/name/text/path/children`），
    存 `/tmp/uictl-<uid>/<id8>.json`；`delta` 对树做多重集 diff（稳定键
    `role+name+text`，忽略 path/坐标 churn），输出 `+` 新增 / `-` 消失 / `~` 文本编辑。
  - **默认 diff 完即删**，强制"基线→操作→验证"原子化，防止拿过期基线对比；`--keep`
    保留、`snapshots` 列出、`--clear` 清空。
  - 性能说明：`tree`/`snapshot`/`delta` 都是整树遍历，AT-SPI 每节点一次 DBus（无缓存），
    重页面约 5–15s，属正常范围（`tree --json` 用增量 path，比旧版快约 5 倍）。

## 平台限制说明（为什么有的命令报错）

- **读**（apps/windows/tree/find/inspect/read/search/snapshot/delta/wait/expect）走 AT-SPI 总线，Wayland 全可用。
- **控**（click/type-fill）走 **AT-SPI 接口**（Action/EditableText/Component），
  应用自己实现，Wayland 全可用——这是 GNOME 给无障碍"控制"设计的官方通道。
- **原始输入注入**（key/hover/dblclick/type-keystrokes）在 X11 走 XTEST；
  在 Wayland 被设计成受控特权（需 RemoteDesktop portal 授权）。
  另外 GNOME 46+ 移除了 `org.gnome.Shell.Introspect.GetWindows`，
  gnome-ponytail-daemon 依赖它做坐标换算，因此 dogtail 的注入在
  GNOME 46+ Wayland 上整体不可用 → uictl 会报清晰错误。

## 退出码

- `0` 成功
- `1` 未找到 / 断言失败 / strict 多匹配 / 平台不支持
- `130` 用户中断

## Roadmap

- `screenshot`（GNOME 50 需走 XDG Desktop Portal，`core.take_screenshot` 已预留）
- RemoteDesktop portal 输入后端（Wayland 注入的官方通道，需用户授权）

## 发布到 PyPI

```bash
cd ~/projects/uictl
uv build                                    # 构建 sdist + wheel（产出 dist/）
uv publish                                 # 需要 PyPI API token 凭据
```

凭据（三选一）：

- `~/.pypirc`（`[pypi] username=__token__ password=pypi-xxx`）——本机已配置
- 环境变量：`UV_PUBLISH_TOKEN=pypi-xxx uv publish`
- 交互输入：`uv publish --username __token__`（会提示输入密码）

> ⚠️ 版本一旦发布不可覆盖，改代码后记得 bump `pyproject.toml` 的 `version`。
> 正式发布前可先试水 Test PyPI：
> `uv publish --publish-url https://test.pypi.org/legacy/ --check-url https://test.pypi.org/simple/`
> （需要 test.pypi.org 的 token，写到 `~/.pypirc` 的 `[testpypi]` 段）。
