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

+9 -1
View File
@@ -22,6 +22,7 @@ import (
"strings"
"unicode/utf8"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/remote"
"git.stormux.org/storm/cthulhu/internal/wine"
"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.
kinds := strings.Fields(event.States)
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
custom := false
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{
Text: event.Name, Role: "system", Cause: "wine-accessible-event", Append: true,
+22
View File
@@ -7,7 +7,11 @@ import (
"strings"
"sync"
"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"
)
@@ -40,6 +44,24 @@ func newWineEventPresenter(present func(string) bool) *wineEventPresenter {
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 {
return owner.present_event(event, nil)
}
+19 -2
View File
@@ -11,6 +11,7 @@ import (
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/remote"
"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")
}
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 {
return handler.presentMessage(message, "wine-accessible-event")
})
@@ -71,6 +75,19 @@ func TestNativeSessionBusWineEvents(t *testing.T) {
for _, text := range []string{"A", "a", "É", "é"} {
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)
owner.flush()
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)
owner.flush()
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 {
case request := <-output.speech:
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)
}
})
}
}