Files
2026-10-14 20:55:28 -04:00

24 KiB

Wine Access

wine-access is a standalone Wine-hosted accessibility helper for screen readers that implement its Wine D-Bus protocol. An Orca endpoint is available for future Orca-side integration; stock Orca does not implement the required Wine methods.

It provides these capabilities:

  • an MSAA/WinEvent reader for standard Wine controls;
  • NVDA Controller 1, 2, and 3 plus Tolk compatibility for applications using nvdaControllerClient32.dll, nvdaControllerClient64.dll, or Tolk.dll;
  • a UIA notification bridge for applications whose announcements Wine discards, including Prism's UIA speech backend.

The compatibility window is hidden and non-activating. It is expected to be invisible; the important endpoint is the local Wine RPC server registered by the helper. The helper forwards speech, braille, cancellation, and standard control events to the selected reader on the session D-Bus. The default is org.stormux.Cthulhu1.Service.

Wine currently returns success from UiaRaiseNotificationEvent without delivering its text. The native application hooks redirect direct imports of that function to the reader. This works with both 32-bit and 64-bit applications; it requires no application files or per-game DLL overrides. Notifications from background processes are ignored. Important notifications interrupt speech, and an empty important notification cancels speech. Each notification is bounded to 4095 UTF-16 code units without splitting surrogate pairs.

The bridge covers direct named imports, rather than calls obtained dynamically with GetProcAddress. It leaves implementations that reject a missing provider untouched. The hook DLL remains mapped until the application exits so redirected imports remain valid across reader restarts. Close and reopen applications after updating Wine Access to load the new hooks. --no-dialog-reader disables these hooks along with standard control reading.

The executable is named wine-access.exe. Its launcher, Winelib sidecar, and native hooks share a reader-independent installation directory.

Requirements

