Compare commits
4
Commits
f8efae9a92
...
88e8627867
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88e8627867 | ||
|
|
dcdeab256e | ||
|
|
8ef838e16e | ||
|
|
538ff88028 |
No files matched your search
@@ -4,6 +4,14 @@
|
||||
|
||||
Cthulhu must never depend on a specific Linux init system or service manager. This applies to the reader, Tentacles, process supervision, installation, packaging, and required tests. Do not require systemd, `systemd-run`, `systemctl`, or a systemd user manager. Use init-independent Linux interfaces and detect the capabilities actually available. Cthulhu and its Tentacles must work on non-systemd distributions, including Artix.
|
||||
|
||||
## Before Starting a Fix or Feature
|
||||
|
||||
Inspect `git status --short --branch`, confirm the intended branch and upstream, and complete `git pull --ff-only` before starting implementation. If local and upstream commits have diverged, resolve that divergence with a normal merge before proceeding; never discard local work to make the pull succeed. If a merge or rebase is already in progress, finish it first. Preserve uncommitted changes and diagnostic files; do not automatically stash, reset, or clean them.
|
||||
|
||||
On every branch other than `testing` or `master`, also fetch the maintained repository's latest `testing` branch and merge it into the working branch. With the maintained repository configured as `origin`, use `git fetch origin` followed by `git merge origin/testing`. Resolve conflicts, run the relevant checks, and finish any merge commit before starting the new work. An unresolved or failed synchronization blocks implementation. Do not merge `testing` into `master` as part of this preparation; release integration is separate.
|
||||
|
||||
After synchronization, check whether the reported issue still exists and inspect recent changes before implementing a fix. Another contributor may already have fixed it. See [the contributor synchronization procedure](CONTRIBUTING.md#update-your-branch-before-starting-work), including instructions for a new branch without an upstream.
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
Cthulhu is a Go Linux screen reader. `cmd/` contains executables; `internal/` owns reader subsystems. Embedded audio lives in `assets/sounds/`, Tentacle libraries in `sdk/`, native harnesses in `tests/`, and parity records in `testdata/`. Go tests sit beside implementation files. Arch packaging lives in `distro-packages/Arch-Linux/cthulhu-git/`.
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
|
||||
Read [README.md](README.md) and [AGENTS.md](AGENTS.md) first. Start with a user-visible behavior and reproduce the issue in the current code. The maintained repository is `git.stormux.org/storm/cthulhu`; confirm the intended target branch with the maintainer. The `legacy` branch preserves Python Cthulhu; the current checkout is the Go implementation.
|
||||
|
||||
## Update Your Branch Before Starting Work
|
||||
|
||||
Update your checkout before starting each fix or feature. This helps avoid fixing an issue that another contributor has already resolved.
|
||||
|
||||
1. Run `git status --short --branch` and confirm you are on the intended branch. If a merge or rebase is in progress, finish it first. Preserve any uncommitted work and diagnostic files before updating; do not discard them or automatically stash them.
|
||||
2. Run `git pull --ff-only` to update the current branch from its configured upstream. A successful pull saying the branch is already up to date also satisfies this step. If Git reports divergent branches, merge the intended upstream into your branch and resolve the conflicts before continuing. Ask for help if you are unsure; do not use a reset or force push to make the error disappear.
|
||||
3. If your branch is neither `testing` nor `master`, also run:
|
||||
|
||||
```sh
|
||||
git fetch origin
|
||||
git merge origin/testing
|
||||
```
|
||||
|
||||
These commands bring the latest shared development changes into your contributor branch. They assume `origin` points to the maintained repository. If `origin` is your fork, use the remote for the maintained repository in both commands instead. If you are unsure which remote that is, ask before merging.
|
||||
4. Resolve any conflicts, run the relevant checks, and complete any merge commit before starting implementation. A failed pull or an unfinished merge means the update is not complete.
|
||||
5. Reproduce the reported issue again and review recent changes. If it is already fixed, verify the existing fix rather than writing a second implementation.
|
||||
|
||||
For a new local branch without an upstream, first update `testing` with `git pull --ff-only`, then create your contributor branch from that updated `testing`. Do this before making changes. Existing contributor branches still need the merge from the maintained repository's latest `testing` described above.
|
||||
|
||||
Do not merge `testing` into `master` during this preparation. Updating `master` from its own upstream and integrating a release are separate operations. The historical Python `legacy` branch is not a target for Go changes.
|
||||
|
||||
## Proposing a Change
|
||||
|
||||
Keep each change to one complete behavior. Describe the reproduction, expected result, affected subsystem, and plausible regressions. A reported application or page is a reproduction case: fix the shared layer when it explains the defect. Use an application-specific script only when a shared fix would be wrong; document and test the exception.
|
||||
@@ -57,6 +78,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);
|
||||
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
|
||||
|
||||
@@ -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