accessibility

package module
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

go-macos/accessibility

CI Go Reference coverage license

Move another application's window to a chosen display, from pure Go with CGO_ENABLED=0 — the macOS Accessibility (AX) API through purego, with no cgo anywhere.

w, err := accessibility.FocusedWindow()   // "put THIS application…"
if err != nil {
        return err
}
defer w.Close()

displays, _ := accessibility.Displays()
panel, _ := accessibility.DisplayByID(displays, ribbonPosition3)

res, err := accessibility.MoveToDisplay(w, panel, displays, nil)
if err != nil {
        return err // it did not go there, and this says how far off it landed
}
log.Print(res) // 4915,155 603x505 → wanted 137,211 640x480, got 137,211 640x480

The consumer is go-xrkit/desk, which puts several virtual displays on a 360° ribbon inside AR glasses. The person wearing them says "put this application on ribbon position 3" instead of dragging a window across a display boundary they cannot see the edge of. AX is the only supported way for one process to move another's window on macOS, so AX is what this binds.

A status code proves nothing here

This is the single most important thing about the AX API, and the reason a naive implementation is worse than useless.

A write to kAXPositionAttribute returns kAXErrorSuccess whether or not the application honours it. A window pinned by its own controller, a full-screen window, a sheet, a window whose application is busy — all of them accept the message, return 0, and stay exactly where they were. A caller that checked the return value would be told the move worked.

So the return value is not what this package checks. [Move] writes the position, writes the size, and then reads the window back and compares. If the window is not where it was told, it reports ErrRefused with the measured difference, and does not raise the window either — bringing a window forward that is still on the wrong display is worse than leaving it alone.

accessibility: the window did not move where it was told: 4915,155 603x505 →
wanted 4915.5,155 603x505, got 4915,155 603x505 (NOT MOVED: off by -0.5,0) after 2 attempts

Two attempts is normal, not padding. Setting the size can move the origin — the window server refuses to leave a newly enlarged window hanging off an edge — so the position is written, then the size, then the position again if the read-back disagrees.

A refused size is not a failed move. A terminal that snaps to whole character cells, or any window with a minimum size, keeps the size it had. Result.Moved and Result.Resized are separate, and only the first is an error.

The coordinate space, and the trap in it

Every rectangle here is in global display coordinates: origin at the top-left of the main display, y increasing downwards. That is the space of kAXPositionAttribute, of CGDisplayBounds and of CGWindowListCopyWindowInfo — three independent instruments that agree.

It is not NSScreen's space, whose origin is bottom-left. Mixing the two is the classic way to send a window to a plausible-looking wrong place, and it is why NSScreen is not consulted anywhere in this package. (It is also a cache that is stale in a process with no running NSApp; see go-macos/virtualdisplay.)

TestLiveAXAndTheWindowServerAgree asserts the two views agree across the whole machine, so a drift between the spaces fails a test rather than misplacing a window.

Permission — and who actually holds it

Unlike go-macos/hotkey, which found a permission-free route to system-wide shortcuts, this package needs the Accessibility (TCC) grant. There is no alternative: AX is the API, and AX is gated.

Two calls, and only one of them prompts.

call dialog
Trusted()AXIsProcessTrusted() none. Safe from a status line.
RequestTrust()AXIsProcessTrustedWithOptions with kAXTrustedCheckOptionPrompt yes, always, when not already trusted

The grant does not belong to the executable that asks for it. It belongs to the responsible process, which for a command-line binary is the terminal that launched it. An unbundled Go binary is trusted exactly when its terminal is, will never appear in System Settings under its own name, and cannot be granted the permission on its own. Telling a user to "add this binary in System Settings" when that is impossible is worse than telling them nothing, so Status() works out which case it is and Trust.Advice() says the true thing:

trusted (accessibility.test, unbundled binary)
accessibility.test is trusted for Accessibility, by way of whatever launched it
(Code); nothing to do, but note that the grant belongs to that parent and not to
this binary, so running it from somewhere else may not be trusted.

The responsible process is found with libsystem's responsibility_get_pid_responsible_for_pid, which is SPI, so it is resolved through dlsym and its absence falls back to the parent process rather than failing.