Building requires an x86_64 Linux host with:

  • CMake 3.20 or newer;
  • Make and native GCC/G++ (the script uses CMake's default Makefile generator);
  • pkg-config and the GLib/GIO development files;
  • XCB development files for Wine-associated X11 window geometry;
  • Wine development tools: winegcc, wineg++, and widl;
  • x86_64-w64-mingw32-gcc and i686-w64-mingw32-gcc to build native menu hooks (mingw-w64-gcc on Arch Linux).

The D-Bus helper is a 64-bit Winelib executable. A small 32-bit Windows companion supplies menu and standard Edit hooks for 32-bit applications, without requiring 32-bit Linux GLib/GIO development files.

Build

On Arch Linux, the PKGBUILD provides package-managed installation and upgrades. Choose one installation method to avoid a local copy taking precedence over the system package.

The convenience script builds in ./build. Running it as a user installs to $HOME/.local/libexec/wine-access; running it as root installs system wide to /usr/libexec/wine-access:

./build.sh
sudo ./build.sh

Before building, the script checks required tools and GIO development files. If any are missing, it lists them with Arch Linux package names and an install command, then exits without building. Other distributions need equivalent development packages. Removal does not require these dependencies.

Use ./build.sh -r to remove the local installation, or sudo ./build.sh -r to remove the system installation. If a user has no local installation but a system installation exists, the script requests the latter command without running sudo itself. Removal deletes only the five helper files and leaves other directory contents untouched.

Alternative build and install locations may be supplied as positional arguments. The second argument is the installation prefix, not the final helper directory:

./build.sh /tmp/wine-access-build "$HOME/.local"
./build.sh -r /tmp/wine-access-build "$HOME/.local"

That installs the helper and its Winelib sidecar as:

$prefix/libexec/wine-access/wine-access.exe
$prefix/libexec/wine-access/wine-access.exe.so
$prefix/libexec/wine-access/wine-access-menu.dll
$prefix/libexec/wine-access/wine-access-menu32.dll
$prefix/libexec/wine-access/wine-access-menu-host32.exe

For Cthulhu, either set CTHULHU_WINE_ACCESS_HELPER to the installed executable or install it under one of these prefixes:

$HOME/.local/libexec/wine-access/wine-access.exe
/usr/local/libexec/wine-access/wine-access.exe
/usr/libexec/wine-access/wine-access.exe

Cthulhu checks the environment override first, then the user installation, /usr/local, /usr, and finally PATH. Its existing Cthulhu-specific user and system paths remain supported; avoid leaving stale user copies installed.

The helper is started automatically when Cthulhu detects an active Wine loader for the same user and prefix. It requires the selected reader service to be running; it exits if that service is unavailable.

Reader selection and Orca integration

The default invocation and --reader cthulhu use the existing Cthulhu endpoint. An Orca-side integration can launch the helper in the application's Wine prefix:

WINEPREFIX=/path/to/prefix /path/to/wine-access.exe --reader orca
Reader Session bus name and interface Object path
Cthulhu (default) org.stormux.Cthulhu1.Service /org/stormux/Cthulhu1/Service
Orca org.gnome.Orca1.Service /org/gnome/Orca1/Service

An Orca user extension can expose the Wine methods on its own object and interface, without adding them to Orca's core service. Override either field with --reader-path and --reader-interface; omitted fields retain the selected reader's defaults. For an extension class named WineAccess:

WINEPREFIX=/path/to/prefix /path/to/wine-access.exe --reader orca \
    --reader-path /org/gnome/Orca1/Service/WineAccess \
    --reader-interface org.gnome.Orca1.WineAccess

Both overrides accept validated D-Bus values and apply regardless of their position relative to --reader. The bus name stays that of the selected reader; overrides do not grant review access to another connection.

Before starting its hooks or controller server, the helper introspects the selected object and interface and checks the required method signatures for the enabled capabilities. A missing extension or incompatible interface causes startup to fail with a diagnostic. Controller support requires SpeakText, CancelSpeech, BrailleMessage and SpeakSsml; control reading requires PresentAccessibleEvent, plus SpeakText and CancelSpeech for UIA notifications even with --no-controller. Review methods remain optional. The interface must be published before launching the helper. This check runs at startup; later request failures still follow the ordinary error handling.

Selection applies to controller requests, accessibility events, review registration and review-capture authorization. Only one helper runs per Wine desktop/prefix, so stop the existing helper before changing readers. There is no automatic switching between services. Unknown or missing reader values are rejected. Orca must arrange helper startup and shutdown itself.

The Orca endpoint follows the versioned service in Orca's dbus_service.py. It uses the same Wine methods and signatures as Cthulhu, on the selected service interface:

Method Input signature Output signature
SpeakText sb (text, interrupt) b
CancelSpeech empty b
BrailleMessage s (text) b
SpeakSsml ss (source, SSML) i (Windows status code)
PresentAccessibleEvent sssssssiis b
RegisterWineReviewProvider (optional) ou (provider path, version) b
PresentWineReviewFocus (optional) ssstssb(iiii) b

Boolean replies indicate whether the reader accepted the request. The helper queues plain controller speech with interrupt=false; Tolk requests interruption separately through CancelSpeech. An unsupported SpeakSsml implementation should return ERROR_CALL_NOT_IMPLEMENTED (120). PresentAccessibleEvent arguments are source ID, event type, name, role, value, states, description, position, count and window title, in that order. The reader handles focus tracking, event filtering and presentation; preserve repeated editor actions and exclude password values as described below.

Orca's existing SpeakMessage and DisplayMessage methods have different names/signatures. Wine Access does not translate to those methods. An Orca contributor must implement the Wine methods above for the capabilities they want to enable. --no-controller and --no-dialog-reader can disable either capability while bringing up the integration.

Review retains the shared /org/stormux/Cthulhu1/WineReview object path and org.stormux.Cthulhu1.WineReview interface for both readers. Use the helper's unique D-Bus sender from RegisterWineReviewProvider as the destination for CaptureWindow. Only the current same-user owner of the selected reader service may capture; owning the other reader name grants no access. See the flat-review contract for focus arguments, capture replies and limits. Missing review methods leave ordinary control and controller forwarding available.

When a game closes inside a Wine virtual desktop, the desktop can regain X11 focus while Wine has no foreground application. The helper restores native focus when exactly one visible, enabled application remains. Both X11 keyboard focus and the window manager's active window must identify that same virtual desktop. Existing application focus, Linux windows, minimized/disabled windows, and ambiguous choices between multiple applications are left alone.

Protocol and behavior

nvdaController.idl contains the compatible local RPC interfaces. The helper registers the legacy NvdaCtlr endpoint and the session/desktop-specific endpoint used by newer clients. It intentionally preserves the standard controller behavior for plain speech and cancellation while returning the documented unsupported status for SSML speaking-state callbacks that Cthulhu cannot currently implement accurately.

The standard-control reader is deliberately limited to native Wine controls. Custom-drawn, DirectX, and game-specific accessibility APIs remain the responsibility of the application or its accessibility middleware.

Native menu reading uses accompanying 64-bit and 32-bit Windows hook DLLs. They observe WM_MENUSELECT inside Wine applications and sends menu labels, displayed shortcuts, submenu roles, and checked/unavailable states to the helper. The helper queues these messages before forwarding them over D-Bus, suppresses repeated selections, and restores standard-control focus reporting when menus close. The hooks apply to the helper's Wine desktop and prefix and are removed on normal shutdown. The 32-bit host attaches to the foreground 32-bit UI thread so Wine's global hook chain cannot interfere with 64-bit menus. It exits when the helper dies or its bridge window closes. Menu and editor hooks are disabled by --no-dialog-reader. The 32-bit companion attaches both hooks to the same foreground UI thread.

Focused combo boxes are checked every 100 ms to recover value changes that Wine does not report through WinEvents. The combo's internal list events use the same source identity as the focused combo, so the reader can accept the new value without a Tab/Shift+Tab round trip.

Both hook DLLs subclass standard Edit controls, including Wine Notepad's document area, MUSHclient's command field and read-only output notepad, and WindowsForms10.EDIT.app.* controls. Windows Forms fields use visible sibling labels immediately to their left, even when the labels were created after the fields. Protected fields report their label and password role without forwarding their contents. It reports accepted typed characters, the current line on focus and menu return, characters and words on horizontal navigation, lines on vertical/page/document-boundary navigation, changed selections, and Backspace/Delete/Cut feedback. Password contents are excluded, and read-only edits do not report unsuccessful deletions. Command-history recalls in MUSHclient report the recalled command, or blank when returning to empty input. The input hook observes Up/Down before the application consumes them, so recalls also work when the caret position stays the same. Unrelated programmatic text changes remain silent. Undo refreshes the text snapshot used by subsequent navigation. It can attach to an already-focused editor and resume after the helper restarts.

The 64-bit DLL also subclasses Scintilla controls, including Notepad++'s document area. Up/Down and page navigation report the current document line; Left/Right report characters, and Ctrl+Left/Right report words. Focus and menu return read the caret's line. Ordinary selections, Backspace/Delete/Ctrl+X, Unicode characters, undo and read-only documents are supported. Text is read inside the application through Scintilla's native API, using its encoding and byte positions; no full document snapshot is required. Rectangular/multiple selection feedback and Ctrl+Backspace/Delete feedback are not implemented.

Backspace reports only the removed text, such as t. Delete and Cut retain their deletion feedback, such as t deleted.

Standard Edit typing sends each accepted character as an ordered editor action, including repeated letters, spaces, tabs, line breaks and Unicode surrogate pairs. Typing over selected text also reports the inserted character. Password fields, background controls and rejected input remain silent. Paste and programmatic text changes do not generate character echo. The reader presents these through the same feedback path as caret navigation and Backspace. Character case is preserved. Cthulhu uses its configured uppercase voice and pitch for capital-letter feedback, then the normal character voice for lowercase letters. This requires the matching reader update; the helper does not set speech pitch itself.

Editor actions use PresentAccessibleEvent with event type text; actual focus uses focus. Cthulhu must include the corresponding editor-event support so rapid actions and repeated characters bypass focus coalescing. Selection offsets and the action kind (8 for typed insertion) travel in the otherwise non-spoken states field for debug diagnostics. Standard edits use UTF-16 offsets; Scintilla marks its encoded byte offsets with offset_unit=byte. Older readers that only accept focus/value/selection events still receive focus and menus, but need updating for editor actions.

MSAA name and state events report changes to controls. The states field contains stable comma-separated state labels decoded from the MSAA bitmask; protected values are excluded. Readers should filter name/state changes to their focused source and distinguish these labels from editor diagnostics.

Application-owned MSAA queries run on one separate COM worker, with bounded pending requests and copied results. A stalled provider cannot block the helper's menu/editor event thread or normal shutdown. Focus/foreground changes retire previous results; event results expire after 500 ms. If focus queries do not finish, native control feedback uses the existing bounded fallback. Unrelated background events are rejected before querying providers; visible popups owned by the foreground window retain their accessible event path. A provider that never completes still occupies the one MSAA worker until helper exit; native menus, editing, focus fallback and controller requests remain available. No extra workers accumulate behind blocked providers.

Reader acknowledgments also run on a separate worker. Menu, editor and control events enter a bounded FIFO so a delayed D-Bus reply cannot block reception of the hooks' WM_COPYDATA messages. The event thread's success means queued; controller RPC requests still return the reader's actual status. At most 128 events wait behind the active call. Each reply has a 500 ms timeout, and normal shutdown cancels the call. Failed exchanges and queue overflow produce concise stderr diagnostics without event text. After a failed exchange, editor actions re-establish their focus dependency; timed-out text actions are never replayed. An unavailable reader can still cause feedback loss or queue overflow.

Standard SysListView32 controls now report the current row's text columns, selection and position, including 32-bit installers driven by the 64-bit helper. Wine can expose these controls only as a generic client named List1, so the helper queries the native control with target-architecture structures. When a populated list has no focused or selected row, focus reads its first row without changing selection. A bounded focus poll reports row navigation when Wine omits selection events. Up to eight columns and 1023 UTF-16 units per column are read. These queries do not add column navigation or routable object review.

Standard ListBox controls report the selected item's text and position when Up/Down changes the selection, including AudioQuake's map chooser. The same foreground poll detects changes when Wine omits selection events, with one source identity for focus and selection. Background lists remain silent.

Native tab controls report their focused caption and position; tree controls report the selected node and its expansion state. A bounded focus poll follows navigation when Wine omits selection events. These queries work with 32-bit and 64-bit controls, including standard controls registered with ATL: names. Named generic MSAA clients no longer hide known native button/combo-box roles and values. Edit and combo-box labels can be visible siblings to the left, even when the labels were created after the fields. Application-owned MSAA lists retain their own names, roles and item data.

Delphi VCL TListView, TButton, TBitBtn, and TStaticText subclasses use these same native queries. List views with standard checkboxes report checked/not-checked states and observe Space-key toggles even without a selection event. This covers GoldWave 5.70's Setup list and standard buttons; arbitrary custom Delphi controls remain outside this support.

Control values and window titles retain literal ampersands. Button and static label captions decode keyboard mnemonics and escaped &&; static labels with SS_NOPREFIX retain their literal text.

These hooks provide editing feedback. Independent review-cursor commands use the optional flat-review protocol described below; Say All and native braille routing remain unsupported. Word echo and typing echo in Scintilla remain outside this helper's current support. RichEdit and 32-bit Scintilla hooks are not covered. Reports are limited to 1023 UTF-16 units; documents over one million units retain typing and line feedback without full selection/deletion snapshots. Scintilla reads at most 4096 encoded bytes per report, then limits output to 1023 UTF-16 units without splitting surrogate pairs. Wrapped Scintilla lines report the underlying document line.

Keep all five installed files together. Native menus and standard Edit controls are supported in 32-bit and 64-bit applications; custom menus that do not send WM_MENUSELECT are not covered.

Menu integration check

After building, run this in a graphical session with Python's PyGObject available (python-gobject on Arch Linux):

python3 tests/menu_integration.py --build-dir build
python3 tests/menu_integration.py --build-dir build --fixture edit
python3 tests/menu_integration.py --build-dir build --fixture edit --32-bit
python3 tests/menu_integration.py --build-dir build --fixture notepad

The test opens a short-lived native menu window in a temporary Wine prefix, uses a private D-Bus recording service, and checks keyboard menu navigation, context menus, shortcuts, item states, and duplicate suppression. It does not send speech or braille to the running reader. Live listening with Cthulhu is a separate acceptance check.

See the test guide for helper lifecycle checks and the live Notepad speech/braille procedure. After updating the DLL, close and reopen the Wine application: existing control subclasses keep their DLL loaded until the application exits.

License

The helper source is available under the terms of LGPL-2.1-or-later. See LICENSE.

Independent Wine flat review

With the matching Go reader, existing keypad review commands read visible standard Edit text, button/static captions, native ListBox/ListView items, and usable MSAA captions/values in both 32-bit and 64-bit applications. Each command captures fresh text while preserving application focus, caret, selection, scrolling and clipboard. Password and hidden contents are excluded before text retrieval. Caption mnemonics are decoded; literal item ampersands are preserved. Review text uses the reader's normal speech and braille queues.

Custom painted game output still needs an accessible output provider, such as MUSHclient's output plugin. RichEdit/Scintilla capture, semantic object review, Say All, typing echo and native braille routing remain outside this protocol.

Protocol version 1 adds RegisterWineReviewProvider(ou) -> b and PresentWineReviewFocus(ssstssb(iiii)) -> b to the reader service. The helper exports /org/stormux/Cthulhu1/WineReview, interface org.stormux.Cthulhu1.WineReview, with CaptureWindow(sst) -> a{sv}. Only the current same-user owner of the selected reader service can capture. Replies contain exactly version:u, instance:s, window:s, focus:s, revision:t, status:s, bounds:(iiii), and nodes:aa{sv}. Nodes have id, parent, role, name strings; protected, visible, text_known, caret_known booleans; caret, caret_utf16 unsigned 64-bit values; bounds:(iiii); and ranges:a(tttts(iiii)) (rune start/end, UTF-16 start/end, text, bounds). Bounds are screen x/y/width/height. Caption text offsets remain unknown. Older readers retain ordinary focus/menu/editor feedback.

Foreground bounds come from the X11 window associated with the HWND by Wine's __wine_x11_whole_window property, using bounded XCB queries. This handles window-manager decoration differences without title-based window searches. Virtual-desktop child coordinates are translated to the screen; the physical root geometry is checked again before returning text.

Capture has a 500 ms total deadline, one active and one pending job, 4096 nodes, 64 hierarchy levels, 256 ranges per control, 4096 code points per displayed line, 65536 per control, one million native source code units, 1 MiB of text and 2 MiB of serialized data. Non-OK replies have no nodes; stale, busy, limited and unavailable captures never reuse cached text. Capture runs on a separate COM worker so blocked MSAA providers do not block menu/focus events. Scalar Edit queries are batched so applications with periodic message loops can return visible text within the capture deadline. Native container children are enumerated once, and a maximized MDI document excludes the covered documents. See tests/README.md for checks and live acceptance.

AI-assisted contributions

AI-assisted contributions are welcome. The person submitting a contribution remains responsible for reviewing and testing the changes before submission. Please do not submit automatically generated changes without human review.

Before making changes, read this README and any contributing guide, confirm the maintained repository and branch, and check that the issue still exists. Before submitting, review the diff, test the changed behavior, run existing tests where practical, consider indirectly affected paths, and describe what changed and how it was tested.

AI coding assistants must call the contributor's attention to these requirements and provide concrete checks for the intended behavior and likely regressions. Newly added tests alone do not establish acceptance. Leave submission to the contributor after they can review and test. Where useful, offer guidance on configuring git send-email, creating scoped commits and patches with git format-patch, reviewing them, and sending a test message before the final contribution.