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
- Variables
- func CloseWindows(ws []*AXWindow)
- func RequestTrust() bool
- func SortWindows(windows []WindowInfo)
- func Trusted() bool
- type AXError
- type AXWindow
- func (w *AXWindow) App() string
- func (w *AXWindow) Attributes() ([]string, error)
- func (w *AXWindow) Close() error
- func (w *AXWindow) Frame() (Rect, error)
- func (w *AXWindow) Info() (WindowInfo, error)
- func (w *AXWindow) PID() int
- func (w *AXWindow) Raise() error
- func (w *AXWindow) Role() (role, subrole string, err error)
- func (w *AXWindow) SetPosition(Point) error
- func (w *AXWindow) SetSize(Size) error
- func (w *AXWindow) Title() string
- type Application
- type Display
- type Options
- type Placement
- type Point
- type Rect
- func (r Rect) Area() float64
- func (r Rect) Bottom() float64
- func (r Rect) Center() Point
- func (r Rect) Contains(p Point) bool
- func (r Rect) Empty() bool
- func (r Rect) Inset(d float64) Rect
- func (r Rect) Intersect(o Rect) Rect
- func (r Rect) NearlyEqual(o Rect, tol float64) bool
- func (r Rect) Offset(dx, dy float64) Rect
- func (r Rect) Origin() Point
- func (r Rect) Right() float64
- func (r Rect) Size() Size
- func (r Rect) String() string
- type Result
- type ServerWindow
- type Size
- type Trust
- type Window
- type WindowInfo
Constants ¶
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.
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 ¶
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 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.
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 ¶
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.
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 WindowsOf ¶
WindowsOf reports ErrUnsupported.
func (*AXWindow) Attributes ¶ added in v0.2.0
Attributes reports ErrUnsupported.
func (*AXWindow) Role ¶ added in v0.2.0
Role reports ErrUnsupported.
func (*AXWindow) SetPosition ¶
SetPosition reports ErrUnsupported.
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 (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 ¶
DisplayByID finds a display by its CGDirectDisplayID.
func DisplayFor ¶
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 ¶
Displays reports ErrUnsupported. Supply your own Display values to MoveToDisplay and Place to exercise the placement policy here.
func MainDisplay ¶
MainDisplay returns the main display.
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.
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 ¶
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) Contains ¶
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) Inset ¶
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 ¶
Intersect returns the overlap of two rectangles, or the zero Rect when they do not overlap.
func (Rect) NearlyEqual ¶
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.
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 ¶
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 ¶
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.
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 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.
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 (WindowInfo) String ¶
func (i WindowInfo) String() string
String renders the window for a listing.