2 Commits
Author SHA1 Message Date
deedra 538ff88028 Document Go installation and standalone Wine helper recovery
Record the user-local Go installation used by the working reader: install
cthulhu and cthulhu-braille together, validate the configuration, and use
--replace from the original desktop session. Explain that another session
bus can start a second reader instead of replacing the intended instance.

Document the complete five-file standalone wine-access installation and
the legacy helper path supported by the Go supervisor. Older Python
Cthulhu builds can overwrite the launcher and sidecar at that path while
leaving the native hook files behind. Such a helper can connect to D-Bus
without supplying the current native menu and editor support.

Describe recovery by reinstalling the standalone helper and reloading the
reader, including restarting Wine applications when older hook DLLs are
already loaded. Keep the source/build distinction explicit: rebuilding
the Go reader alone does not refresh wine-access.

Validation for these documentation changes:
- Reviewed the complete diff and ran git diff --check.
- Checked the documented shell commands with bash -n.
- Installed both documented Go build outputs into a temporary directory,
  compared their bytes with the builds, and passed --check-config.
- Verified the installed reader, Braille helper, and all five Wine helper
  files match the builds used for the restored installation.

Runtime validation during the preceding repair:
- Cthulhu internal/wine and internal/remote tests passed; Wine-specific
  internal/app tests passed.
- The legacy installed helper failed the isolated 32-bit editor check;
  the standalone helper passed it.
- Standalone controller, 32-bit late-attachment/restart, and hook-safety
  checks passed on private displays and session buses.
- The user confirmed MUSHclient reading was restored. Physical Braille
  was not checked.

These are documentation changes; the installed behavior already comes
from the existing source revisions, with no new runtime source patch.
2026-10-20 02:44:48 -07:00
deedra 15d527c619 Respect Cthulhu key echo settings for Wine typing
Wine editor insertion events previously reached speech and Braille without
checking the user's typing echo preferences. Turning off key echo or an
individual character category therefore left Wine typing feedback enabled.

Read the live settings for each insertion and reuse the native text
presentation policy for alphabetic, numeric, punctuation, symbol, space and
diacritical input. Honor character echo and the master key echo switch;
apply the action-key setting to the helper's tab and new-line labels.
Reject unexpected multi-character insertion labels rather than treating
these ordered character events as paste notifications.

Preserve navigation, selection and deletion feedback when typing echo is
disabled. Retain repeated characters, case, uppercase/custom voice routing,
focus ordering and the existing stale-source/password protections.

Add regression coverage for live disable/re-enable, independent categories,
Unicode, character-echo overrides, and unaffected editor actions. Extend
the private D-Bus test to exercise settings changes through the real Wine
service signature. Document behavior, current word/sentence echo limits,
and live Notepad acceptance checks in the README.

Validation:
- Reproduced disabled typing reaching output with the new test before fixing.
- Full go test -race ./... -count=1 passed outside the socket-restricted sandbox.
- Private-session TestNativeSessionBusWineEvents passed with -race.
- Focused typing/editor regressions passed, including a fresh pre-commit run.
- go vet passed for internal/app, presentation, remote and wine.
- Reader and BrlAPI helper builds passed; reader --version succeeded.
- gofmt and git diff --check passed; independent review found no blockers.

Live audible speech, physical keyboard and physical Braille acceptance
remain pending. No running reader was replaced. Changes target deedra.
2026-10-14 22:32:37 -07:00
6 changed files with 228 additions and 3 deletions

No files matched your search

