kinax

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 8, 2026 License: MIT Imports: 17 Imported by: 0

README

kinax-go

Pure-Go binding to the macOS Accessibility (AX) API.

Navigate and manipulate the system-wide UI tree — inspect running applications, find buttons by semantic identity, read text field contents, click elements without hardcoded pixel coordinates.

Single binary, zero cgo, go install-able. Built on AXUIElement via purego + an embedded companion dylib — the same pattern as sckit-go, kinrec, and input-go.

kinax-go is part of KinKit — the pure-Go macOS system library family powering the LocalKin agent swarm. It pairs with input-go: kinax-go sees the UI, input-go moves it.

go install github.com/LocalKinAI/kinax-go/cmd/kinax@latest

kinax tree --bundle com.apple.Safari --depth 4
kinax find AXButton --bundle com.apple.Safari
kinax click "Continue"
kinax at-point 200 100

Features

  • Element tree navigationChildren, Parent, Windows, FocusedWindow, FocusedElement, AttributeElement.
  • Typed attribute readersAttribute (string), AttributeInt, AttributeBool, AttributePoint (CGPoint), AttributeSize (CGSize), AttributeElements (array).
  • Attribute + action introspectionAttributeNames, ActionNames return live lists from the element.
  • Semantic searchFindFirst / FindAll with composable matchers (MatchRole, MatchTitle, MatchTitleContains, MatchIdentifier, MatchAll, MatchAny).
  • Hit testingElementAtPoint(x, y) returns whatever AX element is at the given global coords (what Accessibility Inspector shows when you hover).
  • App targetingFocusedApplication, ApplicationByPID, ApplicationByBundleID, SystemWide.
  • Action + writePerform("AXPress"), SetString, SetBool.
  • No cgo: downstream projects stay pure Go. The ObjC companion dylib is //go:embedded (~70 KB universal arm64+x86_64) and extracted to ~/Library/Caches on first call.

Install

# CLI
go install github.com/LocalKinAI/kinax-go/cmd/kinax@latest

# Library
go get github.com/LocalKinAI/kinax-go

Requires macOS 12+ and Go 1.22+.

Permission

macOS requires the invoking binary to be listed in System Settings → Privacy & Security → Accessibility for AX* calls to succeed. Unlike input-go (which silently no-ops without permission), kinax-go returns real errors — every accessor will fail until permission is granted.

if err := kinax.RequireTrust(); err != nil {
    kinax.PromptTrust() // shows system dialog
    log.Fatal("grant Accessibility permission, then rerun")
}

Library usage

package main

import (
    "fmt"
    "log"

    "github.com/LocalKinAI/kinax-go"
)

func main() {
    if err := kinax.Load(); err != nil {
        log.Fatal(err)
    }
    if err := kinax.RequireTrust(); err != nil {
        log.Fatal(err)
    }

    // Attach to Safari (must be running)
    app, err := kinax.ApplicationByBundleID("com.apple.Safari")
    if err != nil { log.Fatal(err) }
    defer app.Close()

    // Walk all windows
    wins, _ := app.Windows()
    for _, w := range wins {
        t, _ := w.Title()
        fmt.Println("window:", t)
        w.Close()
    }

    // Find every text field, print its value
    fields := app.FindAll(kinax.MatchRole(kinax.RoleTextField), 30)
    for _, f := range fields {
        v, _ := f.Value()
        fmt.Println("field:", v)
        f.Close()
    }

    // Click the "New Tab" button
    if btn, ok := app.FindFirst(kinax.MatchTitle("New Tab"), 20); ok {
        defer btn.Close()
        btn.Perform(kinax.ActionPress)
    }
}

CLI usage

# Dump the AX tree of the focused app (default target)
kinax tree --depth 4

# Dump a specific app's tree
kinax tree --bundle com.apple.Safari --depth 5
kinax tree --pid 1234

# Inspect one attribute of the app element
kinax attr AXTitle --bundle com.apple.Safari

# List every attribute or action the app exposes
kinax attrs --focused
kinax actions --focused

# Find every button (optionally with a specific title)
kinax find AXButton --bundle com.apple.Safari
kinax find AXButton "New Tab" --bundle com.apple.Safari --depth 25

# Click a button by title
kinax click "Continue"
kinax click "OK" --role AXButton --bundle com.apple.SystemPreferences

# Hit-test a screen coordinate
kinax at-point 200 100
# → AXMenuBar    Apple

# Permission
kinax trust              # prints 1 or 0
kinax trust --prompt     # shows system dialog

Memory ownership

Every *Element returned by kinax-go wraps a retained CFTypeRef. The caller must call (*Element).Close — forgetting to leaks a handle for the process lifetime.

Traversal helpers (FindFirst, FindAll) return fresh handles; any siblings they walk past are closed automatically. You only need to close what's returned to you.

