Skip to content

feat(linux): port type_into_editable from pyatspi subprocess to native AT-SPI - #1853

Open
r33drichards wants to merge 1 commit into
mainfrom
feat/linux-native-type-into-editable
Open

feat(linux): port type_into_editable from pyatspi subprocess to native AT-SPI#1853
r33drichards wants to merge 1 commit into
mainfrom
feat/linux-native-type-into-editable

Conversation

@r33drichards

@r33drichards r33drichards commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #1789. Ports the last remaining pyatspi Python-subprocess path, atspi::type_into_editable, to the native Rust AT-SPI implementation in atspi::native:

  • Walks the target pid's accessibility tree over the existing AccessibilityConnection (no python3 -c subprocess, no pyatspi runtime dependency for this path)
  • Picks the best editable via the shared pick_editable heuristic
  • Calls EditableText.set_text_contents directly — setTextContents semantics, unlike insert_text which inserts at the caret

Behaviour contract is unchanged: works on unfocused windows when the toolkit exposes EditableText (Qt6, GTK4); returns Err when none is exposed (e.g. Qt5 unfocused) so callers can run the focus workaround and retry.

Also drops stale pyatspi references in comments (AtspiNode::element_key, resolve_element_local_coords).

Test plan

  • CI: cua-driver-linux-background-gui-* matrix (exercises the AT-SPI read/write paths end-to-end in NixOS VMs, incl. the tk full entry which types via the native AT-SPI path)
  • CI: cua-driver-linux-background-terminal-gif / cua-driver-linux-cursor-click-gif

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Improved text input handling for editable fields through internal implementation updates
  • Documentation

    • Updated AT-SPI path naming documentation for accessibility features

…e AT-SPI

Replace the python3/pyatspi -c subprocess in atspi::type_into_editable with
a native Rust implementation in atspi::native: walk the target pid's tree
over the existing AccessibilityConnection, pick the best editable via
pick_editable, and call EditableText.set_text_contents directly
(setTextContents semantics, unlike insert_text which inserts at the caret).

Same contract as before: works on unfocused windows when the toolkit
exposes EditableText (Qt6, GTK4); errs when none is exposed (Qt5
unfocused) so callers can run the focus workaround and retry. Also drop
stale pyatspi references in comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Jun 5, 2026 7:39pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Looking for one thing? Review this PR in Change Stack to search files, summaries, diffs, and code without losing your place.

Review Change Stack

📝 Walkthrough

Walkthrough

This PR migrates the type_into_editable text input implementation from an external Python pyatspi subprocess to native Rust using AT-SPI interfaces. A new public function in native.rs walks the accessibility tree, selects an editable element, and writes text; the prior public interface now delegates to it. Documentation references to pyatspi are updated to reflect native AT-SPI.

Changes

Native AT-SPI Text Input

