Files
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

162 lines
4.6 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package app
import (
"slices"
"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"
)
type remoteSpeech struct {
speech speechEnqueuer
braille interface {
EnqueueMessage(string) bool
}
wine *wineEventPresenter
wineReview *wine.ReviewRegistry
showPreferences func()
quit func()
runtimeSnapshot func() string
live *liveSettings
preferences *preferencesWorker
}
func (presenter remoteSpeech) RegisterProvider(sender string, path dbus.ObjectPath, version uint32) bool {
return presenter.wineReview != nil && presenter.wineReview.RegisterProvider(sender, path, version)
}
func (presenter remoteSpeech) PresentFocus(sender string, notice wine.ReviewFocusNotice) bool {
return presenter.wineReview != nil && presenter.wineReview.PresentFocus(sender, notice)
}
func (presenter remoteSpeech) BindWineReviewConnection(connection *dbus.Conn) {
if presenter.wineReview != nil {
presenter.wineReview.BindWineReviewConnection(connection)
}
}
func (presenter remoteSpeech) ShowPreferences() bool {
if presenter.showPreferences == nil {
return false
}
presenter.showPreferences()
return true
}
func (presenter remoteSpeech) Quit() bool {
if presenter.quit == nil {
return false
}
presenter.quit()
return true
}
func (presenter remoteSpeech) RuntimeStateSnapshot() string {
if presenter.runtimeSnapshot == nil {
return ""
}
return presenter.runtimeSnapshot()
}
func (presenter remoteSpeech) PresentAccessibleEvent(event remote.AccessibleEvent) bool {
return presenter.wine != nil && presenter.wine.present_event(event, presenter.presentWineEditor)
}
func (presenter remoteSpeech) PresentMessage(message string) bool {
return presenter.presentMessage(message, "remote-present-message")
}
func (presenter remoteSpeech) presentMessage(message, cause string) bool {
return presenter.presentRequest(speechRequest{
Text: message, Role: "system", Cause: cause, Append: true,
})
}
func (presenter remoteSpeech) presentWineEditor(event remote.AccessibleEvent) bool {
// These action kinds are part of the helper editor protocol. Keep
// 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(settings, "character")
}
return presenter.presentRequest(speechRequest{
Text: event.Name, Role: "system", Cause: "wine-accessible-event", Append: true,
Character: character, CustomEcho: custom,
})
}
func (presenter remoteSpeech) presentRequest(request speechRequest) bool {
if request.Text == "" {
return false
}
accepted := false
if presenter.speech != nil {
presenter.speech.Enqueue(request)
accepted = true
}
if presenter.braille != nil && presenter.braille.EnqueueMessage(request.Text) {
accepted = true
}
return accepted
}
func (presenter remoteSpeech) SpeakText(text string, interrupt bool) bool {
if presenter.speech == nil || text == "" {
return false
}
if interrupt {
presenter.speech.Interrupt()
}
presenter.speech.Enqueue(speechRequest{
Text: text,
Role: "system",
Cause: "remote-speak-text",
Append: true,
})
return true
}
func (presenter remoteSpeech) BrailleMessage(text string) bool {
if presenter.braille == nil || text == "" {
return false
}
return presenter.braille.EnqueueMessage(text)
}
func (presenter remoteSpeech) CancelSpeech() bool {
if presenter.speech == nil {
return false
}
presenter.speech.Interrupt()
return true
}