// Correct
wins, _ := app.Windows()
for _, w := range wins {
    defer w.Close()
}

// Also correct — find returns a fresh handle
if btn, ok := app.FindFirst(kinax.MatchTitle("OK"), 20); ok {
    defer btn.Close()
    btn.Perform(kinax.ActionPress)
}

Combining with input-go

kinax finds the element; input drives the cursor there. This is the natural UI-automation loop:

import (
    "github.com/LocalKinAI/kinax-go"
    "github.com/LocalKinAI/input-go"
)

btn, _ := app.FindFirst(kinax.MatchTitle("Save"), 20)
defer btn.Close()

pos, _ := btn.Position()
size, _ := btn.Size()
cx := float64(pos.X + size.X/2)
cy := float64(pos.Y + size.Y/2)

input.MoveSmooth(ctx, cx, cy, 300*time.Millisecond)
input.ClickAt(ctx, cx, cy)

For many cases btn.Perform(kinax.ActionPress) is enough — clicking via AX is faster and doesn't require Accessibility permission on the binary that runs input.Click (though it does require it on the one that runs kinax.Perform). Use input-go for pixel-level drag gestures, hover highlights, and scrolling.

How it works

kinax-go follows the embedded dylib pattern documented in Paper #9 of localkin.dev/papers.

Go code  ─── purego.Dlopen ────► libkinax_sync.dylib (embedded)
                                     │
                                     └──► AXUIElement* APIs
  • objc/kinax_ax.m — ~450 LOC ObjC shim exposing 20 C-ABI functions (kinax_element_attr_string, kinax_element_perform, etc.).
  • internal/dylib/libkinax_sync.dylib — universal Mach-O, committed.
  • Opaque uintptr handles for elements; CFRetain on the ObjC side, CFRelease via kinax_element_release when Go Closes.
  • JSON encoding for list attributes (attribute names, action names) — avoids shipping a full CF→Go type system across the FFI.

Known limitations (v0.1)

  • macOS only. The Accessibility API is macOS-specific — no cross-platform ambitions.
  • Read-heavy API. Writing is limited to string and bool attribute sets. CGPoint/CGSize/CGRect AXValue setters (e.g. to move a window programmatically) are deferred to v0.2.
  • No observers / notifications. AXObserverRef + kAXFocusedWindowChangedNotification etc. are planned for v0.2 — they enable agents to react to UI changes rather than polling.
  • Numeric attributes only as int64. Float-valued attributes (AXValue on sliders) currently string-stringify. Dedicated AttributeFloat planned for v0.2.
  • Single main thread assumption for some CF calls. In practice kinax works from any goroutine because we don't use CFRunLoop.
  • Tested only on macOS 26.3 arm64 so far; Intel + macOS 14/15 verification pending CI.

Roadmap

  • v0.2 — AXObserver subscription (OnNotification callback), more typed setters (SetPoint, SetSize), AttributeFloat.
  • v0.3 — helpers for common idioms: WaitForWindow(bundleID, title, timeout), TypeInFieldLabeled(app, label, text), "did the UI change" snapshots.
  • Cross-app automation recipes — Safari URL read, screenshot a specific window via sckit-go + AXFrame, etc.

Contributing

git clone https://github.com/LocalKinAI/kinax-go
cd kinax-go
make dylib            # rebuild universal Mach-O after ObjC changes
make test             # unit tests (no Accessibility permission needed)
make test-integration # requires Accessibility permission
make lint             # go vet + staticcheck + golangci-lint

License

MIT. See LICENSE.

See also

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

View Source
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.

View Source
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.

View Source
const (
	ActionPress           = "AXPress"
	ActionIncrement       = "AXIncrement"
	ActionDecrement       = "AXDecrement"
	ActionConfirm         = "AXConfirm"
	ActionCancel          = "AXCancel"
	ActionShowMenu        = "AXShowMenu"
	ActionRaise           = "AXRaise"
	ActionShowAlternateUI = "AXShowAlternateUI"
	ActionShowDefaultUI   = "AXShowDefaultUI"
	ActionPick            = "AXPick"
	// AXScrollToVisible asks scroll-area / table / outline containers
	// to bring the receiver into view. Most table / list elements
	// honor this; some custom-drawn views ignore it.
	ActionScrollToVisible = "AXScrollToVisible"
)

Standard AX actions (stable constants from AXActionConstants.h).

View Source
const (
	AttrMenuItemCmdChar       = "AXMenuItemCmdChar"
	AttrMenuItemCmdModifiers  = "AXMenuItemCmdModifiers"
	AttrMenuItemCmdVirtualKey = "AXMenuItemCmdVirtualKey"
	AttrMenuItemMarkChar      = "AXMenuItemMarkChar"
)

