Merge branch 'deedra' into testing

This commit is contained in:
Storm Dragon committed 2026-10-15 01:49:44 -04:00
commit c0e4f7d292
5 files changed
+183 -3

No files matched your search

+16
View File
@@ -22,3 +22,19 @@ 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.
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)
}
})
}
}