On the bundling question go-macos/hotkey ran into: that package measured CGEventPost being silently refused to an unbundled binary that was already trusted, so it could not tell the grant from the bundling. Here it is answerable, and the answer is different. An unbundled go test binary with AXIsProcessTrusted() == true moves another application's window without complaint — proved below. AX is gated by the TCC grant alone, not by bundling.

-runningApplications is a CACHE — measured

-[NSWorkspace runningApplications] is maintained by notifications delivered on a run loop. A process that never runs one keeps whatever list it had when NSWorkspace was first touched, for the life of the process. Not "briefly stale": permanently wrong.

That was measured, not assumed. Identical program, one line different:

nopump  before: 133 apps, TextEdit: []
          NEVER SAW IT after 15s
pump    before: 133 apps, TextEdit: []
          t=0.5s SAW IT: apps=134 TextEdit=[20745]

Applications() therefore does two things about it. It pumps the current thread's run loop for 50 ms, which is what lets the notification through in an ordinary command-line process; and — because that only reaches the run loop NSWorkspace's source is attached to, and a library cannot dictate which thread it is called on — it completes the list from the window server, which keeps no cache. Any process CGWindowListCopyWindowInfo says owns an on-screen window is included even if NSWorkspace has never heard of it.

FocusedWindow() avoids the cache entirely by going through the system-wide AXUIElement (kAXFocusedApplicationAttributekAXFocusedWindowAttribute) rather than -frontmostApplication.

An autorelease pool needs its thread pinned — measured

NSAutoreleasePool belongs to the thread that created it, and Go moves an unlocked goroutine to another M at any preemption point. A pool created on one thread and drained on another segfaults inside libobjc, and it is not a rare race:

unlocked  SIGSEGV: segmentation violation  PC=0x18c727c60  addr=0x10
locked    survived 300 rounds
unlocked  SIGSEGV: segmentation violation  PC=0x18c727c60  addr=0x10
locked    survived 300 rounds

Every path in this package that allocates an Objective-C object goes through one helper that holds runtime.LockOSThread for the life of the pool. This is worth knowing for anyone using go-macos/objc's AutoreleasePool directly, which does not pin the thread itself.

The API

Trust. Trusted(), RequestTrust() (the only call that prompts), Status() (Trust, error), Trust.Advice().

Displays. Displays() ([]Display, error) from CGGetActiveDisplayList and CGDisplayBounds. DisplayByID, MainDisplay, DisplayFor.

Listing. Applications(), WindowsOf(pid, name), AllWindows(), FocusedWindow(), List() — pid, application name, window title, position, size and the display each window is on. ServerWindows() is the same machine seen through CGWindowListCopyWindowInfo, which needs no grant.

Diagnosis. w.Role() (role, subrole string, err error) and w.Attributes() ([]string, error). An element that answers AXError -25205the element does not have that attribute — will happily list the attributes it DOES have, and name its own role. Without that, a failed read of kAXPosition is a dead end: nothing in the error says whether the element is a window at all. That gap cost an hour of guessing once, and it is what these two close. Measured immediately: kAXWindows of Finder returns the desktop as an AXScrollArea, not a window — so an element in that list is not guaranteed to be movable, and a caller that cares should read the role rather than assume.

Moving. Move(w, rect, opts) and MoveToDisplay(w, display, displays, opts). The target is a rectangle in global coordinates, so the caller — which knows what a ribbon position means — decides, and this package does not have to.

Placement is Relative (keep the position and proportion the window had, the default), Origin, Center or Fill, with an Inset and an opt-out from clamping. Note that Relative scales the size proportionally, which is a no-op between equal-sized ribbon panels and a real shrink between a 7680×2160 display and a 1920×1200 one; use Center or Origin to keep the size.

DisplayFor decides by overlap area, not by the window's origin. A window straddling a seam has its origin on exactly one display, and it is routinely the one showing the smaller sliver. Answering with that one would compute the relative position against the wrong display and put the window somewhere nobody asked for.

The seam

Move speaks only to a Window interface — Frame, SetPosition, SetSize, Raise — exactly as go-macos/hotkey's Resolve speaks only to a Registrar. The whole placement policy, including the read-back check that decides whether a window really moved, is therefore tested to the last branch on Linux with no AX anywhere in sight. That is where the negative control lives that no Mac is needed for: a fake window that accepts every write and does not move, which is precisely what AX hands you for a pinned window.

