skylight

package
v0.6.18 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package skylight provides high-level, idiomatic Go bindings for macOS WindowServer (SkyLight) features.

This package wraps the low-level private SkyLight SPIs in github.com/tmc/apple/private/skylight, providing typed and memory-safe abstractions for Space management, off-space window detection, focus switching without window raising, and process ownership queries.

Core Concepts

  • Space: represents a macOS display space (virtual desktop).
  • Window: represents a WindowServer window ID.
  • ProcessSerialNumber: uniquely identifies a process in the WindowServer.

Usage Overview

Space detection:

activeSpace, err := skylight.ActiveSpace()
if err != nil {
    log.Fatal(err)
}
spaces, err := skylight.SpacesForWindow(windowID)

Focusing without raising windows:

err := skylight.FocusWithoutRaise(targetPID)

Querying window owners:

pid, err := skylight.WindowOwnerPID(windowID)

Thread Safety & Initialization

WindowServer connection handles (CGSConnectionID) are lazily resolved upon first call using sync.Once. Package methods may be safely invoked concurrently from any goroutine.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ActivateForMenuShortcut

func ActivateForMenuShortcut(targetPID int, targetWid Window) error

ActivateForMenuShortcut makes the process owning targetWid frontmost to the window server, without raising its windows, so that a key equivalent posted to the HID tap reaches its menu. Callers should prefer WithMenuShortcutActivation, which restores the previous frontmost process.

targetPID is used only if targetWid cannot be resolved to an owner.

func FocusWithoutRaise

func FocusWithoutRaise(targetPID int, targetWid Window) error

FocusWithoutRaise makes targetWid the key window of its owning process without raising it or changing the window server's z-order, which is what makes it usable on a window that is not on the active space.

It posts a defocus record to the process that is currently frontmost and a focus record naming targetWid to the target, following yabai's window_manager_focus_window_without_raise. It deliberately does not call SLPSSetFrontProcessWithOptions: that is what keeps Chromium's user-activation gate open, so synthetic input is still treated as user-generated.

targetPID is used only if targetWid cannot be resolved to an owner.

Example
package main

import (
	"fmt"

	"github.com/tmc/apple/x/skylight"
)

func main() {
	// Focus target window 1001 owned by process PID 1234 without raising windows
	err := skylight.FocusWithoutRaise(1234, 1001)
	if err != nil {
		fmt.Printf("focus failed: %v\n", err)
		return
	}
	fmt.Println("process focused")
}

func IsWindowOffSpace

func IsWindowOffSpace(w Window) (bool, error)

IsWindowOffSpace reports whether the window is present on the current active space.

func PostEventToPID

func PostEventToPID(pid int, event coregraphics.CGEvent) error

PostEventToPID delivers event to pid directly, bypassing the global HID tap and the hit-testing that comes with it.

It posts twice, by design. SLEventPostToPid reaches Catalyst and Chromium targets that the public path does not, and CGEventPostToPid reaches AppKit targets that SkyLight drops. cua-driver does the same and calls it belt-and-suspenders; neither path alone covers every target.

An error is returned only if the SkyLight path fails to be called at all. A non-zero status from it is not treated as fatal because the public path may still deliver, so callers that need to know an event arrived must check for the effect rather than trusting the return.

func RouteMouseEventToWindow

func RouteMouseEventToWindow(event coregraphics.CGEvent, w Window, pid int, local corefoundation.CGPoint, clickState, clickGroupID int64) error

RouteMouseEventToWindow addresses event at window w of process pid, using the window-local point local rather than the global cursor position.

A synthetic mouse event posted to the HID tap is delivered by hit-testing the cursor position against the window server's front-to-back order, so it lands on whatever is frontmost at that point -- not necessarily w, and never w when w is on another space. Stamping the window-local point and the routing fields makes the window server use the point and window id directly instead.

Call this on an event built by CGEventCreateMouseEvent before posting it with PostEventToPID. Posting a routed event through the HID tap ignores the routing entirely.

clickState is the click count: 1 for a single click, 2 for a double click. clickGroupID ties the events of one gesture together; use the same value for the down and up of a click, and a different value for the next click.

func WindowOwnerPID

func WindowOwnerPID(w Window) (int, error)

WindowOwnerPID returns the process ID of the window owner.

func WithMenuShortcutActivation

func WithMenuShortcutActivation(targetPID int, targetWid Window, action func() error) error

WithMenuShortcutActivation activates targetWid for menu shortcut dispatch, calls action, and restores the previously frontmost process. The restore runs even if action fails or panics. The menu still fires because the key event is enqueued on the target's run loop before the restore takes effect.

Unlike the reference implementation in cua-driver, action is not called when activation fails: a caller that runs it anyway would post input to whichever process happened to be frontmost.

Types

type EventRecord

