21 KiB
LibreOffice integration
Current Orca is the primary behavioral reference for LibreOffice. Python
Cthulhu remains available on the legacy branch for historical comparisons:
git show legacy:src/cthulhu/scripts/apps/soffice/script.py.
Do not switch the active Go checkout to inspect that reference.
Architecture
Application handling runs after screen-lock and foreground validation, before
generic focus and text filtering. internal/app/application_behavior.go
routes copied accessibility events by verified provider process identity.
The LibreOffice handler lives in internal/app/libreoffice.go.
It can normalize an event, request focus adoption, or suppress presentation.
The reader retains window ownership, input policy, pending-output cancellation,
review, and speech/Braille delivery. Ordinary applications keep their existing
event path. Steam's existing integration remains in place.
internal/atspi/libreoffice_native.go captures bounded Calc metadata on the
native owner thread. It does not enumerate a sheet. The controller and
presentation code never retain native objects or perform provider queries.
internal/presentation/libreoffice.go owns the corresponding speech
formatting; LibreOffice Braille formatting stays beside its controller.
Logical focus belongs to the existing event loop. Selection comparison keeps one bounded snapshot for the focused sheet, keyed by its application bus and accessible sheet path and admitted only within the owned document window. It is retired on sheet/application focus changes and ordinary owned-focus reset, including destruction, foreground loss, and screen lock. There are no selection workers, timers, or retained native references. Dynamic header choices live in the adapter's bounded, synchronized store of copied identities and coordinates. Commands update that store without provider queries; the native owner captures header text. Choices use application bus, document/view, window and the provider's sheet name. Calc recreates table objects on tab switches, so a table path cannot identify a sheet across those switches. Focus loss cancels output while preserving document header choices. Use the existing input machinery for additional Office commands. Application code must not bypass security/foreground gates or manage its own speech connection, key grabs, or background event loop.
Keep generic table operations and genuinely shared fixes in their existing subsystems. Keep verified LibreOffice exceptions in the dedicated files. Adding later Office features should extend these boundaries, without reorganizing unrelated reader code or building a speculative plugin framework.
First slice
- Calc active-cell events establish logical focus, including after editing and when the sheet changes.
- LibreOffice frame activation and X11 foreground recovery establish window ownership without the generic focused-descendant search. Calc's following cell/editor events establish document focus. Searching the previous view during workbook activation caused stale-object queries and timeouts on return; leaving activation on the frame avoids that work. Dialogs retain normal focused-control resolution.
- Cell contents and addresses are separate. Normal speech gives the value, then the address; an empty value gives “blank.” Only-displayed-text mode omits the address and the synthetic blank label.
- Recorded Braille preserves value spacing followed by the address. A cell component is not an editable text route.
- F2 resolves a temporary editor's focused paragraph when available. If the paragraph appears later, its first owned text/caret event establishes editor focus before generic filtering.
- Post-editing sheet focus is adopted silently. Spreadsheet capacity and cell/container state chatter are omitted as in Orca.
- Ancestor focus claims are suppressed when captured ancestry identifies the existing focused spreadsheet object.
- Non-editor caret events do not displace a focused cell. Selection events do not promote the first selected cell of a range into logical focus.
References inspected: Orca commit cd58a88ed (2026-10-14),
scripts/apps/soffice/script.py, script_utilities.py,
speech_generator.py, braille_generator.py, and shared
speech_generator.py / ax_utilities_table.py.
Formula slice
- Normal cell speech appends “has formula” after the value and address. Only-displayed-text mode continues to omit synthetic metadata.
- Basic Where Am I includes the formula after the cell information.
- Cthulhu+= presents the current cell's formula in the desktop layout,
following Orca's
where_am_i_presenter.py:present_cell_formula. The semantic command ispresent-cell-formula; it is listed in preferences and Learn Mode. As in current Orca, the laptop layout leaves it unbound; assign a shortcut in Key Bindings if desired. - A cell without a formula gives its value, or “empty” for an empty cell. The command currently applies to Calc cells; other focus reports “Not in a table.” Temporary F2 editors are separate objects, so leave editing before requesting the cell's formula.
- Native capture reads the uncached
formulaattribute (orFormula, as accepted by Orca) alongside the existing bounded cell metadata. Formula string-literal spacing is preserved. Expressions are bounded to the native snapshot field limit, retaining the beginning of long expressions. Commands consume the latest accepted focus snapshot; they never scan a sheet or query a provider from the event loop. A provider change without an event requires revisiting the cell to refresh its snapshot. - Formula speech uses the existing command and speech queues, foreground and screen-lock gates. Normal Braille retains the value and address, with no new routes or formula suffix. The explicit formula command uses the existing system-message flash path and its Braille preferences, as in Orca.
Selection slice
- Shift+Arrow selection growth and shrinkage announce selected/unselected coordinates. Adjacent cells form compact rectangles; disjoint cells retain their gaps. The active anchor is left to ordinary cell presentation.
- Full rows and columns use row/column labels. Select All says “All cells selected”; leaving a selection says “Selection cleared.” Ctrl+A can first select the used area; press it again to select the entire sheet. These announcements require a provider event. An independent AT-SPI observer on LibreOffice 26.2.6.3 confirmed that Shift+Up after extending whole rows, and Shift+Left after extending whole columns, change the selection without emitting a selection event. Ctrl+Home can also clear those selections without an event. Immediate announcements for those cases remain unsupported; the next selection event refreshes the snapshot. The reader does not poll or infer selection changes from keystrokes.
- Small selections capture at most 64 cells, rows, or columns. Messages with more than eight rectangles use a count. Larger selections use a count and two boundary samples for duplicate suppression, without treating endpoints as proof of a filled rectangle. Overlapping count-only selections report the new total rather than falsely announcing the entire old range unselected.
- The GTK bridge caps cell counts at 2147483647; partial-sheet selections at that limit say “At least … cells selected.” Whole-sheet selection is checked separately. Large selections with identical counts and boundary samples cannot be distinguished by this bounded snapshot; interior-only changes may therefore be suppressed. Full topology/delta reporting remains future work.
- Native capture uses a fixed number of count queries and at most 64 selected positions. It requests no selected-row/column arrays. Ordinary small cell selections skip full-row/column counts; LibreOffice itself scans its row marks when larger selections need a selected-row count. Fixed RPC counts do not guarantee a provider's response time.
- Selection events keep both logical and retained native focus on the active cell. Formula commands, object review, cell Braille frames, and routing continue to use that cell. Selection messages use the existing system-message Braille flash path and preferences. Only-displayed-text mode suppresses them.
- Current Orca's selection callbacks and utilities guide the behavior. The LibreOffice spreadsheet provider and GTK selection bridge were also inspected for selection ordering, row-count cost, and count limits.
Dynamic headers slice
- In Preferences → Key Bindings, assign shortcuts to “Use the current row
as Calc column headers,” “Use the current column as Calc row headers,” and
the corresponding clear commands. All four are available in both layouts
and Learn Mode, and are unbound by default. Orca's Cthulhu+Shift+C equivalent
conflicts with Cthulhu's existing clipboard command; it is preserved.
Semantic commands are
set-column-headers-row,set-row-headers-column,clear-column-headers-rowandclear-row-headers-column. - Set column headers from any cell in the heading row, and row headers from
any cell in the label column. Each axis can be changed or cleared separately.
This follows Orca's shared
table_navigator.pyandax_utilities_table.py, rather than just its soffice script. - Automatic speech prefixes the row heading when the row changes and the column heading when the column changes. A cell does not repeat itself as its own heading. Blank headings add no label. Basic Where Am I includes both headings; normal Braille includes both through its existing context path. Only-displayed-text suppresses automatic speech metadata. Formula commands, active-cell focus, selections and editor routing retain their existing paths.
- Header text is read afresh with at most two direct cell lookups per snapshot, bounded to the normal field limit. There is no row/sheet enumeration, polling, worker or native reference retained by the header feature. Provider response times still apply. Commands consume the accepted cell snapshot; a provider mutation without a new event requires revisiting the cell.
- Choices survive sheet/workbook switches and lock/unlock for this reader session. They are separate per document/view and sheet; they are not saved to the workbook or settings. A reader restart or document/window destruction clears them. The store accepts up to 64 sheets; clearing both axes frees a slot.
- Sheet names come from the verified provider's uncached
page-nameattribute, preserving exact whitespace. Missing or oversized identity metadata disables these commands. This metadata is exposed by LibreOffice's spreadsheet document provider. Tab-list child changes and tab-name events conservatively clear choices for that document window. Reassign headers after adding, removing or renaming sheets. This depends on provider events; silent structural mutations cannot be detected. Tab selection alone preserves choices. Merged headings and structural row/column edits remain unverified; header choices are coordinates, not references that follow moved content.
Next slices
- Spreadsheet full-row reading needs a bounded, explicitly defined extent; the generic setting is not full Calc support.
- Writer paragraph navigation, selection, Say All, spell-check presentation, and Braille panning. Compare each actual workflow with current Orca.
Merged cells, overflow announcements, large-sheet responsiveness, other LibreOffice backends, and independent-workbook acceptance remain separate checks. These slices do not establish complete Office support.
Verification
dbus-run-session -- env GOCACHE=/tmp/cthulhu-go-cache go test -race ./... -count=1
dbus-run-session -- env GOCACHE=/tmp/cthulhu-go-cache go test -tags brlapi ./... -count=1
python3 tests/check_speech.py
/usr/bin/python3 tests/native_libreoffice.py
python3 tests/native_atspi.py --table-only
GOCACHE=/tmp/cthulhu-go-cache go build -o /tmp/cthulhu-libreoffice ./cmd/cthulhu
GOCACHE=/tmp/cthulhu-go-cache go test -c -o /tmp/cthulhu-libreoffice-xorg.test ./internal/platform/xorg
CTHULHU_BINARY=/tmp/cthulhu-libreoffice CTHULHU_XRECORD_TEST=/tmp/cthulhu-libreoffice-xorg.test python3 tests/native_x11.py
xvfb-run -a env CTHULHU_NATIVE_X11=1 /tmp/cthulhu-libreoffice-xorg.test -test.run '^TestNativeDispatchBeforeRelease$' -test.v
The Calc harness requires i3 as well as LibreOffice, Xvfb, xdotool, xprop, the Python AT-SPI bindings and the normal native build dependencies. It launches installed LibreOffice with a disposable profile, document, private i3 window manager, virtual X11 display, and private session/accessibility buses. It exercises the production native adapter and event loop; recorded speech and Braille are asserted for cell movement and blanks, F2 typing, Escape cancellation, Enter commit, caret movement inside an existing numeric cell, and switching between two sheets. Formula assertions cover a numeric result, an empty string result, literal spaces, replacement with a number, formula and basic Where Am I requests, and non-formula cells on a second sheet. Selection checks cover cell-range growth/shrinkage, clearing, row/column extension, Select All, disjoint cells made through the provider selection API, a 2097150-cell selection, and a partial-sheet selection above GTK's count limit. Formula commands during selection verify preservation of the active cell. The native listener tests check the desktop formula shortcut before key release; the Calc harness sends semantic requests through the production event loop. Dynamic-header checks exercise both axes, automatic axis-change speech, Where Am I, complete Braille context, heading edits, independent clearing, sheet round trips, and both return directions between workbooks with matching sheet names and different headings, using normal navigation without Ctrl+F6 recovery. Recorded speech, Where Am I and Braille must retain each workbook's own headings. Find and Replace typing and cancellation check the ordinary focused-control path and return to the sheet. Header command bindings are checked through the shared binding resolver in both layouts. No active reader is replaced and no normal Office profile is changed.
These automated checks do not establish audible speech, physical input, or physical Braille. Before submission, review the diff and test a rebuilt binary:
- In a disposable Calc workbook, populate text, numbers, formulas and blanks. Arrow between them and listen for the value followed by its address.
- Visit a formula cell: listen for “has formula,” then use Cthulhu+= and basic Where Am I. With Braille flash messages enabled, verify that the formula command flashes its result and restores the cell display. Try a formula returning an empty string, a formula containing a string literal with repeated spaces, a number, and a blank. Replace a formula with a number and revisit it; the formula cue must clear. With only-displayed-text enabled, automatic speech should omit the cue.
- Press F2, edit, cancel with Escape, then edit and commit with Enter. Check character/word navigation in an existing cell editor and continued arrows.
- Switch sheets and switch between two workbooks. Return from menus, dialogs, the formula bar, and another application; check logical focus recovery.
- Check the cell value/address, spacing, editor cursor and return to cell display on physical Braille. Text routing applies inside the editor.
- Use Shift+Arrows to grow and shrink a range; check selected/unselected addresses and continued active-cell/formula output. Try Ctrl+Space, Shift+Space, their Shift+Arrow extensions, and Ctrl+A twice. Clear each selection with an arrow. Select disjoint areas with Ctrl+click and a large area with Ctrl+Shift+Arrows; check gaps in small selections and count summaries for large ones. Enable Braille flash messages and verify selection messages restore the cell display. Enable only-displayed-text and verify selection chatter is omitted. Repeat on another sheet and in a second workbook.
- Assign the four header commands in Key Bindings. In a workbook with headings across row 1 and labels down column A, set both axes. Move horizontally and vertically; listen for the changed axis's heading, value and coordinate. Use Where Am I to hear both headings. Try a blank heading and a formula heading, edit a heading and revisit a data cell, then clear each axis separately. Check complete Braille context and that command flashes restore the updated cell frame. Verify assignment/persistence of shortcuts after reopening preferences; header choices themselves last only for this session. Switch sheets, switch to a second workbook with the same sheet names, leave Calc and return, and lock/unlock. Check independent choices and resumed headings. Add, remove and rename a disposable sheet; reassign headers after the conservative reset. Test structural row/column edits and merged headings separately; these cases have no acceptance claim yet.
- Repeat ordinary GTK table movement/multiselection, browser typing and table reading, and Steam navigation. Check lock/unlock and stale/background events.
Human review and testing remain required by CONTRIBUTING.md. Passing this
fixture alone is not sufficient for submission or live accessibility acceptance.
The tightening pass against LibreOffice 26.2.6.3 verifies the existing cell, editor, formula, selection and dynamic-header slices and adds workbook return coverage. Returning to the original workbook previously reproduced stale-object timeouts through the production adapter. Keeping LibreOffice frame activation out of the generic descendant search makes the recorded return checks pass; header choices remain independent in both workbooks. Foreground recovery uses the same policy, preventing raw sheet-state chatter before the first cell event. The virtual-display driver uses window-manager activation and waits for the heading cell before editing it.
Full default, race and BrlAPI-tagged Go suites, the speech regression gate, vet/build, parity-ledger validation, ordinary GTK table/multiselection checks, and the isolated X11 reader/listener harness pass. The former shutdown test failure is fixed by waiting for completed runtime startup before cancellation. A process-cleanup assertion also now accepts ESRCH when a killed task disappears during a procfs read. Neither correction skips the underlying shutdown checks. The earlier native probe linker failure no longer reproduces with the current listener harness.
These results establish recorded private-session behavior. Audible output, physical input/Braille, normal-profile workbook return, tab rename/add/remove event delivery, merged/structurally edited headings, large-sheet responsiveness and independent real-workbook acceptance still need their own checks. Earlier browser checks recorded an editing-title failure and one non-reproducible mode failure; this pass does not establish current live browser acceptance.
Calc startup focus diagnosis
A normal-profile startup under i3 can leave Calc accepting arrow keys while omitting active-cell accessibility events. In the observed case, cell events were followed by focus claims from the floating Form Design and Form Controls toolbars; subsequent arrows produced formula-bar text updates but no new cell events. A separate AT-SPI observer reproduced the missing events without Cthulhu. The affected workbook worked with a fresh profile, and a generic workbook reproduced the failure with a disposable copy of the normal profile. This establishes a profile-dependent LibreOffice focus/event problem; the precise profile setting responsible has not been isolated.
Press Ctrl+F6 to return focus to the document, then retry the arrows. This directly restored active-cell events in the isolated reproduction. LibreOffice documents this shortcut in its Calc accessibility keyboard guide. Automatic focus stealing would interfere with intentional toolbar use and is not part of the reader's handling.
The native Calc test activates its initial window, establishes document focus with F6 followed by Ctrl+F6, and uses ordinary key sequences within each document. Explicit window-manager activation switches workbooks and focuses the dialog. It also checks later F6/Ctrl+F6 recovery and resumed cell speech/Braille. Its fresh-profile private i3 display does not cover this normal-profile startup failure. The same production test also passed under private i3 with the copied normal profile after Ctrl+F6 recovery, asserting recorded cell speech and Braille along with editing and sheet switching. Verify startup and recovery under the actual window manager, plus audible speech and physical Braille, separately.