Verification

The move is proved by two instruments, neither of which performed the write. TestLiveMoveIsProvedByTwoIndependentInstruments opens a fresh TextEdit instance of its own (never a window that was already on screen), moves it, and then re-reads it through a brand-new AXUIElement — new AXUIElementCreateApplication, new kAXWindowsAttribute copy, new element — and through CGWindowListCopyWindowInfo, which is the window server's own record and has no connection to AX at all. The two must agree with the target and with each other.

before: AX 4915,155 603x505 | window server 4915,155 603x505
Move reported: 4915,155 603x505 → wanted 137,211 640x480, got 137,211 640x480
after : AX(fresh element) 137,211 640x480 | window server 137,211 640x480
PROVED: the window is at 137,211 640x480, confirmed by a fresh AXUIElement AND
by CGWindowListCopyWindowInfo, which had nothing to do with the write

The negative control is a parameter, not a comment. proveMove takes the write as a boolean, and TestLiveNegativeControlTheProofFailsWithoutTheWrite runs the identical measurement with it off. It must report "not moved"; if it did not, the test above would be measuring something other than the write.

NEGATIVE CONTROL: the write is skipped; everything else is identical
after : AX(fresh element) 4915,155 603x505 | window server 4915,155 603x505
CONTROL HELD: with the write removed the window is still at 4915,155 603x505,
not 137,211 640x480, so the assertion in the test above is really testing the write

The no-op detector is proved on a real window too. Asked to move half a point with the tolerance set to zero — a target the window server, which works in whole points, cannot reach — Move reports the refusal instead of the kAXErrorSuccess it was handed.

And a move across displays is judged by where the window server puts it, not by a number: the assertion is that the window's centre lies inside the target display's bounds and that DisplayFor attributes it there.

moving from display 4 [3840,0 7680x2160] to display 2 [0,0 1920x1200] (main)
PROVED: 268,86 151x281 is on display 2 [0,0 1920x1200] (main), per the window server

Raising is proved through a third instrument. Another application is brought to the front first, and "frontmost" afterwards is read from the system-wide AXUIElement, not from NSWorkspace's cache.

A refusal is a skip, never a failure. Where Accessibility has not been granted, the live suite says exactly what is missing, what would grant it and to whom, and gets out of the way. It is measuring the machine, not the package.

No screen capture is taken anywhere in this repository, so no artefact of a person's desktop can be committed. The instruments are geometry, not pixels.

# The portable suite: no AX, no display, no permission. Runs anywhere.
CGO_ENABLED=0 go test ./...
CGO_ENABLED=0 GOOS=linux go test ./...

# The live suite. It opens a TextEdit instance of its own, moves that, and
# kills it. It never touches a window that was already on screen.
ACCESSIBILITY_INTEGRATION=1 go test -tags integration -v -run TestLive .

# The one test that is allowed to show the system permission dialog.
ACCESSIBILITY_INTEGRATION=1 ACCESSIBILITY_PROMPT=1 go test -tags integration -run TestLivePrompt .

The portable layer is at 100% statement coverage, gated in CI on the darwin lane; off darwin, where nothing exists but that layer and the stubs, the whole package is at 100%, gated on the linux lane. The policy is run, not merely compiled, on all six of Go's 64-bit architectures.

The tool

go run ./cmd/axmove -trust                          # the trust state, and what to do about it
go run ./cmd/axmove -displays                       # the displays, in global coordinates
go run ./cmd/axmove -list                           # every window, with its display
go run ./cmd/axmove -focused -display 5             # move the window you are looking at
go run ./cmd/axmove -focused -rect 100,100,800,600  # or to an explicit rectangle

axmove never shows the permission dialog unless -prompt is given.

Platforms

macOS only. Every other platform compiles and reports ErrUnsupported, so consumers cross-compile without a build tag of their own — verified on linux/{amd64,arm64,riscv64,loong64,ppc64le,s390x}, windows/{amd64,arm64}, darwin/{amd64,arm64} and freebsd/amd64. The whole placement policy works on all of them, which is what makes it testable off a Mac.

Licence

