Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pinchy, a small crab poking an app window

pinchy

CI Version License Rust GNOME

Poke GNOME apps from the command line.
A private, headless GNOME session you can screenshot, click and type into -
for end-to-end tests, scripts and coding agents. Ferris does the pinching.


Testing a GTK app by hand means clicking through it on your own desktop. Automating that usually means fighting your real session: windows popping up, focus stolen, the mouse moving under your hand. pinchy starts a separate, invisible GNOME session instead - its own D-Bus, its own accessibility bus, a headless mutter with a virtual monitor - and gives you one small binary to drive whatever runs inside it.

$ pinchy up
session 'default' up: 1280x800 on pinchy-default

$ pinchy launch gnome-calculator
launched 'gnome-calculator' (pid 100015)

$ pinchy click 7 && pinchy click × && pinchy click 6 && pinchy click =
$ pinchy text | grep 42
#0.0.0.0.0.0.0.0.1.0.0.0 [text box] "": "42"

$ pinchy shot calc.png
/home/you/calc.png

$ pinchy down
session 'default' down, stopped 13 processes

gnome-calculator showing 7×6 = 42, screenshotted by pinchy

Why pinchy

  • Your desktop stays yours. Everything runs in a session pinchy started itself. Nothing is clicked, typed or screenshotted on your real screen, and pinchy down stops exactly the processes it started - including the portals and helpers D-Bus spawned on its behalf.
  • Click by name, not by pixel. Widgets are found through the accessibility tree (AT-SPI), the same way screen readers see them: pinchy click "Import Photos". When there is no name, pinchy tree gives every widget an id. Coordinates are there as a fallback.
  • The real thing. It is the real mutter compositor on the real Wayland backend your users run - not a deprecated X11 or Broadway backend.
  • Talks to your app directly. GActions exported on D-Bus can be listed and fired (pinchy act win.show-view "'gallery'"), which reaches menus and dialogs the accessibility tree cannot.
  • Made for scripts and agents. Every command prints plain text or, with --json, JSON. pinchy mcp serves the same commands as tools over the Model Context Protocol, screenshots included as images.
  • One binary, no runtime. Rust, synchronous D-Bus via zbus, no Python, no browser.

Install

pinchy needs a GNOME stack on the host: a recent mutter (with --headless and --virtual-monitor), at-spi2-core, dbus and gstreamer with the PipeWire plugin for screenshots.

# Arch Linux
sudo pacman -S mutter at-spi2-core gstreamer gst-plugin-pipewire
# Debian / Ubuntu
sudo apt install mutter at-spi2-core gstreamer1.0-tools gstreamer1.0-pipewire

cargo install --git https://github.com/xuedi/pinchy

Commands

Command What it does
pinchy up [--size 1280x800] Start the private headless session (reuses a running one)
pinchy down Stop the session and every process pinchy started in it
pinchy status Session, processes and accessible apps
pinchy launch -- <cmd> [args] Start an app inside the session, wait until it is accessible
pinchy apps List accessible applications
pinchy tree [--app A] [--text] [--all] [--raw] Accessibility tree: ids, roles, names, actions, states
pinchy click <name> [--role R] [--id ID] [--nth N] Run a widget's accessible action (click it)
pinchy text [--app A] All visible text: labels, entries, displays
pinchy set <name> --value V Replace the text of an editable widget
pinchy wait-for <name> [--role R] [--timeout S] Wait until a widget is showing
pinchy actions [--app-id ID] List the GActions an app exports
pinchy act <app.x|win.x> [param] Activate a GAction, parameter in GVariant text
pinchy shot [file.png] Screenshot the virtual monitor
pinchy tap <x> <y> [--button right] [--double] Pointer click at coordinates
pinchy type <text> Type with the virtual keyboard
pinchy key <combo> Press keys: Return, Escape, ctrl+q, alt+F4, F5
pinchy env / pinchy run -- <cmd> Run your own commands (e.g. cargo test) inside the session
pinchy mcp Serve everything above as MCP tools over stdio

Global flags: --json for machine-readable output, -s/--session NAME to run several sessions side by side (or PINCHY_SESSION_NAME).

A tour

$ pinchy tree --app gnome-calculator
#app [application] "gnome-calculator"
  #0 [window] "Calculator" actions=win.close,win.paste,win.back,win.preferences,win.clear,win.copy
    #0.0.0.0.0.0.0.0.1.0.0.0 [text box] "" (editable,focusable)
    #0.0.0.0.0.0.0.0.1.0.1 [button] "Backspace" actions=Click (focusable)
    #0.0.0.0.0.0.0.1 [generic] "" actions=cal.logarithm,cal.clear,cal.square,cal.subtract,cal.solve
      #0.0.0.0.0.0.0.1.0.0.0.0.3 [button] "7" actions=Click (focusable)
      ...

$ pinchy click --id 0.0.0.0.0.0.0.0.1.0.1          # or by id, straight from the tree
$ pinchy set "" --role "text box" --value "100/4"  # the unnamed entry
$ pinchy key Return
$ pinchy wait-for "25" --role label --timeout 2

Apps that are unique GtkApplications export their actions on D-Bus:

$ pinchy launch gnome-text-editor
$ pinchy actions
org.gnome.TextEditor
app.new-window
app.quit
app.style-scheme (s) state="Adwaita"
...
$ pinchy act app.new-window

Use it from tests

Run your test suite inside the session, so GTK tests never open a window on your desktop:

pinchy up
pinchy run -- cargo test -- --test-threads=1
pinchy down

Or drive the binary from an integration test and assert on --json output - see tests/e2e.rs, which does exactly that against gnome-calculator and gnome-text-editor.

Use it as an MCP server

pinchy mcp speaks the Model Context Protocol over stdio. Add it to any MCP client:

{
  "mcpServers": {
    "pinchy": { "command": "pinchy", "args": ["mcp"] }
  }
}

Tools: session_up, session_down, session_status, launch, apps, screenshot (returns the PNG), tree, click, text, set_text, wait_for, actions, activate, tap, type_text, key. Tool errors come back as results with isError, so the client can react.

Make your app pinchy-friendly

Everything that helps pinchy also helps people using a screen reader:

  • Give every icon-only button an accessible label (button.update_property(&[gtk::accessible::Property::Label("Import")])).
  • Use a real application id and keep the app unique, so its GActions appear on D-Bus.
  • Expose the things you want to test as actions (win.show-view, app.open-path); a debug-only action that dumps state as JSON makes assertions trivial.
  • Point the app at test data through its own settings or environment - pinchy sets GSETTINGS_BACKEND=memory, so nothing leaks into your real settings.

How it works

See docs/how-it-works.md: the session layout, how screenshots travel through PipeWire, how input is injected through mutter's remote desktop interface, and the small gotchas pinchy handles for you.

License

pinchy is licensed under the European Union Public Licence v1.2.

About

Poke GNOME apps from the command line: a private headless session you can screenshot, click and type into - for tests, scripts and coding agents. MCP server included.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages