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, orTolk.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-configand the GLib/GIO development files;- XCB development files for Wine-associated X11 window geometry;
- Wine development tools:
winegcc,wineg++, andwidl; x86_64-w64-mingw32-gccandi686-w64-mingw32-gccto build native menu hooks (mingw-w64-gccon 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.