BSD-3-Clause.

Documentation

Overview

Package accessibility moves another application's window to a chosen place on macOS, from pure Go with CGO_ENABLED=0.

The macOS Accessibility (AX) API is the only supported way for one process to move another process's window, and it is what this package binds — through purego, so nothing here needs cgo.

What it is for

The consumer is an XR virtual-desktop app that puts several displays on a 360° ribbon around the wearer. "Put this application on ribbon position 3" has to become "move that window onto that display", without the wearer dragging anything across a display boundary by hand.

The coordinate space

Every rectangle in this package is in GLOBAL DISPLAY COORDINATES: the origin is the top-left corner of the main display and y increases DOWNWARDS. That is the space of kAXPositionAttribute, of CGDisplayBounds and of CGWindowListCopyWindowInfo — three independent instruments that agree. It is NOT NSScreen's space, whose origin is at the bottom-left. Mixing the two is the classic way to send a window to a plausible-looking wrong place, so NSScreen is not consulted anywhere in this package. (It is also a cache; see go-macos/virtualdisplay.)

Permission

Unlike go-macos/hotkey, this package DOES need the Accessibility (TCC) grant. There is no equivalent of Carbon's permission-free route: AX is the API, and AX is gated. Trusted reports the state without side effects; RequestTrust is the only call that shows the system dialog, and a caller has to ask for it on purpose. See Trust for what a refusal usually means, which on macOS is rarely what it first looks like.

Portability

Every exported symbol exists on every platform, so a consumer cross-compiles without build tags of its own; off darwin the operating-system calls report ErrUnsupported. The whole placement policy — the rectangle arithmetic, the choice of display, the clamping, and the read-back check that decides whether a move actually happened — is OS-independent and is exercised in full on Linux, with no AX anywhere in sight.

Index

Constants

View Source
const DefaultTolerance = 2.0

DefaultTolerance is how far, in points, a window may land from where it was told before Move calls that a refusal. A window manager rounds to whole points and some applications snap to a grid, so demanding an exact match would report a refusal for a move that plainly happened.

View Source
const RoleWindow = "AXWindow"

RoleWindow is what an element that really is a window answers to kAXRoleAttribute.

It is exported because it is the difference between a window and the DESKTOP: the Finder answers kAXWindows with an AXScrollArea covering every display, and a caller that treats the contents of that list as windows will try to move it.

Variables

View Source
var (
	// ErrUnsupported is returned by every operating-system call on
	// non-darwin platforms.
	ErrUnsupported = errors.New("accessibility: unsupported on this platform (darwin only)")

	// ErrNotTrusted reports that this process does not hold the
	// Accessibility (TCC) grant, so AX will not answer for other
	// applications. Call [Status] and show [Trust.Advice] to the user: on
	// macOS the grant is usually held by a parent application rather than
	// by the binary that is running.
	ErrNotTrusted = errors.New("accessibility: this process is not trusted for Accessibility")

	// ErrNoDisplays reports that no display was found to place a window on.
	ErrNoDisplays = errors.New("accessibility: no displays")

	// ErrNoWindow reports that the application has no window that can be
	// moved — it may have none, or only windows AX declines to describe.
	ErrNoWindow = errors.New("accessibility: no movable window")

	// ErrRefused reports that the write was accepted — AXError 0, no
	// complaint from anyone — and the window nevertheless did not go where
	// it was told. This is the failure that matters: a status check alone
	// cannot see it, which is why [Move] reads the window back and returns
	// this instead of pretending.
	ErrRefused = errors.New("accessibility: the window did not move where it was told")

	// ErrClosed reports use of a window handle that has already been
	// released.
	ErrClosed = errors.New("accessibility: window handle already released")
)

Errors reported by this package. They are stable and may be tested with errors.Is.

Functions

func CloseWindows

func CloseWindows(ws []*AXWindow)

CloseWindows releases a whole listing.

func RequestTrust

func RequestTrust() bool

RequestTrust would show the macOS Accessibility dialog. There is none here, and nothing is prompted.

func SortWindows

func SortWindows(windows []WindowInfo)

SortWindows orders a listing the way a person reads one: by application name, then by window title, then by position. It is stable across runs, which matters because AX returns windows in an order that changes as the user clicks around.

