Files

167 lines
5.0 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 && (settings.KeyEchoInEditFieldsOnly && !wineEchoEditField(event) || !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")) {
kind := "character"
if inserted && !settings.EnableEchoByCharacter {
kind = "key"
}
custom = customEcho(settings, kind)
}
return presenter.presentRequest(speechRequest{
Text: event.Name, Role: "system", Cause: "wine-accessible-event", Append: true,
Character: character, CustomEcho: custom,
PhoneticHint: slices.Contains(kinds, "kind=5"),
})
}
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
}
// The protocol's interrupt flag does not establish user input. External
// announcements obey the same queue policy as accessibility events; callers
// requesting an explicit stop can use CancelSpeech.
func (presenter remoteSpeech) SpeakText(text string, _ bool) bool {
if presenter.speech == nil || text == "" {
return false
}
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
}