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:
commit
dcdeab256e
7 files changed
+229
-24
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
g_free(name);
|
||||
g_object_unref(label);
|
||||
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(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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -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.
|
||||
|
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user