func Trusted

func Trusted() bool

Trusted reports whether this process may use the Accessibility API. There is no such API here, so it is always false.

Types

type AXError

type AXError int32

AXError is a status value from the Accessibility API. It is defined here, in the portable half, because it is pure data: the mapping from a number to what it means to a caller is the part that can be wrong, and it is tested everywhere rather than only on a Mac.

Note what an AXError CANNOT tell you. A write to kAXPositionAttribute that the application quietly ignores returns AXSuccess. That is the whole reason Move measures instead of trusting a status.

const (
	AXSuccess                    AXError = 0
	AXFailure                    AXError = -25200
	AXIllegalArgument            AXError = -25201
	AXInvalidUIElement           AXError = -25202
	AXInvalidUIElementObserver   AXError = -25203
	AXCannotComplete             AXError = -25204
	AXAttributeUnsupported       AXError = -25205
	AXActionUnsupported          AXError = -25206
	AXNotificationUnsupported    AXError = -25207
	AXNotImplemented             AXError = -25208
	AXNotificationAlreadyRegd    AXError = -25209
	AXNotificationNotRegistered  AXError = -25210
	AXAPIDisabled                AXError = -25211
	AXNoValue                    AXError = -25212
	AXParameterizedAttrUnsupport AXError = -25213
	AXNotEnoughPrecision         AXError = -25214
)

The AXError values from HIServices/AXError.h.

func (AXError) Err

func (e AXError) Err(op string) error

Err turns a status into an error: nil for AXSuccess, and an error that wraps ErrNotTrusted for AXAPIDisabled, so a caller can tell a permission problem — which a person can fix — from a window that has gone away, which they cannot.

func (AXError) Error

func (e AXError) Error() string

Error implements error.

type AXWindow

type AXWindow struct {
	// contains filtered or unexported fields
}

AXWindow is a live handle on another application's window. One can never be created here, so WindowsOf and AllWindows never hand one out; the type exists so consumer code naming it still compiles, and every method reports ErrUnsupported rather than panicking on a value a test constructed itself.

func AllWindows

func AllWindows() ([]*AXWindow, error)

AllWindows reports ErrUnsupported.

func FocusedWindow

func FocusedWindow() (*AXWindow, error)

FocusedWindow reports ErrUnsupported.

func WindowsOf

func WindowsOf(pid int, appName string) ([]*AXWindow, error)

WindowsOf reports ErrUnsupported.

func (*AXWindow) App

func (w *AXWindow) App() string

App returns the owning application's localised name.

func (*AXWindow) Attributes added in v0.2.0

func (w *AXWindow) Attributes() ([]string, error)

Attributes reports ErrUnsupported.

func (*AXWindow) Close

func (w *AXWindow) Close() error

Close releases the handle. There is nothing to release here.

func (*AXWindow) Frame

func (w *AXWindow) Frame() (Rect, error)

Frame reports ErrUnsupported.

func (*AXWindow) Info

func (w *AXWindow) Info() (WindowInfo, error)

Info reports ErrUnsupported.

func (*AXWindow) PID

func (w *AXWindow) PID() int

PID returns the owning process.

func (*AXWindow) Raise

func (w *AXWindow) Raise() error

Raise reports ErrUnsupported.

func (*AXWindow) Role added in v0.2.0

func (w *AXWindow) Role() (role, subrole string, err error)

Role reports ErrUnsupported.

func (*AXWindow) SetPosition

func (w *AXWindow) SetPosition(Point) error

SetPosition reports ErrUnsupported.

func (*AXWindow) SetSize

func (w *AXWindow) SetSize(Size) error

SetSize reports ErrUnsupported.

func (*AXWindow) Title

func (w *AXWindow) Title() string

Title returns the window title.

type Application

type Application struct {
	// PID is the process identifier.
	PID int
	// Name is the localised application name.
	Name string
	// Bundle is the bundle identifier.
	Bundle string
	// Active reports whether this application is frontmost.
	Active bool
}

Application is a running application that might own a movable window. None is ever returned on this platform; the type exists so consumer code naming it still compiles.

func Applications

func Applications() ([]Application, error)

Applications reports ErrUnsupported.

func (Application) String