type EventRecord struct {
	MajorVersion   uint16
	MinorVersion   uint16
	DeclaredLength uint32
	EventType      uint32
	SubtypeFlags   uint32
	LocationX      float64
	LocationY      float64
	WinLocationX   float64
	WinLocationY   float64
	EventTime      uint64
	EventFlags     uint32
	WindowID       uint32
	ConnectionID   uint32

	SessionField uint32 // 0x48

	Attributes uint32 // 0x54

	// ActivationState is 0x01 to focus and 0x02 to defocus. See the type
	// documentation: SkyLight does not read it.
	ActivationState uint8 // 0x8a

	KeyCode uint16 // 0x90

	AppendixPtr uintptr // 0xe0
	// contains filtered or unexported fields
}

EventRecord is the 248-byte record that SkyLight passes between the window server and its clients. Apple does not document it, so it is described here by hand rather than in private/skylight, which holds only generated bindings. Nothing generated needs it: SLPSPostEventRecordTo takes the record as a byte slice, so the layout is this package's concern alone.

Its size is load-bearing. Offset 0x04 holds the record's own declared length, and _SLSEventRecordLength is a constant function returning it (mov w0, #0xf8; ret at 0x186d83290), so buffers passed to SLPSPostEventRecordTo must be exactly 248 bytes. Use EventRecord.Bytes rather than reslicing by hand.

Field provenance, all read from the arm64e dyld shared cache of macOS 15. Offsets not listed here rest on structural inference only:

0x04 DeclaredLength   _SLSEventRecordLength returns 0xf8
0x08 EventType        decodeEventRecordForPostTo masks off the low bit and
                      compares against kCGEventKeyDown, which is what makes
                      this a type field rather than half of an opcode
                      (and w8, w8, #0xfffffffe; cmp w8, #0xa)
0x38 EventFlags       decodeEventRecordForPost ORs it with the result of
                      _CGXCurrentEventFlags and stores it back
0x3c WindowID         the window id the focus recipe stamps
0x48 SessionField     written from the session object when Attributes bit 2
                      is clear
0x54 Attributes       bit 2 gates SessionField above
0x90 KeyCode          uint16; SLPSPostEventRecordTo refuses a key event
                      whose value here is 0x7f
0xe0 AppendixPtr      _SLSEventRecordSetBaseAttributes stores the result of
                      _CGSEventAppendixCreate here, and
                      _SLSEventRecordGetContextID dereferences it as a
                      null-checked pointer

The tail past the common header is variant by event type -- a keyboard record carries keyboard payload from roughly 0x88 on -- so a field there is only meaningful for the matching type.

ActivationState is the one field carried on faith. yabai and cua-driver write 0x01 to focus and 0x02 to defocus and the recipe works, but SkyLight itself never reads the byte: scanning its __text for byte and halfword accesses at +0x8a finds eighteen sites, none in event-record code, and the word loads at +0x88 in both decode functions are on the session object rather than the record. The value is carried opaquely to the receiving process and interpreted there, so naming it means disassembling AppKit or HIToolbox.

func EventRecordFromCGEvent

func EventRecordFromCGEvent(event coregraphics.CGEvent) (*EventRecord, error)

EventRecordFromCGEvent returns the EventRecord backing event.

__CGEvent is opaque, so the record is found by probing the offsets at which its pointer has been observed and accepting the first candidate that declares itself 248 bytes long. That check is what makes the result trustworthy: an unrelated pointer field is very unlikely to address memory whose second word is exactly 248. The record is owned by event and stays valid only as long as the caller holds a reference to it.

An error is returned rather than a possibly-bad pointer when no candidate qualifies, which is the expected result if the layout changes.

func (*EventRecord) Bytes

func (r *EventRecord) Bytes() []byte

Bytes exposes r as the raw buffer SLPSPostEventRecordTo expects. The result aliases r, so a write through r after the call is visible through the slice.

The array length is written out rather than derived so that a change to EventRecord fails to compile here instead of silently truncating the buffer.

type ProcessSerialNumber

type ProcessSerialNumber = applicationservices.ProcessSerialNumber

ProcessSerialNumber uniquely identifies a process in the macOS window system.

func WindowOwnerPSN

func WindowOwnerPSN(w Window) (ProcessSerialNumber, error)

WindowOwnerPSN returns the Process Serial Number of the process owning the window.

type Space

type Space uint64

Space represents a macOS display space (virtual desktop).

func ActiveSpace

func ActiveSpace() (Space, error)

ActiveSpace returns the identifier for the currently active space.

Example
package main

import (
	"fmt"

	"github.com/tmc/apple/x/skylight"
)

func main() {
	space, err := skylight.ActiveSpace()
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}
	fmt.Printf("active space: %d\n", space)
}

func SpacesForWindow

func SpacesForWindow(w Window) ([]Space, error)

SpacesForWindow returns all space IDs that contain the specified window.

type Window

type Window uint32

Window represents a window managed by the WindowServer.

Jump to

Keyboard shortcuts

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