Menu-item shortcut attribute names (stable strings from <HIServices/AXAttributeConstants.h>; the ones AppKit menu items expose for keyboard equivalents). Use these with Element.Attribute / Element.AttributeInt when reading shortcuts.

View Source
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.

View Source
const Version = "0.3.0"

Version is the semantic-version tag of this package.

Variables

View Source
var DylibPath = ""

DylibPath is an optional override for the location of libkinax_sync.dylib. Default (empty): extract embedded copy to cache directory.

View Source
var ErrClosed = errors.New("kinax: element closed")

ErrClosed is returned when a method is called on an Element after Close.

View Source
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).

View Source
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).

View Source
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.

View Source
var ErrObserverClosed = errors.New("kinax: observer is closed")

ErrObserverClosed is returned from Next() / Subscribe() if Close() has already been called.

View Source
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 Load

func Load() error

Load explicitly loads the companion dylib. Idempotent.

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.

func Trusted

func Trusted() bool

Trusted returns true if the current process has Accessibility permission. Does not prompt.

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

func ApplicationByBundleID(bundleID string) (*Element, error)

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

func ApplicationByPID(pid int) (*Element, error)

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

func ElementAtPoint(x, y float64) (*Element, error)

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

func FocusedApplication() (*Element, error)

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

func SystemWide() (*Element, error)

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

func (e *Element) ActionNames() ([]string, error)

ActionNames returns all action names this element responds to.

func (*Element) Attribute

func (e *Element) Attribute(name string) (string, error)

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

func (e *Element) AttributeBool(name string) (bool, error)

AttributeBool reads a bool-valued attribute.

func (*Element) AttributeElement

func (e *Element) AttributeElement(name string) (*Element, error)

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

func (e *Element) AttributeElements(name string) ([]*Element, error)

AttributeElements reads an array-of-elements attribute (e.g. AXChildren, AXWindows). Each returned Element must be closed by the caller.

func (*Element) AttributeInt

func (e *Element) AttributeInt(name string) (int64, error)

AttributeInt reads a number-valued attribute as int64.

func (*Element) AttributeNames

func (e *Element) AttributeNames() ([]string, error)

AttributeNames returns all attribute names this element exposes. Useful for dumping the full AX state of an element during debugging.

func (*Element) AttributePoint

func (e *Element) AttributePoint(name string) (image.Point, error)

AttributePoint reads a CGPoint-valued attribute (e.g. AXPosition).

func (*Element) AttributeSize

func (e *Element) AttributeSize(name string) (image.Point, error)

AttributeSize reads a CGSize-valued attribute (e.g. AXSize).

func (*Element) Children

func (e *Element) Children() ([]*Element, error)

Children returns the element's direct children. Caller must Close each.

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

func (e *Element) Description() (string, error)

Description returns the element's AXDescription.

func (*Element) Enabled

func (e *Element) Enabled() (bool, error)

Enabled reports whether the element is enabled (vs. grayed out).

func (*Element) FindAll

func (e *Element) FindAll(m Matcher, maxDepth int) []*Element

FindAll collects every descendant of e matching `m` (excluding e). Caller must Close every returned element.

func (*Element) FindFirst

func (e *Element) FindFirst(m Matcher, maxDepth int) (*Element, bool)

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) Focused

func (e *Element) Focused() (bool, error)

Focused reports whether the element currently has keyboard focus.

func (*Element) FocusedElement

func (e *Element) FocusedElement() (*Element, error)

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

func (e *Element) FocusedWindow() (*Element, error)

FocusedWindow returns the app's currently-focused window.

func (*Element) GetMany added in v0.2.0

func (e *Element) GetMany(attrs ...string) (map[string]any, error)

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:

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

func (e *Element) Handle() uintptr

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

func (e *Element) Identifier() (string, error)

Identifier returns the element's AXIdentifier — the stable string ID set by the app developer for automation.

func (*Element) MainWindow

func (e *Element) MainWindow() (*Element, error)

MainWindow returns the app's main window (usually the frontmost). Returns (nil, nil) if the app has no main window right now.

func (*Element) MenuItemShortcut added in v0.4.0

func (e *Element) MenuItemShortcut() (char string, mods int, vk int, err error)

MenuItemShortcut reads a menu item's keyboard equivalent. Returns the character (e.g. "s"), the modifier bitfield, and the virtual keycode (set when the shortcut is a non-character key like F-keys or arrows). Apple's encoding quirk: bit 3 of mods means "no ⌘" (i.e. ⌘ is implicit when bit 3 is clear).

char, mods, vk, err := el.MenuItemShortcut()
// e.g. char="s" mods=0 vk=0  → ⌘S
// e.g. char=""  mods=0 vk=122 → ⌘F1 (virtual key 122)

