167 lines
5.0 KiB
Go
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
|
|
}
|