func (a Application) String() string

String renders the application for a listing.

type Display

type Display struct {
	// ID is the CGDirectDisplayID. It is stable while the display stays
	// attached and is what a caller should remember a ribbon position by.
	ID uint32
	// Bounds is the display's rectangle in global coordinates. The main
	// display's origin is (0,0) and every other display is placed relative
	// to it, so a display above or to the left of the main one has negative
	// coordinates.
	Bounds Rect
	// Main reports whether this is the main display — the one carrying the
	// menu bar, and the origin of the coordinate space.
	Main bool
}

Display is one active display, as CoreGraphics describes it.

func DisplayByID

func DisplayByID(displays []Display, id uint32) (Display, bool)

DisplayByID finds a display by its CGDirectDisplayID.

func DisplayFor

func DisplayFor(frame Rect, displays []Display) (Display, bool)

DisplayFor reports which display a window is on: the one covering the most of it.

Overlap area, not the window's origin, decides. A window straddling two displays has an origin on exactly one of them, and it is routinely the one showing the smaller sliver — a title bar dragged just past the seam. Answering with that display would make MoveToDisplay compute the wrong relative position and put the window somewhere the user did not ask for.

A window that overlaps nothing at all — entirely off every display, which happens after a display is unplugged — is attributed to the display whose centre is nearest, so it can still be brought back. Ties go to the lowest ID so the answer is deterministic.

func Displays

func Displays() ([]Display, error)

Displays reports ErrUnsupported. Supply your own Display values to MoveToDisplay and Place to exercise the placement policy here.

func MainDisplay

func MainDisplay(displays []Display) (Display, bool)

MainDisplay returns the main display.

func (Display) String

func (d Display) String() string

String renders the display for a log line.

type Options

type Options struct {
	// Placement selects where on the target display the window lands.
	Placement Placement

	// Inset shrinks the target display's usable rectangle by this many
	// points on every side. Use it to keep clear of the menu bar or of a
	// ribbon's own furniture.
	Inset float64

	// NoClamp lets the window keep a size and position that hang off the
	// edge of the target display. By default a window is shrunk and nudged
	// until it fits, because a window half off a ribbon panel is not on that
	// ribbon panel.
	NoClamp bool

	// NoRaise leaves the window's stacking order alone. By default a move
	// also raises the window and makes its application frontmost, because
	// "send it there" nearly always means "and let me see it".
	NoRaise bool

	// Tolerance overrides [DefaultTolerance]: how far the window may land
	// from where it was told before the move counts as refused. A negative
	// value means the same as zero — exact.
	Tolerance float64
}

Options tunes Move and MoveToDisplay. The zero value is the sensible default: Relative placement, no inset, clamped to the target display, raised afterwards, DefaultTolerance.

type Placement

type Placement int

Placement says where on the target display a window should land.

const (
	// Relative keeps the window's position and size as a fraction of the
	// display it came from.
	Relative Placement = iota
	// Origin puts the window's top-left corner at the display's top-left
	// corner and leaves its size alone.
	Origin
	// Center centres the window on the display and leaves its size alone.
	Center
	// Fill makes the window cover the whole display.
	Fill
)

The placements. The zero value, Relative, is what a ribbon wants: the window keeps the position and proportion it had, so a window that filled the left half of one display fills the left half of the next.

func (Placement) String

func (p Placement) String() string

String names the placement.

type Point

type Point struct{ X, Y float64 }

Point is a position in global display coordinates.

func (Point) String

func (p Point) String() string

String renders the point for a log line.

type Rect

type Rect struct{ X, Y, W, H float64 }

Rect is a rectangle in global display coordinates: origin top-left, y increasing downwards. See the package comment on the coordinate space.

func Place

func Place(frame Rect, from, to Display, opts *Options) Rect

Place computes where a window should go, and is the whole of this package's geometry policy: a pure function of two rectangles, with no operating system anywhere near it.

from is the display the window is on now and to is the display it should end up on; they may be the same. Only Relative reads from at all, and a degenerate from — a display of zero width or height, which is what an unplugged display leaves behind — falls back to Origin rather than dividing by zero.

func (Rect) Area

func (r Rect) Area() float64

Area returns the enclosed area, or zero for an empty rectangle.

