macos

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package macos groups the drivers that operate on the Mac's own apps.

Unlike iOS, there is no single CLI here that does everything: the accessibility tree and the menus come from one tool, the input from another, and the captures from the system itself. The router already knows how to split by capability and cost, so each driver declares what it does well and leaves the rest.

Index

Constants

View Source
const AxcliID = "axcli"

AxcliID is the driver's registry key.

View Source
const CuaID = "cua"

CuaID is the driver's registry key.

View Source
const ScreencaptureID = "screencapture"

ScreencaptureID is the registry key of the system capture driver.

View Source
const SystemID = "macsystem"

SystemID is the driver's registry key.

Variables

This section is empty.

Functions

func CuaVideoFile added in v0.13.0

func CuaVideoFile(outPath string) string

CuaVideoFile is the file the daemon produces inside cuaVideoDir. Exported because the caller has to move it into place: VideoStop is handed a pid and nothing else, so the driver has no way to know where the run wanted the finished file.

Types

type Axcli

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

Axcli wraps axcli, which is in the mix for one reason only: it delivers events to the target process with CGEventPostToPid, without activating the app, without moving the cursor and without jumping Spaces. If an agent validates while you work, that is not a comfort detail: it is the difference between being able to use the Mac and not.

It stays as an escape hatch next to cua-driver, which is the canonical one. The reason is concrete: cua-driver resolves the window through `list_windows`, which only enumerates layer 0, so a floating UI, a panel, HUD, popover, an onboarding, is invisible to it. axcli targets by `--app` and needs no window id, so it reaches exactly those windows.

Its capture is NOT declared, and that is not an omission: it returned the desktop cropped to the window's dimensions, with no error, when the process has no graphical session, and it also activated the app. A plausible but false PNG is worse than having no capture.

func NewAxcli

func NewAxcli(exec drivers.Executor) *Axcli

NewAxcli builds the driver.

func (*Axcli) Cost

func (d *Axcli) Cost(c drivers.Capability, _ drivers.Target) int

Cost is where the split with Peekaboo is expressed, with no new traits on the Driver interface.

Taps are cost 0: they go through CGEventPostToPid, axcli's default for click and scroll, and they do not steal focus.

Typing is NOT cost 0, and this is easy to get wrong: `input` and `fill` activate the app before typing, they do it in the code, with no flag to avoid it, so there axcli is no better than Peekaboo. It is declared equally expensive so the router does not prefer it believing it gains something.

func (*Axcli) ID

func (d *Axcli) ID() string

func (*Axcli) Probe

Probe checks the binary. axcli has no diagnostic command: it verifies AXIsProcessTrusted when starting any targeted command and dies with `error: accessibility not granted`. It is not invoked here on purpose, asking a real app for permission just to probe would have side effects, so the TCC state is reported by Peekaboo, which does know how to ask without touching anything.

func (*Axcli) Provides

func (d *Axcli) Provides(target drivers.Target) drivers.CapabilitySet

func (*Axcli) Tap

func (d *Axcli) Tap(ctx context.Context, target drivers.Target, spec drivers.TapSpec) (drivers.TapResult, error)

Tap clicks without stealing focus.

func (*Axcli) Type

func (d *Axcli) Type(ctx context.Context, target drivers.Target, spec drivers.TextSpec) error

Type writes into a specific element. Beware: this DOES activate the app, `fill` calls activate() before typing and there is no way around it, hence Cost declares it expensive.

func (*Axcli) Warm

func (d *Axcli) Warm(_ context.Context, _ drivers.Target) <-chan error

type Cua

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

Cua wraps cua-driver (trycua/cua, MIT), mav's canonical driver on macOS.

It is here for a structural reason, not preference: on macOS the Accessibility and Screen Recording permissions are granted ONLY to interactive GUI processes. A CLI cannot hold them no matter how much you grant them to the terminal. The only architecture that works is a broker, an app holding the permissions plus a socket, and cua-driver ships it built in: the binary we invoke lives inside /Applications/CuaDriver.app.

What it adds over what was there before, measured inside a VM against a floating window (layer != 0), which is the case that broke the others:

  • Peekaboo enumerated that window and discarded it on its own because of the layer: no tree, no capture.
  • axcli read the tree, but its capture returned the DESKTOP cropped to the window's dimensions, with no error, and it also activated the app.
  • cua-driver returns a tree WITH geometry and a window capture with real content in the SAME call, and the click lands in the background.

Two known limits, both its own:

  • `list_windows` is layer-0 only by declared design, so a floating window does not show up in discovery.
  • its elements expose no AXIdentifier: only element_token (valid within the snapshot), role, label and frame.

func NewCua

func NewCua(exec drivers.Executor) *Cua

NewCua builds the driver.

func (*Cua) Cost

func (d *Cua) Cost(c drivers.Capability, _ drivers.Target) int

Cost declares it canonical for everything it provides: it is the only one covering all four capabilities with verified background delivery.

func (*Cua) DoubleTap added in v0.15.0

func (d *Cua) DoubleTap(ctx context.Context, target drivers.Target, spec drivers.TapSpec) error