Use this on AXMenuItem elements found via [NavigateMenu]'s tree walk or directly via Element.FindFirst. Returns empty/zero values when the element has no keyboard equivalent (most non-leaf menu items don't).

func (*Element) NavigateMenu added in v0.4.0

func (e *Element) NavigateMenu(path string) error

NavigateMenu walks an application's macOS menu bar by string path and triggers the leaf menu item via Element.Perform(ActionPress). Path separators accepted: " > ", ">", " / ", "/", " → ", "→".

app, _ := kinax.FocusedApplication()
defer app.Close()
if err := app.NavigateMenu("Format > Cell > Conditional Highlighting"); err != nil { ... }

Implementation walks AXMenuBar → AXMenuBarItem → AXMenu → AXMenuItem, pressing each ancestor along the way to open submenus. An 80ms settle is inserted between submenu opens to give AppKit time to populate the AXMenu's children before we look up the next path step.

Errors:

  • "no AXMenuBar": app is sandboxed without standard AppKit menus, or it's not a GUI app.
  • "no child titled X at step N/M": one of the path components didn't match any menu / item title at that level. Check the path against macOS's actual menu structure (Help → search bar shows all items in the menu bar of the focused app).

macOS menu items can have alternate forms reachable only with ⌥ held. Those don't show up in this walk because AX exposes them as AXAlternateUIVisible elements only while ⌥ is pressed; for those, use [Hotkey] with the literal modifier+key from the menu's listed shortcut.

func (*Element) Parent

func (e *Element) Parent() (*Element, error)

Parent returns the element's parent, or nil if it's a root.

func (*Element) Perform

func (e *Element) Perform(action string) error

Perform fires the named action (e.g. AXPress). Returns nil on success or an error with the underlying AXError code.

func (*Element) Position

func (e *Element) Position() (image.Point, error)

Position returns the element's top-left corner in global screen coordinates.

func (*Element) Role

func (e *Element) Role() (string, error)

Role returns the element's AXRole (e.g. "AXButton"). Empty if absent.

func (*Element) SetBool

func (e *Element) SetBool(attr string, value bool) error

SetBool sets a bool-valued attribute (e.g. AXFocused=true to request keyboard focus).

func (*Element) SetString

func (e *Element) SetString(attr, value string) error

SetString sets a string-valued attribute. Typically used to set AXValue on a text field:

field.SetString(kinax.AttrValue, "new text")

func (*Element) Size

func (e *Element) Size() (image.Point, error)

Size returns the element's width and height in pixels.

func (*Element) Subrole

func (e *Element) Subrole() (string, error)

Subrole returns the element's AXSubrole (e.g. "AXCloseButton").

func (*Element) Title

func (e *Element) Title() (string, error)

Title returns the element's AXTitle.

func (*Element) Value

func (e *Element) Value() (string, error)

Value returns the element's AXValue as a string (works for text fields, sliders with a stringified number, etc.).

func (*Element) Windows

func (e *Element) Windows() ([]*Element, error)

Windows returns the top-level windows of an app element. Convenience for (*Element).AttributeElements(AttrWindows).

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

type Matcher func(e *Element) bool

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 MatchAll

func MatchAll(ms ...Matcher) Matcher

MatchAll combines matchers with AND.

func MatchAny

func MatchAny(ms ...Matcher) Matcher

MatchAny combines matchers with OR.

func MatchIdentifier

func MatchIdentifier(id string) Matcher

MatchIdentifier selects elements with AXIdentifier == `id`. App developers set this for automation-stable lookup — prefer it over titles when available.

func MatchRole

func MatchRole(role string) Matcher

MatchRole returns a Matcher that selects elements whose AXRole equals `role`. Use the Role* constants:

app.FindFirst(kinax.MatchRole(kinax.RoleButton), 20)

func MatchRoleAndTitle

func MatchRoleAndTitle(role, title string) Matcher

MatchRoleAndTitle selects elements matching both role and exact title.

func MatchTitle

func MatchTitle(title string) Matcher

MatchTitle selects elements whose AXTitle equals `title` exactly.

func MatchTitleContains

func MatchTitleContains(sub string) Matcher

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

func NewObserver(pid int) (*Observer, error)

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

func (o *Observer) Events(ctx context.Context, pollInterval time.Duration) <-chan Event

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

func (o *Observer) Next(timeout time.Duration) (*Event, error)

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

func (o *Observer) PID() int

PID returns the process this Observer is bound to. Useful for logs.

func (*Observer) Subscribe added in v0.3.0

func (o *Observer) Subscribe(elem *Element, notifications ...string) error

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.

func (*Observer) Unsubscribe added in v0.3.0

func (o *Observer) Unsubscribe(elem *Element, notification string) error

Unsubscribe removes a previous subscription. Safe to call on a notification that was never subscribed (returns nil).

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL