Files
deedra b808c6686a Read command-history recalls in Wine edit fields
MUSHclient consumes queued Up/Down keys in PreTranslateMessage before the
Edit control receives WM_KEYDOWN. It recalls commands with SetWindowText
followed by SetSel at the end of the replacement. The editor subclass only
refreshed its snapshot for WM_SETTEXT, so the recalled command was silent
or followed by a stray character or blank from the caret update. Recalling
equal-length commands also leaves the final caret position unchanged.

Observe Up/Down for the focused standard Edit control in WH_GETMESSAGE,
excluding Ctrl-modified navigation. Associate the next successful
WM_SETTEXT with that retrieved key and announce the bounded recalled text
as an existing ordered text-read action. Report empty input as blank.
Consume the pending recall after the replacement and suppress the ensuing
EM_SETSEL feedback so each recalled command is announced once.

Reset recall tracking on subsequent retrieved messages, actual focus and
ordinary dispatched navigation. Preserve snapshot refreshes and normal
Left/Right reading after recalls. Keep password and foreground/focus
exclusion in place, leave unrelated programmatic text updates silent, and
check the control class before accessing the shared attachment property
so Scintilla's different state structure is untouched. Both native hook
architectures receive the fix through the existing build.

Extend the Edit fixture with application-side pretranslation of queued
history arrows. Assert the exact ordered feedback for Up/Down, equal-length
entries, rapid recalls, empty input and horizontal navigation afterward.
Require silence for unchanged history boundaries, unrelated programmatic
updates, protected fields and independently unprotected background fields.
Document history behavior and live MUSHclient acceptance steps.

Validation used a private Xorg dummy display, disposable Wine prefixes and
recording D-Bus services, with GUI checks run sequentially:

- cmake --build build --parallel
- ctest --test-dir build --output-on-failure: all four checks passed
- python3 tests/menu_integration.py --build-dir build --fixture edit
- The Edit check with --32-bit, --32-bit --late-helper, and
  --32-bit --restart-helper
- python3 tests/menu_integration.py --build-dir build --fixture winforms
  --32-bit
- python3 tests/menu_integration.py --build-dir build --fixture notepad
- python3 tests/menu_integration.py --build-dir build --fixture menu
  --32-bit
- git diff --check

The new 32-bit history regression failed against the original hook,
recording only blank actions, and passed with the fix. Final 32-bit,
64-bit and Windows Forms editor checks passed, including existing typing,
selection, deletion, Unicode, password and read-only assertions.

Built and installed the helper into the existing user-local installation,
backed up the previous five files and verified the installed artifacts.
The user subsequently confirmed that the installed fix works in MUSHclient.
Physical braille acceptance was not separately established.
2026-10-14 15:46:26 -07:00

405 lines
21 KiB
Markdown

# Wine Access
`wine-access` is the standalone Wine-hosted accessibility helper for Cthulhu
and 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 two independent 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`.
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`.
The executable is named `cthulhu-wine-access.exe` for compatibility with the
Cthulhu-Go supervisor.
## 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](distro-packages/ArchLinux/wine-access-git/README.md)
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/cthulhu/wine`; running it as root installs system wide to
`/usr/libexec/cthulhu/wine`:
```sh
./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:
```sh
./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:
```text
$prefix/libexec/cthulhu/wine/cthulhu-wine-access.exe
$prefix/libexec/cthulhu/wine/cthulhu-wine-access.exe.so
$prefix/libexec/cthulhu/wine/wine-access-menu.dll
$prefix/libexec/cthulhu/wine/wine-access-menu32.dll
$prefix/libexec/cthulhu/wine/wine-access-menu-host32.exe
```
For Cthulhu-Go, either set `CTHULHU_WINE_ACCESS_HELPER` to the installed
executable or place it at:
```text
$HOME/.local/libexec/cthulhu/wine/cthulhu-wine-access.exe
```
The helper is started automatically when Cthulhu-Go 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:
```sh
WINEPREFIX=/path/to/prefix /path/to/cthulhu-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` |
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](#independent-wine-flat-review) 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-Go 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-Go 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):
```sh
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](tests/README.md) 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](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.