func (Rect) Bottom

func (r Rect) Bottom() float64

Bottom returns the y coordinate just past the bottom edge.

func (Rect) Center

func (r Rect) Center() Point

Center returns the middle of the rectangle.

func (Rect) Contains

func (r Rect) Contains(p Point) bool

Contains reports whether p lies inside the rectangle. The top and left edges are inside, the bottom and right edges are not, so adjacent displays never both claim the same point.

func (Rect) Empty

func (r Rect) Empty() bool

Empty reports whether the rectangle encloses nothing.

func (Rect) Inset

func (r Rect) Inset(d float64) Rect

Inset returns the rectangle shrunk by d on every side. A d that would leave nothing is refused: the rectangle comes back unchanged rather than empty, because an empty target display is never what a caller means.

func (Rect) Intersect

func (r Rect) Intersect(o Rect) Rect

Intersect returns the overlap of two rectangles, or the zero Rect when they do not overlap.

func (Rect) NearlyEqual

func (r Rect) NearlyEqual(o Rect, tol float64) bool

NearlyEqual reports whether every edge of the two rectangles is within tol. Window managers round, snap and clamp, so exact equality is the wrong test.

func (Rect) Offset

func (r Rect) Offset(dx, dy float64) Rect

Offset returns the rectangle moved by (dx, dy).

func (Rect) Origin

func (r Rect) Origin() Point

Origin returns the top-left corner.

func (Rect) Right

func (r Rect) Right() float64

Right returns the x coordinate just past the right-hand edge.

func (Rect) Size

func (r Rect) Size() Size

Size returns the width and height.

func (Rect) String

func (r Rect) String() string

String renders the rectangle for a log line: "300,200 640x480".

type Result

type Result struct {
	// Before is where the window was, read before anything was written.
	Before Rect
	// Wanted is where [Place] decided it should go.
	Wanted Rect
	// Got is where it actually is, READ BACK after the write. This is the
	// only field that is evidence; the rest is intent.
	Got Rect
	// From and To are the displays the window came from and was sent to.
	// They are the zero Display when the caller used [Move] directly and
	// named no display.
	From, To Display
	// Attempts counts the position writes it took. See defaultAttempts for
	// why more than one is normal.
	Attempts int
	// Moved reports whether Got's origin is within tolerance of Wanted's.
	Moved bool
	// Resized reports whether Got's size is within tolerance of Wanted's. A
	// window with a minimum size refuses to shrink and this is false while
	// Moved is true, which is a success, not a failure.
	Resized bool
	// Raised reports whether the window was brought forward.
	Raised bool
}

Result is what a move actually achieved, measured rather than assumed.

func Move

func Move(w Window, want Rect, opts *Options) (Result, error)

Move puts a window at want and then PROVES it, by reading the window back through Window.Frame and comparing.

This is the point of the package. AX accepts a write to kAXPositionAttribute with AXError 0 whether or not the application honours it: a window pinned by its own controller, a full-screen window, a sheet, a window whose application is not answering — all of them return success and stay exactly where they were. A caller that checked the status would be told the move worked. So the status is not what is checked here; the window's position afterwards is.

The size is written only when it differs from the window's current size, and the position is re-asserted when the first read-back disagrees — setting a size can push the origin back. If the window still is not where it was told, Move returns ErrRefused together with the Result, so a caller can both react to the failure and see exactly how far off it landed.

func MoveToDisplay

func MoveToDisplay(w Window, to Display, displays []Display, opts *Options) (Result, error)

MoveToDisplay sends a window to a display: it works out which display the window is on now, asks Place where it should land on the target, and then Move proves it went there.

The caller supplies the display list rather than this function fetching one, so the same list can be used for a whole batch of windows and so the policy stays a pure function. Pass Displays on darwin, or a list of your own — go-xrkit's ribbon positions are displays that this package never has to know the meaning of.

func (Result) String

func (r Result) String() string

String renders the result the way a person needs to read it: what was asked for, what happened, and the difference between them.

type ServerWindow

type ServerWindow struct {
	// Number is the CGWindowID.
	Number int
	// PID is the owning process.
	PID int
	// Owner is the owning application's name.
	Owner string
	// Title is the window title.
	Title string
	// Layer is the window level.
	Layer int
	// Frame is the window's rectangle.
	Frame Rect
}