DoubleTap double-clicks. cua-driver ships a dedicated `double_click` tool, and that is why this is not two Taps in a row: two separate `click` calls arrive as two single clicks — the event's clickCount never reaches 2 — and nothing that opens on a double click would react. The selector path goes by element token for the same reason Tap does: it lands on background windows without moving the cursor or stealing focus.

func (*Cua) Drag added in v0.15.0

Drag and DragPath are here because AdvancedGestureDriver is a whole interface, and they fail saying why. cua-driver does ship a `drag` tool; leaving it unwired is deliberate, not an oversight — no current flow needs a desktop drag, and shipping an unexercised gesture would claim a capability nothing has verified.

func (*Cua) DragPath added in v0.15.0

func (*Cua) Erase

func (d *Cua) Erase(ctx context.Context, target drivers.Target, spec drivers.TextSpec) error

Erase empties a field.

It goes through set_value and not by typing deletions: writing the empty string leaves the field empty in one shot, while sending N Delete keystrokes depends on guessing how many, and on the field having focus, which is exactly what this driver avoids needing.

func (*Cua) HideKeyboard

func (d *Cua) HideKeyboard(context.Context, drivers.Target) error

HideKeyboard does not exist on macOS: there is no on-screen keyboard to hide. It is declared because TextDriver is a whole interface, and it does nothing instead of failing, because a flow shared between iOS and Mac will call it and here it is simply unnecessary.

func (*Cua) ID

func (d *Cua) ID() string

func (*Cua) Pinch

Pinch, Rotate, TwoFingerPan and W3CActions exist because GestureDriver is a whole interface, and they fail saying why.

It is not a gap to fill later: a trackpad sends gestures to the system and to the focused app, not to a PID, so reproducing them in the background against a specific window is not a matter of effort but of there being no path. A message that says so saves the search.

func (*Cua) Probe

func (d *Cua) Probe(ctx context.Context, p drivers.Probe) drivers.HealthReport

Probe asks the daemon about permissions, not the process running mav.

That is the difference that matters: `permissions status` answers with CuaDriver's identity (com.trycua.driver) because it is its own responsible process. With no daemon it answers `unknown` instead of lying with your terminal's permissions, which is why that case is reported as degraded and not as healthy.

func (*Cua) Provides

func (d *Cua) Provides(target drivers.Target) drivers.CapabilitySet

func (*Cua) Rotate

func (*Cua) Screenshot

func (d *Cua) Screenshot(ctx context.Context, target drivers.Target, spec drivers.ScreenshotSpec) error

Screenshot writes the window capture.

It comes from the same get_window_state as the tree, in base64, so image and tree describe the SAME instant, which for evidence accompanying a tree is exactly what you want, and there is no second invocation that could catch the screen already changed.

func (*Cua) Swipe

func (d *Cua) Swipe(ctx context.Context, target drivers.Target, spec drivers.SwipeSpec) error

Swipe scrolls the window's content.

On a Mac a swipe IS a scroll: there is no finger, there is a wheel. The direction is inverted on purpose, swiping up on a phone moves the content up, which on desktop is requested as scrolling down, so a flow written once means the same on both platforms.

func (*Cua) Tap

func (d *Cua) Tap(ctx context.Context, target drivers.Target, spec drivers.TapSpec) (drivers.TapResult, error)

Tap clicks without bringing the app to the front.

It is two calls and not one on purpose: the tool demands a fresh snapshot before every per-element action and invalidates the index map as soon as you take another. Reusing an old snapshot is exactly how you end up clicking the wrong thing.

func (*Cua) Tree

func (d *Cua) Tree(ctx context.Context, target drivers.Target, _ drivers.TreeSpec) (drivers.TreeResult, error)

Tree returns the window's accessibility tree.

func (*Cua) TwoFingerPan

func (*Cua) Type

func (d *Cua) Type(ctx context.Context, target drivers.Target, spec drivers.TextSpec) error

Type writes into the selector's element.

func (*Cua) VideoStart added in v0.13.0

func (d *Cua) VideoStart(ctx context.Context, _ drivers.Target, spec drivers.VideoSpec) (drivers.VideoResult, error)

VideoStart brings the daemon up, then holds a session open for it.

func (*Cua) VideoStop added in v0.13.0

func (d *Cua) VideoStop(ctx context.Context, _ drivers.Target, pid int) error

VideoStop finalizes the mp4 before anything kills the session holding it.

It asks the daemon to stop rather than just signalling the holder, because only the daemon can write the mp4's index; a file cut off without it is a plausible-looking mp4 no player will open, which is worse than no video. Stopping is documented as unconditional, so it does not matter which session started the recording.

func (*Cua) W3CActions

func (d *Cua) W3CActions(context.Context, drivers.Target, []byte) error

func (*Cua) Warm

func (d *Cua) Warm(_ context.Context, _ drivers.Target) <-chan error

type Screencapture

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

