// 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 . 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 }