ServerWindow is one window as the macOS window server sees it. None is ever returned on this platform.

func ServerWindows

func ServerWindows() ([]ServerWindow, error)

ServerWindows reports ErrUnsupported.

func (ServerWindow) String

func (s ServerWindow) String() string

String renders the window for a log line.

type Size

type Size struct{ W, H float64 }

Size is a width and a height in points.

func (Size) String

func (s Size) String() string

String renders the size for a log line.

type Trust

type Trust struct {
	// Trusted is AXIsProcessTrusted(): whether AX will answer for other
	// applications right now.
	Trusted bool
	// Bundled reports whether the running executable is inside a .app
	// bundle.
	Bundled bool
	// Bundle is the bundle identifier, empty for an unbundled binary.
	Bundle string
	// Name is the application's name as System Settings would list it, or
	// the executable's base name for an unbundled binary.
	Name string
	// Path is the executable's path.
	Path string
	// Responsible is the name of the process that actually holds the grant
	// when this one is unbundled — the terminal, usually. It is empty when
	// it could not be determined.
	Responsible string
}

Trust is what this process may do with the Accessibility API, and why.

The "why" is the part that is worth having. On macOS the TCC grant does not attach to the executable that asks for it: it attaches to the RESPONSIBLE process, which for a command-line binary is the terminal that launched it and for a bundled application is the .app. So an unbundled Go binary is trusted exactly when its terminal is, will never appear in System Settings under its own name, and cannot be granted the permission on its own. Telling a user to "add this binary in System Settings" when that is impossible is worse than telling them nothing.

func Status

func Status() (Trust, error)

Status reports ErrUnsupported.

func (Trust) Advice

func (t Trust) Advice() string

Advice returns what a person has to do about the current state, in words that are true for this process rather than the generic instruction that is wrong half the time.

func (Trust) String

func (t Trust) String() string

String renders the trust state in one line.

type Window

type Window interface {
	// Frame reads the window's current rectangle back from the system. It
	// is called both before and after a write, and it must really ask —
	// returning a remembered value would make the read-back check
	// worthless.
	Frame() (Rect, error)
	// SetPosition writes kAXPositionAttribute.
	SetPosition(Point) error
	// SetSize writes kAXSizeAttribute.
	SetSize(Size) error
	// Raise brings the window forward and makes its application frontmost.
	Raise() error
}

Window is the seam between the placement policy and the operating system. Move speaks only to this, so every branch of the policy — including a window that lies about where it went — is testable on any platform, with no AX at all.

The darwin implementation is *AXWindow. Position and size are separate because AX has two attributes, kAXPositionAttribute and kAXSizeAttribute, and a caller that only wants to move a window should not be made to restate its size.

type WindowInfo

type WindowInfo struct {
	// PID is the owning process.
	PID int
	// App is the application's localised name.
	App string
	// Title is the window's title, which is often empty — a document window
	// that has never been saved, or an application that does not set one.
	Title string
	// Frame is the window's rectangle in global coordinates.
	Frame Rect
	// Display is the CGDirectDisplayID of the display the window is mostly
	// on, or zero when it was not resolved. Fill it with [Annotate].
	Display uint32
	// Minimized reports whether the window is in the Dock. A minimized
	// window still has a position and can still be moved, and will appear
	// where it was put when it is restored.
	Minimized bool
}

WindowInfo is a snapshot of one window: enough to show a person a list and let them pick one.

func Annotate

func Annotate(windows []WindowInfo, displays []Display) []WindowInfo

Annotate fills in each window's Display from a display list. It is separate from the listing itself so that the attribution — which is DisplayFor, and is not obvious — is portable policy rather than something buried in a platform file.

func List

func List() ([]WindowInfo, error)

List reports ErrUnsupported.

func (WindowInfo) String

func (i WindowInfo) String() string

String renders the window for a listing.

Directories

Path Synopsis
cmd
axmove command
Command axmove reports the Accessibility trust state, lists the windows on this machine, and moves one to a display.
Command axmove reports the Accessibility trust state, lists the windows on this machine, and moves one to a display.

Jump to

Keyboard shortcuts

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