Documentation
¶
Overview ¶
Package kinax is a pure-Go binding to the macOS Accessibility (AX) API.
kinax lets Go programs read and manipulate the system-wide UI tree — inspecting running applications, finding buttons, reading text field contents, clicking on elements by semantic identity rather than pixel coordinates. It's the foundation for UI automation agents, screen readers, and accessibility-aware tools.
Quick start ¶
ctx := context.Background()
if err := kinax.Load(); err != nil { log.Fatal(err) }
if err := kinax.RequireTrust(); err != nil { log.Fatal(err) }
app, _ := kinax.FocusedApplication()
defer app.Close()
// Walk windows
wins, _ := app.Windows()
for _, w := range wins {
title, _ := w.Title()
fmt.Println(title)
w.Close()
}
// Click a button by title
if btn, ok := app.FindFirst(kinax.MatchTitle("OK")); ok {
btn.Perform(kinax.ActionPress)
btn.Close()
}
Permission ¶
macOS requires the invoking binary to be listed in System Settings → Privacy & Security → Accessibility for AX calls to succeed. Without permission, every call returns an error (unlike `input-go`, where events silently no-op). Use Trusted / PromptTrust / RequireTrust.
Memory ownership ¶
Every Element returned by kinax wraps a retained CFTypeRef. Callers MUST call (*Element).Close to release it. Forgetting to Close leaks a handle for the lifetime of the process — on the order of tens of bytes per element, but it adds up if you're walking the UI tree on a timer.
Dylib placement ¶
kinax-go ships a universal (arm64+x86_64) companion dylib via //go:embed. On the first call into the package, the embedded bytes are extracted to ~/Library/Caches/kinax-go/<hash>/libkinax_sync.dylib and Dlopened. Set DylibPath to a non-empty value before the first call if you ship a custom-built or patched dylib.
Index ¶
- Constants
- Variables
- func FrontmostPID() int
- func Load() error
- func PromptTrust() bool
- func RequireTrust() error
- func ResolvedDylibPath() string
- func Trusted() bool
- type Element
- func (e *Element) ActionNames() ([]string, error)
- func (e *Element) Attribute(name string) (string, error)
- func (e *Element) AttributeBool(name string) (bool, error)
- func (e *Element) AttributeElement(name string) (*Element, error)
- func (e *Element) AttributeElements(name string) ([]*Element, error)
- func (e *Element) AttributeInt(name string) (int64, error)
- func (e *Element) AttributeNames() ([]string, error)
- func (e *Element) AttributePoint(name string) (image.Point, error)
- func (e *Element) AttributeSize(name string) (image.Point, error)
- func (e *Element) Children() ([]*Element, error)
- func (e *Element) Close()
- func (e *Element) Description() (string, error)
- func (e *Element) Enabled() (bool, error)
- func (e *Element) FindAll(m Matcher, maxDepth int) []*Element
- func (e *Element) FindFirst(m Matcher, maxDepth int) (*Element, bool)
- func (e *Element) Focused() (bool, error)
- func (e *Element) FocusedElement() (*Element, error)
- func (e *Element) FocusedWindow() (*Element, error)
- func (e *Element) GetMany(attrs ...string) (map[string]any, error)
- func (e *Element) Handle() uintptr
- func (e *Element) Identifier() (string, error)
- func (e *Element) MainWindow() (*Element, error)
- func (e *Element) Parent() (*Element, error)
- func (e *Element) Perform(action string) error
- func (e *Element) Position() (image.Point, error)
- func (e *Element) Role() (string, error)
- func (e *Element) SetBool(attr string, value bool) error
- func (e *Element) SetString(attr, value string) error
- func (e *Element) Size() (image.Point, error)
- func (e *Element) Subrole() (string, error)
- func (e *Element) Title() (string, error)
- func (e *Element) Value() (string, error)
- func (e *Element) Windows() ([]*Element, error)
- type Event
- type Matcher
- type Observer
- func (o *Observer) Close()
- func (o *Observer) Events(ctx context.Context, pollInterval time.Duration) <-chan Event
- func (o *Observer) Next(timeout time.Duration) (*Event, error)
- func (o *Observer) PID() int
- func (o *Observer) Subscribe(elem *Element, notifications ...string) error
- func (o *Observer) Unsubscribe(elem *Element, notification string) error
Constants ¶
const ( AttrRole = "AXRole" AttrSubrole = "AXSubrole" AttrRoleDescription = "AXRoleDescription" AttrTitle = "AXTitle" AttrDescription = "AXDescription" AttrHelp = "AXHelp" AttrValue = "AXValue" AttrValueDescription = "AXValueDescription" AttrPlaceholder = "AXPlaceholderValue" AttrIdentifier = "AXIdentifier" AttrEnabled = "AXEnabled" AttrFocused = "AXFocused" AttrSelected = "AXSelected" AttrVisible = "AXVisible" AttrExpanded = "AXExpanded" AttrMain = "AXMain" // primary window AttrMinimized = "AXMinimized" AttrFullscreen = "AXFullscreen" // Geometry AttrPosition = "AXPosition" // CGPoint AttrSize = "AXSize" // CGSize AttrFrame = "AXFrame" // CGRect (not all elements expose this) // Tree navigation AttrParent = "AXParent" AttrChildren = "AXChildren" AttrChildrenInOrder = "AXChildrenInNavigationOrder" AttrWindows = "AXWindows" AttrMainWindow = "AXMainWindow" AttrFocusedWindow = "AXFocusedWindow" AttrFocusedElement = "AXFocusedUIElement" AttrMenuBar = "AXMenuBar" AttrTopLevelElement = "AXTopLevelUIElement" // Containers AttrRows = "AXRows" AttrColumns = "AXColumns" AttrCell = "AXCell" AttrTabs = "AXTabs" AttrSelectedText = "AXSelectedText" AttrNumberOfCharacters = "AXNumberOfCharacters" )
Standard AXUIElement attribute names. These are stable strings from <HIServices/AXAttributeConstants.h>. Use them rather than string literals so typos fail at compile time.
const ( RoleApplication = "AXApplication" RoleWindow = "AXWindow" RoleButton = "AXButton" RoleCheckBox = "AXCheckBox" RoleRadioButton = "AXRadioButton" RoleTextField = "AXTextField" RoleTextArea = "AXTextArea" RoleStaticText = "AXStaticText" RolePopUpButton = "AXPopUpButton" RoleMenu = "AXMenu" RoleMenuItem = "AXMenuItem" RoleMenuBar = "AXMenuBar" RoleMenuBarItem = "AXMenuBarItem" RoleGroup = "AXGroup" RoleList = "AXList" RoleTable = "AXTable" RoleRow = "AXRow" RoleCell = "AXCell" RoleColumn = "AXColumn" RoleScrollArea = "AXScrollArea" RoleScrollBar = "AXScrollBar" RoleSlider = "AXSlider" RoleToolbar = "AXToolbar" RoleImage = "AXImage" RoleLink = "AXLink" RoleTabGroup = "AXTabGroup" RoleSheet = "AXSheet" RoleSplitGroup = "AXSplitGroup" RoleOutline = "AXOutline" RoleWebArea = "AXWebArea" )
Standard AX role values (stable constants from AXRoleConstants.h). Use with Element.Role comparisons.
const ( ActionPress = "AXPress" ActionIncrement = "AXIncrement" ActionDecrement = "AXDecrement" ActionConfirm = "AXConfirm" ActionCancel = "AXCancel" ActionShowMenu = "AXShowMenu" ActionRaise = "AXRaise" ActionShowAlternateUI = "AXShowAlternateUI" ActionShowDefaultUI = "AXShowDefaultUI" ActionPick = "AXPick" )
Standard AX actions (stable constants from AXActionConstants.h).
const ( NotifMainWindowChanged = "AXMainWindowChanged" NotifFocusedWindowChanged = "AXFocusedWindowChanged" NotifFocusedUIElementChanged = "AXFocusedUIElementChanged" NotifWindowCreated = "AXWindowCreated" NotifWindowResized = "AXWindowResized" NotifWindowMoved = "AXWindowMoved" NotifWindowMiniaturized = "AXWindowMiniaturized" NotifWindowDeminiaturized = "AXWindowDeminiaturized" NotifApplicationActivated = "AXApplicationActivated" NotifApplicationDeactivated = "AXApplicationDeactivated" NotifValueChanged = "AXValueChanged" NotifTitleChanged = "AXTitleChanged" NotifSelectedTextChanged = "AXSelectedTextChanged" NotifSelectedChildrenChanged = "AXSelectedChildrenChanged" NotifMenuOpened = "AXMenuOpened" NotifMenuClosed = "AXMenuClosed" NotifAnnouncementRequested = "AXAnnouncementRequested" )
Standard AX notification names. Use these constants rather than string literals so typos fail at compile time. The full list is in <HIServices/AXNotificationConstants.h>; this is the subset most agent use-cases need.
const Version = "0.3.0"
Version is the semantic-version tag of this package.
Variables ¶
var DylibPath = ""
DylibPath is an optional override for the location of libkinax_sync.dylib. Default (empty): extract embedded copy to cache directory.
var ErrClosed = errors.New("kinax: element closed")
ErrClosed is returned when a method is called on an Element after Close.
var ErrInvalidType = errors.New("kinax: attribute has wrong type")
ErrInvalidType is returned when an attribute exists but has the wrong type for the requested accessor (e.g. calling AttributeInt on a string attribute).
var ErrNotFound = errors.New("kinax: not found")
ErrNotFound is returned when a requested element, attribute, or app isn't present. Callers typically want to distinguish this from a real error (e.g. permission denied).
var ErrNotTrusted = errors.New("kinax: accessibility permission not granted")
ErrNotTrusted is returned when the current process lacks Accessibility permission. This is the dominant error shape for kinax — virtually every call fails without permission.
var ErrObserverClosed = errors.New("kinax: observer is closed")
ErrObserverClosed is returned from Next() / Subscribe() if Close() has already been called.
var ErrObserverTimeout = errors.New("kinax: observer next: timeout")
ErrObserverTimeout is returned from Next() when no event arrived within the timeout. Callers typically loop on this.
Functions ¶
func FrontmostPID ¶
func FrontmostPID() int
FrontmostPID returns the PID of the currently-active application. Returns 0 if none (rare — happens during login and app transitions).
func PromptTrust ¶
func PromptTrust() bool
PromptTrust triggers the system "wants to control your computer" dialog if permission has not been granted. Returns the resulting trust state (true if already granted, false otherwise — including the common case where the user hasn't yet responded to the dialog).
func RequireTrust ¶
func RequireTrust() error
RequireTrust returns nil if Accessibility permission is granted, or ErrNotTrusted otherwise. Convenience for callers that want a hard fail at startup rather than opaque failures deep in a traversal.
func ResolvedDylibPath ¶
func ResolvedDylibPath() string
ResolvedDylibPath returns the path Load used (or would use) to Dlopen the dylib. Intended for diagnostics.
Types ¶
type Element ¶
type Element struct {
// contains filtered or unexported fields
}
Element is a reference to a single AXUIElement in the system UI tree. Elements are opaque handles onto a retained Core Foundation object — callers MUST call Element.Close when done to avoid leaking.
Element is safe for concurrent use by multiple goroutines; all AX API calls are thread-safe per Apple docs.
func ApplicationByBundleID ¶
ApplicationByBundleID returns the Element for the first running app with the given bundle identifier (e.g. "com.apple.Safari"). Returns ErrNotFound if no such app is running. Caller must Close.
func ApplicationByPID ¶
ApplicationByPID returns the AX Element for the running application with the given Unix PID. Returns a valid Element even if no such process exists — subsequent AX calls on it will simply fail. Caller must Close.
func ElementAtPoint ¶
ElementAtPoint returns the AX element at the given global screen coordinates. This is how you implement "inspect element under cursor" (hold ⌥ hover over anything → show its AX info). Returns ErrNotFound if no element is at the point (e.g. on the desktop wallpaper in some configurations). Caller must Close.
func FocusedApplication ¶
FocusedApplication returns the Element for the currently active (frontmost) application. Returns ErrNotFound if there is none — this can happen briefly during login screens and app transitions. Caller must Close.
func SystemWide ¶
SystemWide returns an Element representing the system-wide AX root. Useful primarily to query AXFocusedApplication or to hit-test points. Caller must Close.
func (*Element) ActionNames ¶
ActionNames returns all action names this element responds to.
func (*Element) Attribute ¶
Attribute reads a string-valued attribute. The return convention:
- empty string + nil error → attribute absent OR value is empty string
- non-empty string + nil error → attribute present with value
- "" + non-nil error → real failure (permission, etc.)
This collapses "absent" and "empty" into one case; if you need to distinguish, use Element.AttributeNames first.
func (*Element) AttributeBool ¶
AttributeBool reads a bool-valued attribute.
func (*Element) AttributeElement ¶
AttributeElement reads an element-valued attribute (e.g. AXFocusedWindow, AXParent) and returns a new *Element that the caller must Close. Returns (nil, nil) if the attribute is absent — not an error, because "no focused window" is a perfectly normal state.
func (*Element) AttributeElements ¶
AttributeElements reads an array-of-elements attribute (e.g. AXChildren, AXWindows). Each returned Element must be closed by the caller.
func (*Element) AttributeInt ¶
AttributeInt reads a number-valued attribute as int64.
func (*Element) AttributeNames ¶
AttributeNames returns all attribute names this element exposes. Useful for dumping the full AX state of an element during debugging.
func (*Element) AttributePoint ¶
AttributePoint reads a CGPoint-valued attribute (e.g. AXPosition).
func (*Element) AttributeSize ¶
AttributeSize reads a CGSize-valued attribute (e.g. AXSize).
func (*Element) Close ¶
func (e *Element) Close()
Close releases the underlying CFTypeRef. Safe to call multiple times; subsequent calls are no-ops. After Close, all other methods return ErrClosed.
func (*Element) Description ¶
Description returns the element's AXDescription.
func (*Element) FindAll ¶
FindAll collects every descendant of e matching `m` (excluding e). Caller must Close every returned element.
func (*Element) FindFirst ¶
FindFirst walks the descendants of e (depth-first, excluding e itself) and returns the first match. Traversal is bounded by `maxDepth` to prevent runaway on pathological trees.
The returned Element is a fresh handle owned by the caller — Close it when done. Returns (nil, false) if no match.
Note: e itself is NOT considered a match candidate. This keeps ownership semantics clean — the caller's existing handle is never returned, so there's no risk of a double-Close.
func (*Element) FocusedElement ¶
FocusedElement returns the element within an app (or window) that currently has keyboard focus. For the system-wide element, this is the globally-focused UI element across all apps.
func (*Element) FocusedWindow ¶
FocusedWindow returns the app's currently-focused window.
func (*Element) GetMany ¶ added in v0.2.0
GetMany fetches multiple scalar attribute values in a single AX IPC round-trip via AXUIElementCopyMultipleAttributeValues. The returned map's values are the same JSON-decoded shapes the Apple AX API produces — string, float64 (json.Number), bool, or json.RawMessage for nested structures the caller chose to keep raw. Missing or unsupported attributes are simply absent from the result map.
Why use this:
- One IPC instead of N. A tree dump that previously paid an IPC round-trip per (node × attribute) pair now pays one per node. Measured 2-5× speedup on dense Electron / iWork apps.
- Atomicity within the batch. The fetched values are a coherent snapshot of the element at one point in time, not a sequence of N reads with arbitrary state changes between them.
What's *not* returned by GetMany:
- Element-valued attributes (AXChildren, AXMainWindow, AXFocusedWindow, AXParent). The C-level multi-fetch can't return handle-typed values usefully — they'd lose ownership semantics. Use Element.AttributeElement / Element.AttributeElements instead for those names.
- AXValue point/size/rect values come back as descriptions ("CGPoint {x=0,y=0}"). Use Element.AttributePoint / Element.AttributeSize for structured access.
Example:
attrs, err := el.GetMany(kinax.AttrRole, kinax.AttrTitle, kinax.AttrEnabled)
if err != nil { return err }
role := attrs[kinax.AttrRole].(string)
title := attrs[kinax.AttrTitle].(string)
enabled := attrs[kinax.AttrEnabled].(bool)
func (*Element) Handle ¶
Handle returns the opaque pointer value — useful for identity comparisons (though two handles may point to the same element in some cases — prefer comparing semantic identity via Title/Role).
func (*Element) Identifier ¶
Identifier returns the element's AXIdentifier — the stable string ID set by the app developer for automation.
func (*Element) MainWindow ¶
MainWindow returns the app's main window (usually the frontmost). Returns (nil, nil) if the app has no main window right now.
func (*Element) Perform ¶
Perform fires the named action (e.g. AXPress). Returns nil on success or an error with the underlying AXError code.
func (*Element) Position ¶
Position returns the element's top-left corner in global screen coordinates.
func (*Element) SetBool ¶
SetBool sets a bool-valued attribute (e.g. AXFocused=true to request keyboard focus).
func (*Element) SetString ¶
SetString sets a string-valued attribute. Typically used to set AXValue on a text field:
field.SetString(kinax.AttrValue, "new text")
type Event ¶ added in v0.3.0
type Event struct {
Notification string // e.g. "AXFocusedWindowChanged"
Element *Element // the element the notification fired on (often a window)
Timestamp time.Time // when the dylib received the callback
}
Event is one AX notification fired by the system, marshaled from the dylib's worker thread to Go's caller. The Element handle is freshly CFRetain'd by the dylib — caller owns it and MUST Close() to release.
type Matcher ¶
Matcher is a predicate over an Element, used by Element.FindFirst / Element.FindAll to select elements during tree traversal.
A Matcher may freely call accessors (Role, Title, attributes). It MUST NOT Close the element — ownership stays with the traversal.
func MatchIdentifier ¶
MatchIdentifier selects elements with AXIdentifier == `id`. App developers set this for automation-stable lookup — prefer it over titles when available.
func MatchRole ¶
MatchRole returns a Matcher that selects elements whose AXRole equals `role`. Use the Role* constants:
app.FindFirst(kinax.MatchRole(kinax.RoleButton), 20)
func MatchRoleAndTitle ¶
MatchRoleAndTitle selects elements matching both role and exact title.
func MatchTitle ¶
MatchTitle selects elements whose AXTitle equals `title` exactly.
func MatchTitleContains ¶
MatchTitleContains selects elements whose title contains `sub` as a substring (case-insensitive).
type Observer ¶ added in v0.3.0
type Observer struct {
// contains filtered or unexported fields
}
Observer subscribes to AX notifications (focus changes, value edits, window creates, etc.) on a per-pid scope. Each Observer owns a dedicated worker thread inside the embedded dylib that runs a CFRunLoop — Apple's AX API is thread-pinned, so we can't share one across goroutines.
Lifecycle:
obs, err := kinax.NewObserver(pid)
defer obs.Close()
obs.Subscribe(elem, kinax.NotifFocusedWindowChanged, kinax.NotifValueChanged)
// Drain events into a channel:
events := obs.Events(ctx)
for ev := range events {
fmt.Println(ev.Notification, ev.Element)
ev.Element.Close() // caller owns the element handle
}
Or block-on-next-with-timeout directly:
ev, err := obs.Next(500 * time.Millisecond)
if errors.Is(err, ErrObserverTimeout) { continue }
Close() is the only safe way to stop the worker thread. Calling Close() twice is a no-op. After Close the Observer is unusable.
func NewObserver ¶ added in v0.3.0
NewObserver creates an Observer for the given process. Returns (nil, error) if the process doesn't exist or AX permission is missing. The caller MUST Close() to free the underlying worker thread + AXObserverRef.
func (*Observer) Close ¶ added in v0.3.0
func (o *Observer) Close()
Close stops the worker thread, drains any pending events (releasing their element handles), and frees the Observer. Idempotent — safe to call multiple times. After Close, all other methods return ErrObserverClosed.
Close blocks until the worker thread has fully exited (typically <50ms — bounded by the time CFRunLoopStop takes effect).
func (*Observer) Events ¶ added in v0.3.0
Events is a goroutine-friendly wrapper around Next: streams every event into a channel until ctx is cancelled or the Observer is closed. The channel is closed when the streamer exits.
pollInterval is the timeout passed to each Next call — small values are responsive but burn CPU; the default (100ms) is a good baseline. Pass <=0 for the default.
CALLER must Close() each Event.Element when done to release the underlying CFTypeRef; otherwise CF memory accumulates.
func (*Observer) Next ¶ added in v0.3.0
Next blocks for up to `timeout` waiting for the next event. If no event arrives within the timeout, returns (nil, ErrObserverTimeout). On success returns the event; the Element inside is a fresh CFRetain'd handle that the caller MUST Close() to release.
timeout=0 polls without blocking (returns immediately if queue is empty). negative timeout is treated as 0.
func (*Observer) PID ¶ added in v0.3.0
PID returns the process this Observer is bound to. Useful for logs.
func (*Observer) Subscribe ¶ added in v0.3.0
Subscribe registers one or more AX notifications to fire on the given element. Common pattern: subscribe at the application root (kinax.ApplicationByPID) for top-level events like AXFocusedWindowChanged, or at a specific element for AXValueChanged.
Errors are aggregated — one bad notification name doesn't abort the rest, but the returned error mentions every failure.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
kinax
command
Command kinax inspects and manipulates the macOS UI tree via the Accessibility API.
|
Command kinax inspects and manipulates the macOS UI tree via the Accessibility API. |
|
internal
|
|
|
dylib
Package dylib embeds the ObjC companion library so downstream users can simply `go get` kinax-go without building C code on their machine.
|
Package dylib embeds the ObjC companion library so downstream users can simply `go get` kinax-go without building C code on their machine. |