Layer / File(s) Summary
Text input delegation to native implementation
libs/cua-driver/rust/crates/platform-linux/src/atspi/mod.rs
Public type_into_editable now delegates directly to native::type_into_editable(pid, text), removing 54 lines of Python script construction, subprocess invocation, and error handling. AtspiNode.element_key doc comment updated from pyatspi path wording to AT-SPI path terminology.
Native AT-SPI text input implementation
libs/cua-driver/rust/crates/platform-linux/src/atspi/native.rs
New public type_into_editable(pid, text) -> Result<()> blocks on shared Tokio runtime, walks AT-SPI tree, selects prioritized editable element via pick_editable, acquires EditableText proxy, and replaces text via set_text_contents with explicit error handling for missing interfaces and toolkit responses (timeout, false, error).
Documentation comment updates
libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs
resolve_element_local_coords comment updated to describe element-bound lookup as native AT-SPI instead of pyatspi subprocess approach.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A rabbit hops from Python's way,
Native rust now holds the sway,
AT-SPI paths, so clean and bright,
Text input flows with native might! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: porting type_into_editable from pyatspi subprocess to native AT-SPI. It is specific, concise, and directly reflects the primary objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/linux-native-type-into-editable

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/cua-driver/rust/crates/platform-linux/src/atspi/native.rs`:
- Around line 549-578: The async work in type_into_editable (the
runtime().block_on(async { ... }) block that calls AccessibilityConnection::new,
collect_visited, pick_editable, obtains editable_text and calls
set_text_contents via call(...).await) must be bounded by an overall timeout;
wrap that entire async operation with tokio::time::timeout (or equivalent
OP_TIMEOUT) and convert a timeout result into an anyhow error like
"type_into_editable timed out" so callers receive a clear timeout error instead
of a hung blocking worker; ensure existing per-step map_err branches remain but
return a timeout error when tokio::time::timeout returns Err.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8a790977-ac62-43a7-a560-bd46968d88a7

📥 Commits

Reviewing files that changed from the base of the PR and between c08f544 and bdd5adf.

📒 Files selected for processing (3)
  • libs/cua-driver/rust/crates/platform-linux/src/atspi/mod.rs
  • libs/cua-driver/rust/crates/platform-linux/src/atspi/native.rs
  • libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs

Comment on lines +549 to +578
runtime().block_on(async {
let conn = AccessibilityConnection::new()
.await
.map_err(|e| anyhow!("AT-SPI connect failed: {e}"))?;
let visited = collect_visited(&conn, pid)
.await?
.ok_or_else(|| anyhow!("no AT-SPI application for pid {pid}"))?;
let target = pick_editable(&visited)
.ok_or_else(|| anyhow!("no editable element found for pid {pid}"))?;
dlog!(
"type_into_editable target: role={:?} focused={} in_web_doc={}",
target.role, target.focused, target.in_web_doc
);

let et = target
.acc
.proxies()
.await
.map_err(|e| anyhow!("interface proxies unavailable: {e}"))?
.editable_text()
.await
.map_err(|e| anyhow!("EditableText unavailable: {e}"))?;
match call(et.set_text_contents(text)).await {
Some(Ok(true)) => Ok(()),
Some(Ok(false)) => Err(anyhow!("setTextContents rejected by toolkit")),
Some(Err(e)) => Err(anyhow!("setTextContents failed: {e}")),
None => Err(anyhow!("setTextContents timed out")),
}
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an overall timeout for type_into_editable to keep latency bounded.

Line 549 executes the full tree walk/write path without OP_TIMEOUT; on large/unresponsive trees this can block for a long time and tie up blocking workers.

⏱️ Suggested fix
 pub fn type_into_editable(pid: u32, text: &str) -> Result<()> {
     runtime().block_on(async {
-        let conn = AccessibilityConnection::new()
-            .await
-            .map_err(|e| anyhow!("AT-SPI connect failed: {e}"))?;
-        let visited = collect_visited(&conn, pid)
-            .await?
-            .ok_or_else(|| anyhow!("no AT-SPI application for pid {pid}"))?;
-        let target = pick_editable(&visited)
-            .ok_or_else(|| anyhow!("no editable element found for pid {pid}"))?;
-        dlog!(
-            "type_into_editable target: role={:?} focused={} in_web_doc={}",
-            target.role, target.focused, target.in_web_doc
-        );
-
-        let et = target
-            .acc
-            .proxies()
-            .await
-            .map_err(|e| anyhow!("interface proxies unavailable: {e}"))?
-            .editable_text()
-            .await
-            .map_err(|e| anyhow!("EditableText unavailable: {e}"))?;
-        match call(et.set_text_contents(text)).await {
-            Some(Ok(true)) => Ok(()),
-            Some(Ok(false)) => Err(anyhow!("setTextContents rejected by toolkit")),
-            Some(Err(e)) => Err(anyhow!("setTextContents failed: {e}")),
-            None => Err(anyhow!("setTextContents timed out")),
+        let work = async {
+            let conn = AccessibilityConnection::new()
+                .await
+                .map_err(|e| anyhow!("AT-SPI connect failed: {e}"))?;
+            let visited = collect_visited(&conn, pid)
+                .await?
+                .ok_or_else(|| anyhow!("no AT-SPI application for pid {pid}"))?;
+            let target = pick_editable(&visited)
+                .ok_or_else(|| anyhow!("no editable element found for pid {pid}"))?;
+            dlog!(
+                "type_into_editable target: role={:?} focused={} in_web_doc={}",
+                target.role, target.focused, target.in_web_doc
+            );
+
+            let et = target
+                .acc
+                .proxies()
+                .await
+                .map_err(|e| anyhow!("interface proxies unavailable: {e}"))?
+                .editable_text()
+                .await
+                .map_err(|e| anyhow!("EditableText unavailable: {e}"))?;
+            match call(et.set_text_contents(text)).await {
+                Some(Ok(true)) => Ok(()),
+                Some(Ok(false)) => Err(anyhow!("setTextContents rejected by toolkit")),
+                Some(Err(e)) => Err(anyhow!("setTextContents failed: {e}")),
+                None => Err(anyhow!("setTextContents timed out")),
+            }
+        };
+        match tokio::time::timeout(OP_TIMEOUT, work).await {
+            Ok(r) => r,
+            Err(_) => Err(anyhow!("type_into_editable timed out for pid {pid}")),
         }
     })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
runtime().block_on(async {
let conn = AccessibilityConnection::new()
.await
.map_err(|e| anyhow!("AT-SPI connect failed: {e}"))?;
let visited = collect_visited(&conn, pid)
.await?
.ok_or_else(|| anyhow!("no AT-SPI application for pid {pid}"))?;
let target = pick_editable(&visited)
.ok_or_else(|| anyhow!("no editable element found for pid {pid}"))?;
dlog!(
"type_into_editable target: role={:?} focused={} in_web_doc={}",
target.role, target.focused, target.in_web_doc
);
let et = target
.acc
.proxies()
.await
.map_err(|e| anyhow!("interface proxies unavailable: {e}"))?
.editable_text()
.await
.map_err(|e| anyhow!("EditableText unavailable: {e}"))?;
match call(et.set_text_contents(text)).await {
Some(Ok(true)) => Ok(()),
Some(Ok(false)) => Err(anyhow!("setTextContents rejected by toolkit")),
Some(Err(e)) => Err(anyhow!("setTextContents failed: {e}")),
None => Err(anyhow!("setTextContents timed out")),
}
})
}
pub fn type_into_editable(pid: u32, text: &str) -> Result<()> {
runtime().block_on(async {
let work = async {
let conn = AccessibilityConnection::new()
.await
.map_err(|e| anyhow!("AT-SPI connect failed: {e}"))?;
let visited = collect_visited(&conn, pid)
.await?
.ok_or_else(|| anyhow!("no AT-SPI application for pid {pid}"))?;
let target = pick_editable(&visited)
.ok_or_else(|| anyhow!("no editable element found for pid {pid}"))?;
dlog!(
"type_into_editable target: role={:?} focused={} in_web_doc={}",
target.role, target.focused, target.in_web_doc
);
let et = target
.acc
.proxies()
.await
.map_err(|e| anyhow!("interface proxies unavailable: {e}"))?
.editable_text()
.await
.map_err(|e| anyhow!("EditableText unavailable: {e}"))?;
match call(et.set_text_contents(text)).await {
Some(Ok(true)) => Ok(()),
Some(Ok(false)) => Err(anyhow!("setTextContents rejected by toolkit")),
Some(Err(e)) => Err(anyhow!("setTextContents failed: {e}")),
None => Err(anyhow!("setTextContents timed out")),
}
};
match tokio::time::timeout(OP_TIMEOUT, work).await {
Ok(r) => r,
Err(_) => Err(anyhow!("type_into_editable timed out for pid {pid}")),
}
})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/rust/crates/platform-linux/src/atspi/native.rs` around lines
549 - 578, The async work in type_into_editable (the runtime().block_on(async {
... }) block that calls AccessibilityConnection::new, collect_visited,
pick_editable, obtains editable_text and calls set_text_contents via
call(...).await) must be bounded by an overall timeout; wrap that entire async
operation with tokio::time::timeout (or equivalent OP_TIMEOUT) and convert a
timeout result into an anyhow error like "type_into_editable timed out" so
callers receive a clear timeout error instead of a hung blocking worker; ensure
existing per-step map_err branches remain but return a timeout error when
tokio::time::timeout returns Err.

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Linux visual regression artifacts