Screencapture wraps the `screencapture` that ships with macOS. It is the last resort for CapScreenshot: it captures the whole screen, which as evidence of a specific app is worse than a capture bounded to its window. A driver that can resolve the window id must declare a lower cost and beat it.

func NewScreencapture

func NewScreencapture(exec drivers.Executor) *Screencapture

NewScreencapture builds the driver.

func (*Screencapture) Cost

Cost places screencapture as an acceptable fallback, not the canonical path: whole screen instead of window.

func (*Screencapture) ID

func (d *Screencapture) ID() string

func (*Screencapture) Probe

Probe checks that the system binary is where it should be. It cannot check the Screen Recording permission: that belongs to the parent process (the terminal or the agent's harness), not to mav, and there is no cheap way to ask without attempting a real capture.

func (*Screencapture) Provides

func (d *Screencapture) Provides(target drivers.Target) drivers.CapabilitySet

Provides only declares capabilities on the Mac: on the simulator and on device there are better paths already covered.

func (*Screencapture) Screenshot

func (d *Screencapture) Screenshot(ctx context.Context, _ drivers.Target, spec drivers.ScreenshotSpec) error

Screenshot captures the screen. `-x` silences the shutter sound, which in an automated session is only noise.

func (*Screencapture) VideoStart

VideoStart begins a recording. screencapture -v records until it is sent SIGINT, which is how VideoStop stops it.

func (*Screencapture) VideoStop

func (d *Screencapture) VideoStop(ctx context.Context, _ drivers.Target, pid int) error

VideoStop cuts the recording with SIGINT. Killing with SIGKILL would leave the .mov half-written and without an index, that is, unreadable.

func (*Screencapture) Warm

func (d *Screencapture) Warm(_ context.Context, _ drivers.Target) <-chan error

type System

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

System covers the lifecycle and utilities of a macOS app with the tools the system itself ships. It wraps no third-party CLI because none is needed: `open`, `pbcopy`, `pbpaste` and the filesystem are enough.

It exists above all for one concrete reason: without a CapTerminate provider on the Mac, closing the app before seeding a fixture was a silent no-op, and the fixture would write the database while the previous instance kept it open.

func NewSystem

func NewSystem(exec drivers.Executor) *System

NewSystem builds the driver.

func (*System) Boot

func (d *System) Boot(_ context.Context, _ drivers.Target) error

Boot does not exist on the Mac: the machine is already booted.

func (*System) ClipboardRead

func (d *System) ClipboardRead(ctx context.Context, _ drivers.Target) (string, error)

func (*System) ClipboardWrite

func (d *System) ClipboardWrite(ctx context.Context, _ drivers.Target, text string) error

func (*System) Cost

Cost: it is the only provider of all this on the Mac.

func (*System) ID

func (d *System) ID() string

func (*System) Install

func (d *System) Install(_ context.Context, _ drivers.Target, spec drivers.InstallSpec) error

Install on macOS copies nothing to /Applications: to validate, the app is run from wherever the build left it. "Installing" here is checking that the bundle is where we say, which is the only part that can fail and the one that gives a useful error when the launch recipe did not produce what it believed.

func (*System) Launch

func (d *System) Launch(ctx context.Context, target drivers.Target, spec drivers.LaunchSpec) (drivers.LaunchResult, error)

Launch runs the binary inside the bundle instead of using `open`.

It is not a whim: `open` does not propagate environment variables to the process it starts, and the environment is exactly how mav injects its configuration (the equivalent of the simulator's SIMCTL_CHILD_*). Running Contents/MacOS/<binary> directly inherits everything.

func (*System) ListApps

func (d *System) ListApps(ctx context.Context, _ drivers.Target) (string, error)

func (*System) OpenURL

func (d *System) OpenURL(ctx context.Context, _ drivers.Target, url string) error

func (*System) Probe

Probe needs to check nothing installable: these are system binaries. What it does check is that we are on macOS, because a mac target on another system is not a recoverable failure but an impossible config.

func (*System) Provides

func (d *System) Provides(target drivers.Target) drivers.CapabilitySet

func (*System) ResetLocation

func (d *System) ResetLocation(context.Context, drivers.Target) error

func (*System) SetLocation

func (d *System) SetLocation(context.Context, drivers.Target, float64, float64) error

SetLocation and ResetLocation have no equivalent: macOS does not allow overriding the location of an already launched app. The path that does exist is an Xcode scheme's "Simulate Location", which happens at launch and not here.

func (*System) Terminate

func (d *System) Terminate(ctx context.Context, _ drivers.Target, bundleID string) error

Terminate closes the app. `osascript quit` asks for a clean shutdown, which is what lets the app close its database instead of leaving the WAL half-written, exactly what the fixture needs before seeding.

func (*System) Uninstall

func (d *System) Uninstall(ctx context.Context, _ drivers.Target, bundleID string) error

Uninstall is the honest equivalent of `simctl uninstall` on the Mac: it does not uninstall the app, which runs from wherever it is, but deletes its state, which is what --clear-state really means.

func (*System) Warm

func (d *System) Warm(_ context.Context, _ drivers.Target) <-chan error

Jump to

Keyboard shortcuts

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