Native desktop GUI test harness: find/focus a window (composes infrastructure-screencast), synthesize real hardware mouse/keyboard input via xdotool (physical focus, not synthetic events), screenshot via infrastructure-screen-capture, and read rendered text back via infrastructure-ocr-tesseract. For driving/verifying a real running desktop app (egui/eframe, GTK, Qt, ...) end to end, the same way a human would.
| Tier | tools |
| Role | unclassified (baselined) |
| Path | crates/tools/gui-harness |
| Edition | 2021 |
| Targets | guarded_click, smoke, verify_window, tools_gui_harness |
| Public items | 21 across 4 modules |
| Tests | 16 |
What it is for
Native desktop GUI test harness: find/focus a window, drive it with real hardware mouse/keyboard input, screenshot it, and read its rendered text back via OCR. For verifying a real running desktop app (egui/eframe, GTK, Qt, ...) end to end, the way a human tester would -- not a mock, not a synthetic-event framework.
Composes existing library crates rather than re-implementing them (Gate 0.5): window find/focus is infrastructure-screencast, screenshot is infrastructure-screen-capture, OCR is infrastructure-ocr-tesseract. The one genuinely new piece is input -- hardware mouse/keyboard actuation via xdotool, which existed before only as a private helper inside tools-oauth-consent (Gate 4.5 follow-up: that crate should migrate to this one).
Capabilities
capture (other)
Screenshot capture, thin over infrastructure-screen-capture.
| Item |
|---|
fn capture() -> Result <Shot, CaptureError> |
CaptureError
Screenshot capture, thin over infrastructure-screen-capture.
| Item |
|---|
pub enum CaptureError |
fn capture_to_file(path : & Path) -> Result <(u32, u32), CaptureError> |
Shot
Screenshot capture, thin over infrastructure-screen-capture.
| Item |
|---|
pub struct Shot |
Input
Hardware mouse/keyboard actuation via xdotool (argv subprocess, never a
| Item |
|---|
pub struct Input |
Input :: fn default() -> Self |
Input :: fn new() -> Self |
Input :: fn with_bin(bin : impl Into <String>) -> Self |
Input :: fn click(& self, x : i64, y : i64, button : u8) -> Result <(), InputError> |
Input :: fn move_mouse(& self, x : i64, y : i64) -> Result <(), InputError> |
Input :: fn key(& self, sequence : & str) -> Result <(), InputError> |
Input :: fn type_text(& self, text : & str) -> Result <(), InputError> |
Input :: fn verified_click(& self, x : i64, y : i64, button : u8, expected_window_title_contains : & str,) -> Result <(), VerifiedClickError> |
Input :: fn click_and_settle(& self, x : i64, y : i64, button : u8, settle : Duration,) -> Result <(), InputError> |
InputError
Hardware mouse/keyboard actuation via xdotool (argv subprocess, never a
| Item |
|---|
pub enum InputError |
VerifiedClickError
Hardware mouse/keyboard actuation via xdotool (argv subprocess, never a
| Item |
|---|
pub enum VerifiedClickError |
OcrHarnessError
Read rendered text back out of a screenshot, via infrastructure-ocr-tesseract.
| Item |
|---|
pub enum OcrHarnessError |
fn recognize_png(png : & u8, width : u32, height : u32) -> Result <String, OcrHarnessError> |
WindowError
Window find/focus: composes infrastructure-screencast::window rather than
| Item |
|---|
pub enum WindowError |
fn active_window_title(xdotool_bin : & str) -> Result <String, WindowError> |
fn find_and_focus(needle : & str) -> Result <WindowInfo, WindowError> |
How to use it
From `examples/guarded_click.rs`:
use std::env;
use tools_gui_harness::input::Input;
fn main() {
let mut args = env::args().skip(1);
let x: i64 = args
.next()
.expect("usage: guarded_click <x> <y> <expected-title-substring>")
.parse()
.expect("x must be an integer");
let y: i64 = args
.next()
.expect("missing y")
.parse()
.expect("y must be an integer");
let expected = args.next().expect("missing expected-title-substring");
match Input::new().verified_click(x, y, 1, &expected) {
From `examples/smoke.rs`:
use tools_gui_harness::{capture, ocr};
fn main() {
let shot = capture::capture().expect("capture failed");
println!(
"captured {}x{} ({} PNG bytes)",
shot.width,
shot.height,
shot.png.len()
);
std::fs::write("/tmp/gui-harness-smoke.png", &shot.png).expect("write failed");
println!("wrote /tmp/gui-harness-smoke.png");
let text = ocr::recognize_png(&shot.png, shot.width, shot.height).expect("ocr failed");
println!("OCR recognized {} chars", text.len());
println!("--- first 300 chars ---");
println!("{}", text.chars().take(300).collect::<String>());
Module structure
tools_gui_harness
captureinputocrwindow
flowchart TD n_tools_gui_harness["tools_gui_harness"] n_tools_gui_harness --> n_capture["capture"] n_tools_gui_harness --> n_input["input"] n_tools_gui_harness --> n_ocr["ocr"] n_tools_gui_harness --> n_window["window"]
Public surface
`capture`
| Item | What it is |
|---|---|
pub enum CaptureError | — |
pub struct Shot | — |
fn capture() -> Result <Shot, CaptureError> | Capture the primary monitor and return the encoded PNG + its dimensions. |
fn capture_to_file(path : & Path) -> Result <(u32, u32), CaptureError> | Capture the primary monitor and write the PNG to path (for the agent/ operator to visually inspect) |
`input`
| Item | What it is |
|---|---|
pub enum InputError | — |
pub enum VerifiedClickError | Input::verified_click's refusal reasons |
pub struct Input | — |
Input :: fn default() -> Self | — |
Input :: fn new() -> Self | — |
Input :: fn with_bin(bin : impl Into <String>) -> Self | — |
Input :: fn click(& self, x : i64, y : i64, button : u8) -> Result <(), InputError> | Move the mouse to (x, y) (screen coordinates) and click |
Input :: fn move_mouse(& self, x : i64, y : i64) -> Result <(), InputError> | Move the mouse to (x, y) without clicking. |
Input :: fn key(& self, sequence : & str) -> Result <(), InputError> | Send an xdotool key sequence (e.g |
Input :: fn type_text(& self, text : & str) -> Result <(), InputError> | Type literal text (e.g |
Input :: fn verified_click(& self, x : i64, y : i64, button : u8, expected_window_title_contains : & str,) -> Result <(), VerifiedClickError> | Click, but only if the window that currently, actually has X11 input focus is the one the caller intends -- checked immediately before the click, not "focused a while ago" (a real incident, 2026-08-09: a fixed-coordinate click during a long-running automation landed in whatever window had stolen focus in the meantime, not the intended target) |
Input :: fn click_and_settle(& self, x : i64, y : i64, button : u8, settle : Duration,) -> Result <(), InputError> | Convenience: click, then a short settle delay for the app to react before the next screenshot/assertion. |
`ocr`
| Item | What it is |
|---|---|
pub enum OcrHarnessError | — |
fn recognize_png(png : & u8, width : u32, height : u32) -> Result <String, OcrHarnessError> | Recognize text in a PNG screenshot |
`window`
| Item | What it is |
|---|---|
pub enum WindowError | — |
fn active_window_title(xdotool_bin : & str) -> Result <String, WindowError> | The title of whichever window currently has X11 input focus, right now -- not "the window we last told to focus" (a focus() call earlier in the program proves nothing about who has focus this instant; another window can steal it in between) |
fn find_and_focus(needle : & str) -> Result <WindowInfo, WindowError> | Find a window whose title/class contains needle and bring it to the front |
No pub use re-exports: every item above is declared in this crate.
Boundary
Reaches into infrastructure.
Shares tier tools with 84 other crates: tools-advisory-reach, tools-archive-guard, tools-artifact-scaffold, tools-ask-ai-core, tools-ask-ais, tools-ask-gemini, tools-book, tools-book-report, … (84 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | tools |
| Architectural role (taxonomy) | unclassified (baselined) |
| Location | crates/tools/gui-harness |
| Vocabulary in force (lexicon) | current |
Tier flow. Which tiers this crate's own edges cross.
flowchart LR n_tools["tools"] --> n_infrastructure["infrastructure"]
Dependencies
Runtime, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `infrastructure-ocr-engine` | infrastructure | no | always |
| `infrastructure-ocr-tesseract` | infrastructure | no | always |
| `infrastructure-screen-capture` | infrastructure | no | always |
| `infrastructure-screencast` | infrastructure | no | always |
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
thiserror | ^2 | — | no | always |
tokio | ^1 | full | no | always |
Development. None.
Build. None.
Depended on by. 1 workspace crate.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR n_tools_demo_systats["tools-demo-systats"] -->|uses| SELF SELF["tools-gui-harness"] SELF -->|runtime| n_infrastructure_ocr_engine["infrastructure-ocr-engine"] SELF -->|runtime| n_infrastructure_ocr_tesseract["infrastructure-ocr-tesseract"] SELF -->|runtime| n_infrastructure_screen_capture["infrastructure-screen-capture"] SELF -->|runtime| n_infrastructure_screencast["infrastructure-screencast"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
No Cargo features are defined: every capability is unconditional, so no consumer can receive a half-wired crate.
Targets
| Kind | Name | Source |
|---|---|---|
| example | guarded_click | `examples/guarded_click.rs` |
| example | smoke | `examples/smoke.rs` |
| example | verify_window | `examples/verify_window.rs` |
| lib | tools_gui_harness | `src/lib.rs` |
Error model
| Error type | Named by |
|---|---|
CaptureError | capture, capture_to_file |
InputError | declared, no public signature returns it |
OcrHarnessError | recognize_png |
VerifiedClickError | declared, no public signature returns it |
WindowError | active_window_title, find_and_focus |
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | none detected |
| async runtime | yes |
| database access | none detected |
| network I/O | none detected |
| unsafe code | none detected |
| environment variables | none detected |
No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.
Configuration
No environment variable is read with a literal name anywhere in this crate. A variable whose key is computed at run time cannot be listed here, and is not claimed to be absent.
Related capabilities
1 workspace crate depends on this one: tools-demo-systats.
Verification
| Kind | Count |
|---|---|
| Unit tests | 16 |
| Integration tests | 0 |
| Examples | 3 |
| Doctests | 0 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
capture | 4 | 1 | 1 |
input | 3 | 1 | 1 |
ocr | 2 | 1 | 0 |
window | 3 | 1 | 0 |
What the tests establish, by name:
boundary_title_matches_empty_expected_substring_always_matches—src/input.rsdeny_missing_binary_propagates_from_click_and_settle—src/input.rsdeny_missing_binary_propagates_from_key—src/input.rsdeny_missing_binary_propagates_from_move_mouse—src/input.rsdeny_missing_binary_propagates_from_type_text—src/input.rsdeny_missing_binary_yields_spawn_error_not_a_panic—src/input.rsdeny_title_matches_rejects_a_different_window—src/input.rsdeny_verified_click_missing_binary_never_reaches_the_click—src/input.rsvalid_click_args_are_mousemove_then_click—src/input.rsvalid_key_args_use_the_double_dash_separator—src/input.rsvalid_move_args_are_mousemove_only—src/input.rsvalid_title_matches_is_case_insensitive_substring—src/input.rsvalid_type_args_pass_text_verbatim_after_separator—src/input.rsdeny_active_window_title_missing_binary_is_an_error—src/window.rsdeny_active_window_title_nonzero_exit_is_an_error—src/window.rsvalid_active_window_title_returns_trimmed_stdout—src/window.rs
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 12 | 21 |
Public modules with a //! block | 4 | 4 |
pie showData
title Public items with rustdoc
"Documented" : 12
"No rustdoc detected" : 9
Metrics
| Metric | Value |
|---|---|
| Rust source files | 5 |
| Source lines | 468 |
| Code lines | 341 |
| Public API items | 21 |
| Public modules | 4 |
| Tests | 16 |
| Examples | 3 |
| Cargo features | 0 |
| Direct runtime dependencies | 6 |
| Workspace reverse dependencies | 1 |
pie showData
title Public API by kind
"enum" : 5
"function" : 5
"method" : 9
"struct" : 2
pie showData
title Rust source composition
"Code" : 341
"Blank or comment" : 127
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.