+29
View File
@@ -33,6 +33,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. Treat cgroup-v2 containment as resource control, not a same-user security sandbox. Preserve HTTPS artifact and SHA-256 verification before atomic package installation. 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. 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 ## 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: Inspect relevant settings, bindings, speech, Braille, and toolkit behavior before changing user-visible behavior. Read Python reference files without changing branches, for example:
+32
View File
@@ -20,3 +20,35 @@ location follows the new user location, the old `/usr` location follows the new
system locations, and the old name on `PATH` follows the new name. system locations, and the old name on `PATH` follows the new name.
Restart Cthulhu after changing the selected helper. Keep the launcher, its Restart Cthulhu after changing the selected helper. Keep the launcher, its
`.exe.so` sidecar, and the native hooks together in the installed directory. `.exe.so` sidecar, and the native hooks together in the installed directory.
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
key echo's action-key setting. Applying preferences takes effect without
restarting the helper. Navigation, selection and deletion feedback remain
available when typing echo is disabled. Word and sentence typing echo are not
currently supplied by the Wine helper.
For a live check, type repeated letters, numbers, punctuation and spaces in
Wine Notepad, change the corresponding echo settings and apply them, then
continue typing. Disable key echo and verify that typing stays quiet while
arrows, selection and Backspace still report editing feedback. Re-enable it
and check capital/lowercase voices and password exclusion. Automated tests
cover queued speech and Braille; audible speech and a physical Braille display
still require live verification.
+9 -1
View File
@@ -22,6 +22,7 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/remote" "git.stormux.org/storm/cthulhu/internal/remote"
"git.stormux.org/storm/cthulhu/internal/wine" "git.stormux.org/storm/cthulhu/internal/wine"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
@@ -95,10 +96,17 @@ func (presenter remoteSpeech) presentWineEditor(event remote.AccessibleEvent) bo
// character identity for navigation and Backspace as well as typing. // character identity for navigation and Backspace as well as typing.
kinds := strings.Fields(event.States) kinds := strings.Fields(event.States)
inserted := slices.Contains(kinds, "kind=8") inserted := slices.Contains(kinds, "kind=8")
settings := config.DefaultSettings()
if presenter.live != nil {
settings = presenter.live.Load()
}
if inserted && !wineTypingEchoEnabled(event.Name, settings) {
return false
}
character := inserted || utf8.RuneCountInString(event.Name) == 1 character := inserted || utf8.RuneCountInString(event.Name) == 1
custom := false custom := false
if character && presenter.live != nil && (inserted || slices.Contains(kinds, "kind=7")) { if character && presenter.live != nil && (inserted || slices.Contains(kinds, "kind=7")) {
custom = customEcho(presenter.live.Load(), "character") custom = customEcho(settings, "character")
} }
return presenter.presentRequest(speechRequest{ return presenter.presentRequest(speechRequest{
Text: event.Name, Role: "system", Cause: "wine-accessible-event", Append: true, Text: event.Name, Role: "system", Cause: "wine-accessible-event", Append: true,
+22
View File
@@ -7,7 +7,11 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode/utf8"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/presentation"
"git.stormux.org/storm/cthulhu/internal/remote" "git.stormux.org/storm/cthulhu/internal/remote"
) )
@@ -40,6 +44,24 @@ func newWineEventPresenter(present func(string) bool) *wineEventPresenter {
return &wineEventPresenter{present: present} return &wineEventPresenter{present: present}
} }
// wineTypingEchoEnabled applies the native insertion policy to the helper's
// character labels. Navigation and deletion do not pass through this filter.
func wineTypingEchoEnabled(text string, settings config.Settings) bool {
switch text {
case "tab", "new line":
return settings.EnableKeyEcho && settings.EnableActionKeys
case "space":
text = " "
}
// The helper sends one accepted character per insertion. Do not treat an
// unexpected label as a multi-character paste, which has a different policy.
if utf8.RuneCountInString(text) != 1 {
return false
}
event := atspi.Event{Type: atspi.TextInsertedEvent, Payload: text}
return presentation.EventTextWithOptions(event, presentationSettingsFromConfig(settings, false)) != ""
}
func (owner *wineEventPresenter) Present(event remote.AccessibleEvent) bool { func (owner *wineEventPresenter) Present(event remote.AccessibleEvent) bool {
return owner.present_event(event, nil) return owner.present_event(event, nil)
} }
+19 -2
View File
@@ -11,6 +11,7 @@ import (
"testing" "testing"
"time" "time"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/remote" "git.stormux.org/storm/cthulhu/internal/remote"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -32,7 +33,10 @@ func TestNativeSessionBusWineEvents(t *testing.T) {
t.Skip("set CTHULHU_NATIVE_DBUS_TEST=1 inside a private session bus") t.Skip("set CTHULHU_NATIVE_DBUS_TEST=1 inside a private session bus")
} }
output := &wineTestOutput{speech: make(chan speechRequest, 32), braille: make(chan string, 32)} output := &wineTestOutput{speech: make(chan speechRequest, 32), braille: make(chan string, 32)}
handler := remoteSpeech{speech: output, braille: output} settings := config.DefaultSettings()
live := &liveSettings{}
live.Store(settings)
handler := remoteSpeech{speech: output, braille: output, live: live}
owner := newWineEventPresenter(func(message string) bool { owner := newWineEventPresenter(func(message string) bool {
return handler.presentMessage(message, "wine-accessible-event") return handler.presentMessage(message, "wine-accessible-event")
}) })
@@ -71,6 +75,19 @@ func TestNativeSessionBusWineEvents(t *testing.T) {
for _, text := range []string{"A", "a", "É", "é"} { for _, text := range []string{"A", "a", "É", "é"} {
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: text, States: "start=1 end=1 kind=8"}, true) send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: text, States: "start=1 end=1 kind=8"}, true)
} }
settings.EnableKeyEcho = false
live.Store(settings)
for _, text := range []string{"A", "space", "tab", "new line"} {
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: text, States: "kind=8"}, false)
}
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "e", States: "kind=7"}, true)
settings.EnableKeyEcho = true
settings.EnableAlphabeticKeys = false
live.Store(settings)
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "A", States: "kind=8"}, false)
settings.EnableAlphabeticKeys = true
live.Store(settings)
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "A", States: "kind=8"}, true)
send(remote.AccessibleEvent{SourceID: "check", EventType: "focus", Name: "Option", Role: "check box", States: "not checked"}, true) send(remote.AccessibleEvent{SourceID: "check", EventType: "focus", Name: "Option", Role: "check box", States: "not checked"}, true)
owner.flush() owner.flush()
send(remote.AccessibleEvent{SourceID: "check", EventType: "name", Name: "Renamed", Role: "check box", States: "not checked"}, true) send(remote.AccessibleEvent{SourceID: "check", EventType: "name", Name: "Renamed", Role: "check box", States: "not checked"}, true)
@@ -87,7 +104,7 @@ func TestNativeSessionBusWineEvents(t *testing.T) {
send(remote.AccessibleEvent{SourceID: "no", EventType: "focus", Name: "No", Role: "client"}, true) send(remote.AccessibleEvent{SourceID: "no", EventType: "focus", Name: "No", Role: "client"}, true)
owner.flush() owner.flush()
owner.Close() owner.Close()
for _, want := range []string{"edit, hello", "l", "l", "e selected", "e deleted", "A", "a", "É", "é", "Option, check box, not checked", "Renamed, check box, not checked", "Renamed, check box, checked, unavailable", "Yes", "No"} { for _, want := range []string{"edit, hello", "l", "l", "e selected", "e deleted", "A", "a", "É", "é", "e", "A", "Option, check box, not checked", "Renamed, check box, not checked", "Renamed, check box, checked, unavailable", "Yes", "No"} {
select { select {
case request := <-output.speech: case request := <-output.speech:
if request.Text != want || !request.Append { if request.Text != want || !request.Append {
@@ -0,0 +1,117 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"testing"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/remote"
)
func TestWineTypingEchoUsesLiveSettings(t *testing.T) {
settings := config.DefaultSettings()
live := &liveSettings{}
live.Store(settings)
speech := &remoteSpeechQueue{}
braille := &remoteBrailleQueue{running: true}
presenter := remoteSpeech{speech: speech, braille: braille, live: live}
owner := newWineEventPresenter(func(message string) bool {
return presenter.presentMessage(message, "wine-accessible-event")
})
defer owner.Close()
presenter.wine = owner
presenter.PresentAccessibleEvent(remote.AccessibleEvent{SourceID: "edit", EventType: "focus", Role: "edit"})
owner.flush()
check := func(text, kind string, want bool) {
t.Helper()
beforeSpeech, beforeBraille := len(speech.requests), len(braille.text)
accepted := presenter.PresentAccessibleEvent(remote.AccessibleEvent{
SourceID: "edit", EventType: "text", Name: text, States: "start=1 end=1 kind=" + kind,
})
if accepted != want {
t.Errorf("%q kind=%s accepted=%v, want %v", text, kind, accepted, want)
}
if !want {
if len(speech.requests) != beforeSpeech || len(braille.text) != beforeBraille {
t.Errorf("disabled typing %q reached output", text)
}
return
}
if len(speech.requests) != beforeSpeech+1 || speech.requests[beforeSpeech].Text != text ||
len(braille.text) != beforeBraille+1 || braille.text[beforeBraille] != text {
t.Errorf("enabled action %q missing from speech or braille", text)
}
}
for _, text := range []string{"A", "a", "a", "É", "𐐀", "7", "!", "😀", "space", "tab", "new line"} {
check(text, "8", true)
}
settings.EnableKeyEcho = false
settings.EnableEchoByCharacter = true
live.Store(settings)
for _, text := range []string{"a", "7", "!", "space", "tab", "new line"} {
check(text, "8", false)
}
// Reading, deletion and selection remain available with key echo disabled.
for _, action := range []struct{ text, kind string }{
{"a", "5"}, {"a", "7"}, {"line of text", "1"}, {"a selected", "2"}, {"a deleted", "4"},
} {
check(action.text, action.kind, true)
}
settings.EnableKeyEcho = true
settings.EnableEchoByCharacter = false
settings.EnableAlphabeticKeys = false
settings.EnableNumericKeys = false
settings.EnablePunctuationKeys = false
settings.EnableSpace = false
settings.EnableActionKeys = false
live.Store(settings)
for _, text := range []string{"A", "é", "𐐀", "7", "!", "😀", "space", "tab", "new line", "́"} {
check(text, "8", false)
}
settings.EnableEchoByCharacter = true
live.Store(settings)
for _, text := range []string{"A", "é", "7", "!", "😀", "space", "́"} {
check(text, "8", true)
}
for _, text := range []string{"tab", "new line"} {
check(text, "8", false)
}
settings.EnableEchoByCharacter = false
settings.EnableDiacriticalKeys = true
settings.EnableActionKeys = true
live.Store(settings)
for _, text := range []string{"́", "tab", "new line"} {
check(text, "8", true)
}
settings = config.DefaultSettings()
live.Store(settings)
check("a", "8", true)
}
func TestWineTypingEchoFiltersCategoriesIndependently(t *testing.T) {
for _, tc := range []struct {
name, blocked, allowed string
disable func(*config.Settings)
}{
{"alphabetic", "É", "7", func(s *config.Settings) { s.EnableAlphabeticKeys = false }},
{"numeric", "7", "a", func(s *config.Settings) { s.EnableNumericKeys = false }},
{"punctuation", "!", "a", func(s *config.Settings) { s.EnablePunctuationKeys = false }},
{"symbol", "😀", "a", func(s *config.Settings) { s.EnablePunctuationKeys = false }},
{"space", "space", "a", func(s *config.Settings) { s.EnableSpace = false }},
{"action", "tab", "space", func(s *config.Settings) { s.EnableActionKeys = false }},
} {
t.Run(tc.name, func(t *testing.T) {
settings := config.DefaultSettings()
tc.disable(&settings)
if wineTypingEchoEnabled(tc.blocked, settings) {
t.Errorf("disabled category echoed %q", tc.blocked)
}
if !wineTypingEchoEnabled(tc.allowed, settings) {
t.Errorf("unrelated category suppressed %q", tc.allowed)
}
})
}
}