Matrix jobs now run independently. Download visual artifacts from this workflow run.
Each background-GUI job uploads a .gif of the interaction plus two annotated PNGs (<app>.png raw, <app>-atspi.png with AT-SPI element boxes); the cua-driver-linux-som-overlays artifact adds <app>-som.png cua Set-of-Marks overlays:

  • cua-driver-linux-cursor-click-gif
  • cua-driver-linux-background-terminal-gif
  • cua-driver-linux-background-gui-chromium
  • cua-driver-linux-background-gui-tk
  • cua-driver-linux-background-gui-gtk3-gedit
  • cua-driver-linux-background-gui-gtk3-mousepad
  • cua-driver-linux-background-gui-gtk3-scite
  • cua-driver-linux-background-gui-gtk4-characters
  • cua-driver-linux-background-gui-qt5-manuskript
  • cua-driver-linux-background-gui-qt5-klog
  • cua-driver-linux-background-gui-qt5-openambit
  • cua-driver-linux-background-gui-qt6-kate
  • cua-driver-linux-background-gui-qt6-kcalc
  • cua-driver-linux-background-gui-qt6-okular
  • cua-driver-linux-background-gui-qt6-qownnotes
  • cua-driver-linux-background-gui-electron-zettlr
  • cua-driver-linux-background-gui-electron-joplin
  • cua-driver-linux-background-gui-electron-logseq
  • cua-driver-linux-som-overlays

Open workflow run and download artifacts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant