Runtime UI inspector¶
The eepp inspector is a small TCP interface for inspecting and interacting with a running eepp UI. It is useful for debugging layout, CSS, focus, input delivery, multiple windows, and HTML inside UIWebView. Its compact JSON replies and server side batches are designed to reduce tool round trips for automated agents.
Security and activation¶
The capability can be compiled into every eepp application. It is disabled by default: a normal run has no inspector listener, token, network thread, or reachable parser. Enabling it starts an explicit developer debugging interface. In control mode, authenticated clients can click and type into the application.
The listener binds to 127.0.0.1 by default. Every connection must present a bearer token. This is a safeguard against accidental or unintended attachment, including attachment to the wrong process; it is not a sandbox boundary against malicious software running as the same OS user. The interface resembles accessibility or OS automation in its ability to inspect and act, but additionally exposes eepp widget classes, CSS values, geometry, nested scenes, and watches. Availability of accessibility support does not enable the inspector.
Raw TCP is plaintext, including the token, UI text, and actions. Binding to a non-loopback address prints a warning and should be used only on a trusted network. For remote use, leave the inspector on loopback and forward it with ssh -L 9876:127.0.0.1:<remote-port> host. Password input text is returned as [redacted] in summaries, inspection, trees, and watches. Screenshots capture visible pixels, which can include sensitive information even when semantic text is redacted. Screenshot files remain in the OS temporary directory until the client or OS removes them. A generated token is printed once at startup; anyone who can read that process output can authenticate. A caller supplied token is not printed.
Set the following environment variables before launching any normal eepp UI application:
Variable |
Default |
Meaning |
|---|---|---|
EEPP_INSPECTOR=1 ./bin/ecode EEPP_INSPECTOR=1 EEPP_INSPECTOR_PORT=9876 EEPP_INSPECTOR_READ_ONLY=1 ./bin/ecode EEPP_INSPECTOR=1 EEPP_INSPECTOR_TOKEN="$secret" ./bin/ecode EEPP_INSPECTOR=1 EEPP_INSPECTOR_ADDRESS=0.0.0.0 ./bin/ecode # trusted network only
Successful startup writes one compact line to stderr:
EEPP_INSPECTOR={"protocolVersion":1,"host":"127.0.0.1","port":43187,"token":"<generated-token>","pid":19382}
With EEPP_INSPECTOR_TOKEN supplied, the token member is omitted. The prefix EEPP_INSPECTOR= is stable. The server accepts up to 16 simultaneous TCP clients, and a request line is limited to 1 MiB. Each client’s pending output is limited to 4 MiB. A single query returns at most 1000 nodes; a tree returns at most 1000 nodes; a batch accepts at most 100 commands.
Python client¶
projects/scripts/eepp-inspect.py requires Python 3 and only the standard library. It reads EEPP_INSPECTOR_HOST, EEPP_INSPECTOR_PORT, and EEPP_INSPECTOR_TOKEN, or the corresponding --host, --port, and --token options. Use --pretty for indented JSON. Default stdout is compact JSON only; diagnostics go to stderr. Ordinary commands connect, authenticate, print one response, and disconnect.
export EEPP_INSPECTOR_PORT=43187 EEPP_INSPECTOR_TOKEN='<token>' python3 projects/scripts/eepp-inspect.py contexts python3 projects/scripts/eepp-inspect.py query '#preview' python3 projects/scripts/eepp-inspect.py query --scene scene:2 'table > tr > td.problem' --properties geometry.size css.font-size python3 projects/scripts/eepp-inspect.py tree --scene scene:2 --depth 4 python3 projects/scripts/eepp-inspect.py inspect w:42 geometry.size geometry.position css.font-size python3 projects/scripts/eepp-inspect.py focus --scene scene:2 python3 projects/scripts/eepp-inspect.py screenshot --scene scene:2 --format png python3 projects/scripts/eepp-inspect.py screenshot --window win:2 --rect 10 20 400 300 --format webp python3 projects/scripts/eepp-inspect.py click --scene scene:2 '#submit' python3 projects/scripts/eepp-inspect.py key --scene scene:2 Enter python3 projects/scripts/eepp-inspect.py type --scene scene:2 'hello world' python3 projects/scripts/eepp-inspect.py type --target '#search' 'hello world' python3 projects/scripts/eepp-inspect.py watch --scene scene:2 '#problem' geometry.size css.font-size --count 3 --timeout 10 python3 projects/scripts/eepp-inspect.py batch workflow.json python3 projects/scripts/eepp-inspect.py batch - < workflow.json python3 projects/scripts/eepp-inspect.py raw '{"method":"ui.query","params":{"selector":"#foo"}}' python3 projects/scripts/eepp-inspect.py session
type --target sends input.click followed by input.text, using the clicked widget’s scene. watch streams one event per line; --count counts watch.changed events and --timeout bounds elapsed seconds. batch reads a JSON object containing commands from a file or stdin. session authenticates once, then sends one JSON request per stdin line and writes every response or event to stdout. It keeps the connection open while stdin is open. raw supplies a normal request object, assigning an ID if absent.
Transport and envelopes¶
Protocol version 1 uses TCP, UTF-8, and newline delimited JSON: one object followed by \n. \r\n is accepted. TCP read boundaries have no meaning. Messages are compact, and a client may keep a connection open, pipeline requests, and receive unsolicited events between responses. Correlate responses by their non-negative integer id; response order is not guaranteed. A connection’s first request must be session.connect.
{"id":1,"method":"session.connect","params":{"protocolVersion":1,"token":"<token>","client":{"name":"my-tool","version":"1"}}}
Success includes protocolVersion, application, pid, readOnly, defaultWindow, defaultScene, and capabilities. An invalid token returns authentication-failed and closes the connection; a non-v1 version returns unsupported-protocol and closes it. Requests use {"id":17,"method":"ui.query","params":{...}}; params defaults to {}. Success is {"id":17,"result":{...}}. Failure is {"id":17,"error":{"code":"...","message":"...","data":{...}}}; data is optional. Events have no ID: {"method":"watch.changed","params":{...}}.
Every command may have a post-command delay such as "100ms" or "1s". The command runs first; its response or the next batch command waits asynchronously. The normal UI update loop continues. A single delay is limited to 60 seconds. The accepted syntax is a non-negative decimal number followed by ms or s.
Windows, scenes, and handles¶
Handles are opaque: win:* identifies a native window, scene:* a UISceneNode, w:* a widget, and sub:* a watch subscription. Window, scene, and widget IDs are monotonic for the server’s lifetime and valid across client reconnections while the objects live. Destroyed objects invalidate their handles. Subscription IDs belong to one connection and are released on disconnect. Handles never contain pointers and do not survive server restarts.
Each selector executes in exactly one scene. An omitted scene means the stable defaultScene from the handshake, never the currently active scene. If that scene is destroyed, omission returns default-scene-unavailable. A scene determines its native window; a widget handle determines its scene and window. ui.contexts reports all discovered top-level and nested scenes. A UIWebView document is a separate scene and is classified webview, with its owner widget and parent scene. Other embedded scenes are classified nested.
For example, discover the WebView in its parent scene, then query its document scene:
{"id":10,"method":"ui.query","params":{"selector":"#preview"}}
The returned WebView summary includes "documentScene":"scene:2". Then:
{"id":11,"method":"ui.query","params":{"scene":"scene:2","selector":"table > tr > td.problem"}} {"id":12,"method":"ui.inspect","params":{"handle":"w:95","properties":["geometry.position","geometry.size","css.display","css.font-size"]}}
#preview p in the parent scene never enters the HTML document. ui.contexts also reveals secondary native windows and their scenes; pass the secondary scene:* to query or act there.
Method reference¶
All examples below omit id where the surrounding explanation is enough; real requests always require it.
Method |
Parameters and defaults |
Result |
Notable errors |
|---|---|---|---|
ui.contexts window entries include handle, native ID, title, pixel size and position, and focus. Scene entries include handle, window, kind, parent scene, owner widget, and root widget; WebView scenes include URI. The revision increases on observed topology changes. Create/destroy events are checked periodically while clients are connected; ui.contexts is the authoritative current snapshot.
These compact request/response pairs show each method’s envelope. Handle numbers and geometry are illustrative; methods also accept the optional fields in the table above. Errors use the shared error envelope.
{"id":1,"method":"session.connect","params":{"protocolVersion":1,"token":"secret"}} {"id":1,"result":{"protocolVersion":1,"application":"ecode","pid":19382,"readOnly":false,"defaultWindow":"win:1","defaultScene":"scene:1","capabilities":["ui.contexts","ui.query","ui.tree","ui.inspect","ui.focus","ui.screenshot","session.batch","session.nextFrame","watch.properties","events.context-lifecycle","input.click","input.key","input.text"]}} {"id":2,"method":"ui.contexts"} {"id":2,"result":{"revision":0,"defaultWindow":"win:1","defaultScene":"scene:1","windows":[{"handle":"win:1","id":1,"title":"ecode","sizePx":[800,600],"positionPx":[0,0],"focused":true}],"scenes":[{"handle":"scene:1","window":"win:1","kind":"top-level","parentScene":null,"owner":null,"root":"w:1"}]}} {"id":3,"method":"ui.query","params":{"selector":"#panel","properties":["geometry.size"],"limit":1}} {"id":3,"result":{"scene":"scene:1","selector":"#panel","total":1,"offset":0,"returned":1,"truncated":false,"nodes":[{"handle":"w:2","scene":"scene:1","tag":"widget","id":"panel","classes":[],"pseudoClasses":[],"boundsPx":[10,10,100,30],"visible":true,"enabled":true,"focused":false,"properties":{"geometry.size":[100,30]}}]}} {"id":4,"method":"ui.tree","params":{"depth":1,"maxNodes":50}} {"id":4,"result":{"scene":"scene:1","root":"w:1","truncated":false,"nodes":[{"handle":"w:1","scene":"scene:1","tag":"root","id":"","classes":[],"pseudoClasses":[],"parent":null,"children":["w:2"]},{"handle":"w:2","scene":"scene:1","tag":"widget","id":"panel","classes":[],"pseudoClasses":[],"parent":"w:1","children":[]}]}} {"id":5,"method":"ui.inspect","params":{"handle":"w:2","properties":["geometry.size","css.font-size"]}} {"id":5,"result":{"handle":"w:2","scene":"scene:1","properties":{"geometry.size":[100,30],"css.font-size":"14dp"},"unavailable":[]}} {"id":6,"method":"ui.focus"} {"id":6,"result":{"scene":"scene:1","widget":null}} {"id":14,"method":"ui.screenshot","params":{"scene":"scene:2","format":"png"}} {"id":14,"result":{"path":"/tmp/eepp-inspector-19382-<random>.png","format":"png","window":"win:1","scene":"scene:2","rectPx":[100,80,480,320],"sizePx":[480,320]}} {"id":7,"method":"input.click","params":{"handle":"w:2"}} {"id":7,"result":{"target":"w:2","scene":"scene:1","pointPx":[60,25]}} {"id":8,"method":"input.key","params":{"key":"Enter","action":"press","modifiers":[]}} {"id":8,"result":{"key":"Enter","action":"press"}} {"id":9,"method":"input.text","params":{"text":"hello"}} {"id":9,"result":{"characters":5}} {"id":10,"method":"watch.subscribe","params":{"handle":"w:2","properties":["geometry.size"],"initial":true}} {"id":10,"result":{"subscription":"sub:1","targets":[{"handle":"w:2","values":{"geometry.size":[100,30]}}]}} {"id":11,"method":"watch.unsubscribe","params":{"subscription":"sub:1"}} {"id":11,"result":{"unsubscribed":true}} {"id":12,"method":"session.nextFrame","params":{"count":1}} {"id":12,"result":{"frames":1}} {"id":13,"method":"session.batch","params":{"commands":[{"name":"panel","method":"ui.query","params":{"selector":"#panel","limit":1},"return":false},{"name":"size","method":"ui.inspect","params":{"handle":{"$ref":"panel#/nodes/0/handle"},"properties":["geometry.size"]}}]}} {"id":13,"result":{"results":{"size":{"handle":"w:2","scene":"scene:1","properties":{"geometry.size":[100,30]},"unavailable":[]}}}}
ui.query uses eepp’s CSS selector matching and returns an empty nodes array for no matches. Summary fields include handle, scene, tag, ID, classes, active pseudo-classes, pixel bounds, visible, enabled, focused, and optional text. Text is exposed for text controls, UITextNode, UITextSpan, and UIRichText; rich-text containers concatenate descendant source text without forcing layout. Summary text is limited to 256 Unicode characters. When properties is provided, each returned node also has the requested inspection values, avoiding one ui.inspect call per match. ui.tree does not cross a nested scene boundary; a WebView entry instead includes documentScene.
ui.screenshot captures the native window containing the selected scene. With no target it captures the default scene’s full native window. An explicit window captures that whole window; an explicit scene crops to its visible world bounds, including a WebView document viewport. rect overrides that crop and uses top-left native-window pixel coordinates. It must fit entirely inside the window and have positive width and height. Supported formats are png, jpg, bmp, tga, qoi, and webp; eepp’s case-insensitive image extension parser also accepts jpeg and jfif as aliases for jpg. The response uses the canonical extension. The server renders the selected window after the current UI update, saves the image in the OS temporary directory, and returns its absolute path; it does not send image bytes over NDJSON. The caller is responsible for deleting the file when finished. The path is useful to a local agent; remote clients must separately retrieve the file, for example over SSH. Screenshot observation is available in read-only mode and can be used inside session.batch.
Single-widget methods never pick the first of multiple selector matches: zero matches return target-not-found, and multiple matches return target-ambiguous. input.click uses the window input route and scene hit testing; clipped, covered, invisible, disabled, or off-window targets can return target-not-interactable. It does not scroll a target into view. input.key accepts names understood by eepp’s Input::getKeyFromName() plus Enter as an alias for Return, and modifiers Ctrl, Shift, Alt, Meta; action may be press, down, or up. input.text sends Unicode text input to the current focus path and does not focus a widget first. Read-only mode omits input capabilities and returns permission-denied for these methods.
Properties¶
ui.inspect defaults to identity.*, geometry.*, and state.*. Explicit names and the wildcards identity.*, geometry.*, state.*, content.*, css.*, and * are accepted. css.* expands to CSS properties implemented by that widget. Unknown or unavailable properties appear in unavailable rather than failing the whole inspection. Long text properties are bounded to 4096 Unicode characters by default. maxStringLength can raise that limit to 65536; truncatedProperties lists fields cut short. Watches use the 4096 character default.
Group |
Names and values |
Event Example ===== =======
Code Meaning ==== ================================================================
Malformed JSON. Missing/invalid request envelope, ID, or params. Inspector method before connection authentication. Token rejected. Client requested a version other than 1. Unknown method. A method argument is missing, invalid, or beyond a limit. An action was requested in read-only mode. A handle does not identify a live object of that kind. The original default scene was destroyed. Selector is empty or rejected by selector validation. Single-target selector matched zero or multiple widgets. Click point cannot reach the target. Watch property cannot be read. The scene/window has no usable input context. No drawable window/scene area, or image capture/save failed. Invalid command reference or JSON Pointer. A batch step failed; A request line exceeded 1 MiB; the server closes the connection. An unexpected command error was caught.