Merge branch 'deedra' into testing

Combine relation-only edit-label capture while retaining bounded text reads,
password-target exclusion, UTF-8 output limits and menu checkability capture.
Keep both label regression fixtures and the existing foreground test dispatch.
Preserve Wine helper diagnostics and recovery documentation, and retain the
current parity records alongside the incoming edit-label entry.

Validation:
- go test ./... -count=1 -timeout=180s
- go vet ./...
- CTHULHU_NATIVE_EDIT_LABELS=1 python3 tests/native_atspi.py
- Isolated TestNativeTextLabelRelations
- python3 tests/native_atspi.py
- Parity ledger validation: 65 rows
- Python syntax and staged diff checks

Native GTK table fixtures emitted ATK warnings; all assertions passed.
Live speech and physical Braille were not retested.
This commit is contained in:
Storm Dragon committed 2026-10-20 13:35:29 -04:00
commit dcdeab256e
7 files changed
+229 -24

No files matched your search

+29
View File
@@ -57,6 +57,35 @@ Preserve event order, GLib/cgo ownership, and native lifetimes. Balance key grab
Tentacles already use an external process protocol: preserve existing host safeguards and author APIs. Keep runnable implementations in external repositories and leave a new installation's catalog empty until repositories are added. Tentacles must not block input, focus, or speech. Use init-independent process supervision for Tentacles by default, with optional explicitly configured cgroup-v2 resource limits. Do not require a particular init system, a service manager, or administrator setup for normal Tentacle use. Preserve descendant cleanup on disable and reader exit. Treat cgroup-v2 containment as resource control, not a same-user security sandbox. Preserve HTTPS artifact and SHA-256 verification before atomic package installation.
### Local installation and reload
After building, install both Go binaries beside one another so the reader can
find its Braille helper:
```sh
install -Dm755 build/cthulhu "$HOME/.local/bin/cthulhu"
install -Dm755 build/cthulhu-braille "$HOME/.local/bin/cthulhu-braille"
"$HOME/.local/bin/cthulhu" --check-config
```
When replacement of the running reader is authorized, reload it from a terminal
in the same desktop session:
```sh
"$HOME/.local/bin/cthulhu" --replace
```
The replacement command finds the running reader through its session D-Bus
service. A terminal using a different session bus can start another reader
instead of replacing the intended one. Preserve the original reader's desktop
session environment when restarting it from automation.
Wine accessibility also needs the separate standalone `wine-access`
installation described in the [README](README.md#wine-access). Building the Go
reader does not build or refresh that helper. After restoring an overwritten
helper, check MUSHclient menus, command typing, and incoming plugin speech;
automated event recordings alone do not establish audible speech acceptance.
## Behavioral References and User-Facing Messages
Inspect relevant settings, bindings, speech, Braille, and toolkit behavior before changing user-visible behavior. Read Python reference files without changing branches, for example:
+16
View File
@@ -206,6 +206,22 @@ Wine/application stderr is ignored. Capture does not log accessible control
contents or change speech delivery; output already present in debug speech
logs can still contain private text. Review debug logs before sharing them.
The standalone helper currently installs under the compatible name
`~/.local/libexec/cthulhu/wine/cthulhu-wine-access.exe`. Its complete installation
contains the launcher, `cthulhu-wine-access.exe.so`, `wine-access-menu.dll`,
`wine-access-menu32.dll`, and `wine-access-menu-host32.exe`. The 32-bit host
supplies native menu and editor feedback for applications such as MUSHclient.
Older Python Cthulhu builds can overwrite the helper launcher and sidecar in
that same directory while leaving the standalone hook files behind. The old
helper can still run and connect to Cthulhu without providing the standalone
menu and editor hooks. If Wine reading disappears after installing an older
Cthulhu build, rebuild and reinstall the separate `wine-access` project, then
reload Cthulhu from the same desktop session. If a Wine application already
loaded a previous hook DLL, save its work and restart it to load the replacement.
See [local installation and reload](CONTRIBUTING.md#local-installation-and-reload)
for the Go reader commands.
Wine typing feedback in standard Edit controls follows the current key echo
settings, including the master switch, character echo, and alphabetic, numeric,
punctuation, space and diacritical categories. Tabs and line breaks follow
+33 -24
View File
@@ -8,44 +8,53 @@ static gchar *cthulhu_capture_text(AtspiAccessible *source, gchar **beforeCaret,
static void cthulhu_capture_document_metrics(AtspiAccessible *source, cthulhu_snapshot *snapshot);
static gchar *cthulhu_table_cell_label(AtspiAccessible *source, guint depth, guint *remaining);
// Some native entries leave Name empty and expose their label only through
// LABELLED_BY. Follow explicit relationships, never nearby text or field values.
// Some toolkits expose an edit field's label only as a relation, leaving its
// accessible name empty. Follow direct targets only, with a shared query and
// output budget; never infer a label from neighbouring controls or field text.
static gchar *cthulhu_text_control_label(AtspiAccessible *source) {
GArray *relations = atspi_accessible_get_relation_set(source, NULL);
if (relations == NULL) return NULL;
GString *result = g_string_new("");
GString *label = g_string_new("");
guint remaining = 16;
for (guint i = 0; i < relations->len && remaining > 0; i++) {
for (guint i = 0; i < MIN(relations->len, 32) && remaining > 0 && label->len < 4096; i++) {
AtspiRelation *relation = g_array_index(relations, AtspiRelation *, i);
if (atspi_relation_get_relation_type(relation) != ATSPI_RELATION_LABELLED_BY) continue;
gint targets = atspi_relation_get_n_targets(relation);
for (gint j = 0; j < targets && remaining > 0; j++, remaining--) {
AtspiAccessible *label = atspi_relation_get_target(relation, j);
if (label == NULL) continue;
gchar *name = atspi_accessible_get_name(label, NULL);
if ((name == NULL || *name == '\0') &&
atspi_accessible_get_role(label, NULL) == ATSPI_ROLE_LABEL) {
AtspiText *text = atspi_accessible_get_text(label);
if (text != NULL) {
gint count = atspi_text_get_character_count(text, NULL);
g_free(name);
name = count > 0 ? atspi_text_get_text(text, 0, MIN(count, 1024), NULL) : NULL;
g_object_unref(text);
gint count = atspi_relation_get_n_targets(relation);
for (gint j = 0; j < count && remaining > 0 && label->len < 4096; j++) {
remaining--;
AtspiAccessible *target = atspi_relation_get_target(relation, j);
if (target == NULL) continue;
AtspiRole role = atspi_accessible_get_role(target, NULL);
gchar *text = NULL;
if (role != ATSPI_ROLE_PASSWORD_TEXT) {
text = atspi_accessible_get_name(target, NULL);
if ((text == NULL || *text == '\0') &&
(role == ATSPI_ROLE_LABEL || role == ATSPI_ROLE_STATIC)) {
g_free(text);
AtspiText *labelText = atspi_accessible_get_text(target);
if (labelText != NULL) {
gint characterCount = atspi_text_get_character_count(labelText, NULL);
text = characterCount > 0 ? atspi_text_get_text(labelText, 0, MIN(characterCount, 1024), NULL) : NULL;
g_object_unref(labelText);
} else {
text = NULL;
}
}
}
if (name != NULL && *g_strstrip(name) != '\0' &&
result->len + strlen(name) + 1 <= 4096) {
if (result->len > 0) g_string_append_c(result, ' ');
g_string_append(result, name);
if (text != NULL && *g_strstrip(text) != '\0') {
if (label->len != 0) g_string_append_c(label, ' ');
const gchar *end = NULL;
g_utf8_validate(text, MIN(strlen(text), 4096 - label->len), &end);
g_string_append_len(label, text, end - text);
}
g_free(name);
g_object_unref(label);
g_free(text);
g_object_unref(target);
}
}
for (guint i = 0; i < relations->len; i++)
g_object_unref(g_array_index(relations, AtspiRelation *, i));
g_array_unref(relations);
return g_string_free(result, FALSE);
return g_string_free(label, FALSE);
}
// Collect only the selected row. Walking an entire table on every arrow key
+91
View File
@@ -0,0 +1,91 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi_test
import (
"context"
"os"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/presentation"
)
// Missing LABELLED_BY capture must fail even when the toolkit leaves Name empty.
func TestNativeEditFieldLabels(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_EDIT_LABELS") != "1" {
t.Skip("requires isolated edit label fixture")
}
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()
adapter := atspi.New()
if err := adapter.Start(ctx); err != nil {
t.Fatal(err)
}
defer func() {
stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second)
defer stopCancel()
if err := adapter.Stop(stopCtx); err != nil {
t.Error(err)
}
}()
tree, err := adapter.CaptureDesktop(ctx)
if err != nil {
t.Fatal(err)
}
root, err := tree.Root()
if err != nil {
t.Fatal(err)
}
fields := map[string]atspi.Snapshot{}
var walk func(atspi.ObjectReference)
walk = func(ref atspi.ObjectReference) {
snapshot, err := tree.Resolve(ref)
if err != nil {
t.Fatal(err)
}
if snapshot.Description != "" {
fields[snapshot.Description] = snapshot
}
child, ok, err := tree.FirstChild(ref)
if err != nil {
t.Fatal(err)
}
for ok {
walk(child)
child, ok, err = tree.NextSibling(child)
if err != nil {
t.Fatal(err)
}
}
}
walk(root)
for _, test := range []struct{ id, name string }{
{"related", "Nickname"},
{"multiple", "Account identifier"},
{"named", "Explicit name"},
{"unlabelled", ""},
{"protected", "Password"},
} {
t.Run(test.id, func(t *testing.T) {
snapshot, ok := fields[test.id]
if !ok {
t.Fatal("missing edit field")
}
if snapshot.Name != test.name {
t.Errorf("field label = %q, want %q", snapshot.Name, test.name)
}
spoken := presentation.FocusText(snapshot)
if test.name != "" && !strings.Contains(spoken, test.name) {
t.Errorf("speech %q omits label %q", spoken, test.name)
}
if test.id == "protected" && (snapshot.Text != "" || strings.Contains(spoken, "secret value")) {
t.Fatal("password contents exposed")
}
})
}
}
+1
View File
@@ -63,3 +63,4 @@ mozilla-leaf-text-reading,Orca src/orca/scripts/web/script_utilities.py,web,Read
thunderbird-local-mail-composition,Orca src/orca/scripts/apps/Thunderbird/script.py;src/orca/script_utilities.py,application,Navigate local folders and message tabs; select messages; compose replies and forwards; save and add attachments,,internal/atspi internal/app tests/native_thunderbird.py,in-progress,go test -race ./internal/atspi ./internal/app ./internal/review ./internal/presentation -count=1 -timeout=120s; CTHULHU_BINARY=/tmp/cthulhu-mozilla-review /usr/bin/python3 tests/native_thunderbird.py,Private Thunderbird 158.0 HTML and plain-text local mailbox checks cover message-tab return; Subject and body editing; reply and reply-all recipients with Cc; inline forwarding; exact selected rows after Shift and Ctrl navigation; received attachment saving with content verification; and adding a local file to an unsent draft.,,SSIP verifies focus handoff and row roles; selection toggling; Subject and body echo; reply and forward editing; attachment menus and picker return; audible acceptance pending.,Event-loop regression checks paragraph cursor and routing metadata; physical display acceptance pending.,Foreground replacement requires a registered application and fresh complete window membership. Editable paragraph events belong only to their focused editable document. Title echoes require owned editable focus and recent editing plus unchanged title prefix/suffix; unrelated status titles still announce. See docs/testing/thunderbird-workflows.md; no sending remote mailbox or page-load-option acceptance claimed.
flatpak-foreground-identity,src/cthulhu/script_utilities.py:canBeActiveWindow,focus,Correlate Flatpak accessibility providers with the X11 foreground across PID namespaces and D-Bus proxies,,internal/platform/xorg internal/atspi,in-progress,go test -race ./internal/platform/xorg ./internal/atspi ./internal/app -count=1; CTHULHU_FOREGROUND_PID=1 python3 tests/native_atspi.py,Private XRes lookup and fallback; real proxy with matching and different synthetic instance metadata; GTK Qt and Brave ownership checks,,Actual Flatpak Steam speech pending,Physical Braille pending; original report also has helper geometry errors,docs/testing/flatpak-foreground.md; preserves active state geometry and sibling ownership checks; no application name exception
present-window-title,Orca src/orca/where_am_i_presenter.py;src/orca/where_am_i_presenter_command_definitions.py,command,Read the foreground window title with Cthulhu+Numpad Enter on desktop or Cthulhu+Slash on laptop,,internal/input internal/app internal/platform/xorg,in-progress,go test ./internal/input ./internal/app ./internal/platform/xorg -count=1; focused title command race checks; python3 tests/check_speech.py,Private Xvfb command dispatch verifies both layout bindings and both release orders. Private X11 and AT-SPI event loop repeats the current title with automatic changes disabled.,,Speech queue assertions passed; physical keyboard and audible acceptance pending.,Braille flash queue assertion passed; physical display acceptance pending.,Reuses copied foreground title metadata and the owned accessible title. Explicit requests bypass automatic-title policy and duplicate suppression without moving focus. Stale native epochs and unrelated native windows cannot supply a title. Lock and terminal handoff remain enforced. Double-press status-bar reading is outside this change. See tests/window_titles.md.
edit-field-relation-labels,src/cthulhu/generator.py:_generateLabel;src/cthulhu/script_utilities.py:displayedLabel,speech,Announce relation-only labels for unnamed edit fields,,internal/atspi internal/presentation,implemented,CTHULHU_NATIVE_EDIT_LABELS=1 python3 tests/native_atspi.py; go test ./... -count=1; go vet ./...,Private GTK/Xvfb verifies single and multiple labels plus existing names and password exclusion. Read-only live irctest capture verifies all three settings labels. Maintainer confirmed installed-reader label speech on physical Xorg on 2026-10-20.,,Formatted speech includes each label. Maintainer confirmed the installed reader speaks irctest settings field labels on 2026-10-20.,Physical Braille check pending.,Direct LABELLED_BY targets supply an empty text or entry or password-field name. Queries and output are bounded; explicit names are preserved. Human diff review remains required before submission.
1 id python_source subsystem behavior owner go_destination status automated_evidence x11_evidence wayland_evidence speech_evidence braille_evidence notes
63 thunderbird-local-mail-composition Orca src/orca/scripts/apps/Thunderbird/script.py;src/orca/script_utilities.py application Navigate local folders and message tabs; select messages; compose replies and forwards; save and add attachments internal/atspi internal/app tests/native_thunderbird.py in-progress go test -race ./internal/atspi ./internal/app ./internal/review ./internal/presentation -count=1 -timeout=120s; CTHULHU_BINARY=/tmp/cthulhu-mozilla-review /usr/bin/python3 tests/native_thunderbird.py Private Thunderbird 158.0 HTML and plain-text local mailbox checks cover message-tab return; Subject and body editing; reply and reply-all recipients with Cc; inline forwarding; exact selected rows after Shift and Ctrl navigation; received attachment saving with content verification; and adding a local file to an unsent draft. SSIP verifies focus handoff and row roles; selection toggling; Subject and body echo; reply and forward editing; attachment menus and picker return; audible acceptance pending. Event-loop regression checks paragraph cursor and routing metadata; physical display acceptance pending. Foreground replacement requires a registered application and fresh complete window membership. Editable paragraph events belong only to their focused editable document. Title echoes require owned editable focus and recent editing plus unchanged title prefix/suffix; unrelated status titles still announce. See docs/testing/thunderbird-workflows.md; no sending remote mailbox or page-load-option acceptance claimed.
64 flatpak-foreground-identity src/cthulhu/script_utilities.py:canBeActiveWindow focus Correlate Flatpak accessibility providers with the X11 foreground across PID namespaces and D-Bus proxies internal/platform/xorg internal/atspi in-progress go test -race ./internal/platform/xorg ./internal/atspi ./internal/app -count=1; CTHULHU_FOREGROUND_PID=1 python3 tests/native_atspi.py Private XRes lookup and fallback; real proxy with matching and different synthetic instance metadata; GTK Qt and Brave ownership checks Actual Flatpak Steam speech pending Physical Braille pending; original report also has helper geometry errors docs/testing/flatpak-foreground.md; preserves active state geometry and sibling ownership checks; no application name exception
65 present-window-title Orca src/orca/where_am_i_presenter.py;src/orca/where_am_i_presenter_command_definitions.py command Read the foreground window title with Cthulhu+Numpad Enter on desktop or Cthulhu+Slash on laptop internal/input internal/app internal/platform/xorg in-progress go test ./internal/input ./internal/app ./internal/platform/xorg -count=1; focused title command race checks; python3 tests/check_speech.py Private Xvfb command dispatch verifies both layout bindings and both release orders. Private X11 and AT-SPI event loop repeats the current title with automatic changes disabled. Speech queue assertions passed; physical keyboard and audible acceptance pending. Braille flash queue assertion passed; physical display acceptance pending. Reuses copied foreground title metadata and the owned accessible title. Explicit requests bypass automatic-title policy and duplicate suppression without moving focus. Stale native epochs and unrelated native windows cannot supply a title. Lock and terminal handoff remain enforced. Double-press status-bar reading is outside this change. See tests/window_titles.md.
66 edit-field-relation-labels src/cthulhu/generator.py:_generateLabel;src/cthulhu/script_utilities.py:displayedLabel speech Announce relation-only labels for unnamed edit fields internal/atspi internal/presentation implemented CTHULHU_NATIVE_EDIT_LABELS=1 python3 tests/native_atspi.py; go test ./... -count=1; go vet ./... Private GTK/Xvfb verifies single and multiple labels plus existing names and password exclusion. Read-only live irctest capture verifies all three settings labels. Maintainer confirmed installed-reader label speech on physical Xorg on 2026-10-20. Formatted speech includes each label. Maintainer confirmed the installed reader speaks irctest settings field labels on 2026-10-20. Physical Braille check pending. Direct LABELLED_BY targets supply an empty text or entry or password-field name. Queries and output are bounded; explicit names are preserved. Human diff review remains required before submission.
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Edit fields whose labels are exposed only through AT-SPI relations."""
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Atk", "1.0")
from gi.repository import Atk, Gtk
window = Gtk.Window(title="Edit label probe")
window.connect("destroy", Gtk.main_quit)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
window.add(box)
def field(identifier, labels, name="", password=False):
entry = Gtk.Entry()
entry.set_text("secret value" if password else "sample value")
entry.set_visibility(not password)
accessible = entry.get_accessible()
accessible.set_name(name)
accessible.set_description(identifier)
for text in labels:
label = Gtk.Label(label=text)
box.pack_start(label, False, False, 0)
accessible.add_relationship(Atk.RelationType.LABELLED_BY, label.get_accessible())
box.pack_start(entry, False, False, 0)
field("related", ["Nickname"])
field("multiple", ["Account", "identifier"])
field("named", ["Other label"], name="Explicit name")
field("unlabelled", [])
field("protected", ["Password"], password=True)
window.show_all()
Gtk.main()
+24
View File
@@ -23,6 +23,9 @@ def wait_for(check, message, timeout=15):
def run_inside():
if os.environ.get("CTHULHU_NATIVE_EDIT_LABELS") == "1":
run_edit_label_probe()
return
if os.environ.get("CTHULHU_FOREGROUND_PID") == "1":
if os.environ.get("CTHULHU_FOREGROUND_PROXY") == "1":
run_proxy_probe()
@@ -52,6 +55,27 @@ def run_inside():
run_thunar_probe()
def run_edit_label_probe():
fixture = subprocess.Popen([sys.executable, str(Path(__file__).with_name("gtk_edit_labels.py"))])
try:
import pyatspi
def find_window():
return any(window.name == "Edit label probe"
for app in pyatspi.Registry.getDesktop(0) for window in app)
wait_for(find_window, "edit label probe missing")
environment = dict(os.environ, GOCACHE="/tmp/cthulhu-go-cache", GOPROXY="off")
subprocess.run(
["go", "test", "./internal/atspi", "-run", "^TestNativeEditFieldLabels$",
"-count=1", "-timeout=25s", "-v"],
cwd=Path(__file__).resolve().parents[1], env=environment, check=True,
)
finally:
fixture.terminate()
fixture.wait(timeout=3)
def run_thunar_probe():
with tempfile.TemporaryDirectory(prefix="cthulhu-thunar-") as temporary:
home = Path(temporary)