Skip to content

lys-ui accessibility ​

What every widget exposes, the keyboard-first guarantee, and the in-process AT-SPI bridge that serves both to screen readers.

Decision ​

lys-ui speaks AT-SPI in-process, in every build. The bridge (internal/atspi) starts the way Qt's does: while the desktop asks for assistive technologies (org.a11y.Status IsEnabled or ScreenReaderEnabled on the session bus), it starts and stops with that switch. Without a screen reader and with the setting off, the tree is never walked. With Orca running, the application is accessible with no app code. The application chooses the mode with Application.SetAccessibility (A11yAuto by default, A11yOn, A11yOff). The user can override it with LYS_UI_A11Y=none|atspi, which wins, like GTK_A11Y.

Semantic model (widget/a11y.go) ​

  • Role mirrors the AT-SPI roles: button, label, entry, text-area, slider, switch, check-box, progress-bar, scroll-area, list, menu, tab-list, and the rest of the widget set.

  • Describe(w) snapshots:

    • role and name (SetAccessibleName, else the tooltip, else the label text)
    • text contents, with caret and selection as rune offsets
    • checked state and min/max/value/step
    • editable, multiline and enabled (effective, with ancestors folded in)
    • focusability and bounds

    A read-only text widget reports editable false. A disabled one reports enabled, editable and focusable all false.

  • DescribeTree(root) walks in paint order, the order focus traversal uses.

Keyboard-first guarantee ​

Every interactive control is reachable by Tab and activatable by Enter or Space (the GTK pair); KeyActionForSym is the single keysym→action map. Pinned by widget/a11y_test.go (roles and snapshots for every widget, Enter/Space activation through the router) and cmd/lys-ui-hello/a11y_test.go (the showcase's Tab order, wrap, shift+Tab, the TextArea Tab trap with its ctrl+Tab escape, and activation of every focused control).

The bridge ​

The bridge is snapshot-driven. A sampling pass runs on the loop goroutine, rebuilds the tree of widget.A11yState snapshots keyed by stable widget identity, and diffs it against the previous pass. Every difference becomes an event, and every D-Bus handler answers from the published snapshot, never touching widgets from the D-Bus goroutine.

  • Registration. The bridge:

    • discovers the accessibility bus through org.a11y.Bus.GetAddress
    • claims org.a11y.atspi.lysui.p<pid>
    • exports its root at /org/a11y/atspi/accessible/root
    • embeds through org.a11y.atspi.Socket.Embed

    An absent or older registry is a Debug note, never a failure.

  • Interfaces.

    • Accessible
    • Component (window-relative extents and hit testing)
    • Text (contents, caret, selections, boundary readers, character geometry)
    • Action (performed through the widget's own click path on the loop)
    • Value (with a setter for sliders)
    • Application on the root
    • org.a11y.atspi.Cache at /org/a11y/atspi/cache

    Each comes with full org.freedesktop.DBus.Properties and Introspect support.

  • Programmatic side.

    • Component.GrabFocus moves focus through the window's router.
    • Text's caret and selection setters reach any widget.TextSelector.
    • Geometry probes are measured on the loop, bounded so that a stalled loop answers empty instead of hanging the AT.
  • Events.

    • focus (deprecated and modern forms)
    • state-changed (checked, pressed, enabled, sensitive, editable)
    • text-caret-moved, TextChanged, TextSelectionChanged
    • bounds-changed, children-changed
    • PropertyChange for name and value

    The diff is deterministic: focus first, then per-node state by ascending stable id.

  • Cadence. Sampling runs every 100ms by default. An application that serves the bridge itself picks another interval with Application.ServeAccessibility(app.A11YOptions{Poll: d}).

Limits ​

  • Component coordinates are window-relative, because the compositor owns the window's screen position on Wayland.
  • Text attributes (GetAttributeRun) report none.
  • The Selection and Table interfaces are not implemented.
  • Wayland has no compositor-level accessibility protocol, so the bridge is the whole accessibility path.

Verification ​

  • just atspi runs go test ./internal/atspi/. A private dbus-daemon plays the accessibility bus, and a second connection plays the registry and the AT: it walks the tree, reads every interface, drives the setters and receives the events.
  • go run -tags atspi ./cmd/lys-ui-a11y runs the a11y demo with the bridge forced on (A11yOn) whatever the desktop's bus says, so a screen reader or accerciser finds it deterministically; without the tag it follows the desktop's switch like every app.
  • just atspi-verify runs against real at-spi2-core: the bus launcher and registryd on a private session bus, with a client that finds the application, reads its Cache, walks to an entry, reads its text and moves its caret.

Orca against a live desktop is a manual check.

Released under the MIT License.