Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,273 @@
|
||||
// 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 remote publishes Cthulhu's bounded self-voice D-Bus API.
|
||||
package remote
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/version"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type endpoint struct {
|
||||
logger *slog.Logger
|
||||
handler Handler
|
||||
authorize func(dbus.Sender) bool
|
||||
|
||||
mutex sync.Mutex
|
||||
accepting bool
|
||||
active int
|
||||
idle chan struct{}
|
||||
}
|
||||
|
||||
func newEndpoint(logger *slog.Logger, handler Handler, authorize func(dbus.Sender) bool) *endpoint {
|
||||
if logger == nil {
|
||||
logger = slog.New(slog.DiscardHandler)
|
||||
}
|
||||
return &endpoint{logger: logger, handler: handler, authorize: authorize}
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) startAccepting() {
|
||||
endpoint.mutex.Lock()
|
||||
endpoint.accepting = true
|
||||
endpoint.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) begin(sender dbus.Sender, method string) *dbus.Error {
|
||||
endpoint.mutex.Lock()
|
||||
if !endpoint.accepting {
|
||||
endpoint.mutex.Unlock()
|
||||
endpoint.logger.Debug("self-voice D-Bus call unavailable", "method", method, "sender", sender)
|
||||
return &dbus.Error{
|
||||
Name: "org.stormux.Cthulhu1.Error.Unavailable",
|
||||
Body: []any{"self-voice service is not accepting requests"},
|
||||
}
|
||||
}
|
||||
endpoint.active++
|
||||
endpoint.mutex.Unlock()
|
||||
if endpoint.authorize == nil || !endpoint.authorize(sender) {
|
||||
endpoint.end()
|
||||
endpoint.logger.Warn("self-voice D-Bus call denied", "method", method, "sender", sender)
|
||||
return &dbus.Error{
|
||||
Name: "org.freedesktop.DBus.Error.AccessDenied",
|
||||
Body: []any{"self-voice requests are restricted to the runtime user"},
|
||||
}
|
||||
}
|
||||
endpoint.logger.Debug("self-voice D-Bus call accepted", "method", method, "sender", sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) end() {
|
||||
endpoint.mutex.Lock()
|
||||
endpoint.active--
|
||||
if endpoint.active == 0 && endpoint.idle != nil {
|
||||
close(endpoint.idle)
|
||||
endpoint.idle = nil
|
||||
}
|
||||
endpoint.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) stopAccepting() <-chan struct{} {
|
||||
endpoint.mutex.Lock()
|
||||
defer endpoint.mutex.Unlock()
|
||||
endpoint.accepting = false
|
||||
if endpoint.active == 0 {
|
||||
idle := make(chan struct{})
|
||||
close(idle)
|
||||
return idle
|
||||
}
|
||||
if endpoint.idle == nil {
|
||||
endpoint.idle = make(chan struct{})
|
||||
}
|
||||
return endpoint.idle
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) logRequest(method string) {
|
||||
endpoint.logger.Debug("self-voice D-Bus request", "method", method)
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) logResponse(method string, result any) {
|
||||
endpoint.logger.Debug("self-voice D-Bus response", "method", method, "result", result)
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) PresentMessage(sender dbus.Sender, message string) (bool, *dbus.Error) {
|
||||
endpoint.logRequest("PresentMessage")
|
||||
if err := endpoint.begin(sender, "PresentMessage"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
if len(message) > maxTextBytes {
|
||||
endpoint.logResponse("PresentMessage", false)
|
||||
return false, nil
|
||||
}
|
||||
accepted := endpoint.handler.PresentMessage(message)
|
||||
endpoint.logResponse("PresentMessage", accepted)
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) ShowPreferences(sender dbus.Sender) (bool, *dbus.Error) {
|
||||
if err := endpoint.begin(sender, "ShowPreferences"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
if handler, ok := endpoint.handler.(ControlHandler); ok {
|
||||
return handler.ShowPreferences(), nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) GetVersion(sender dbus.Sender) (string, *dbus.Error) {
|
||||
if err := endpoint.begin(sender, "GetVersion"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer endpoint.end()
|
||||
return version.String(), nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) GetRuntimeStateSnapshot(sender dbus.Sender) (string, *dbus.Error) {
|
||||
if err := endpoint.begin(sender, "GetRuntimeStateSnapshot"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer endpoint.end()
|
||||
if handler, ok := endpoint.handler.(ControlHandler); ok {
|
||||
return handler.RuntimeStateSnapshot(), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) Quit(sender dbus.Sender) (bool, *dbus.Error) {
|
||||
if err := endpoint.begin(sender, "Quit"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
if handler, ok := endpoint.handler.(ControlHandler); ok {
|
||||
return handler.Quit(), nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) SpeakText(sender dbus.Sender, text string, interrupt bool) (bool, *dbus.Error) {
|
||||
endpoint.logRequest("SpeakText")
|
||||
if err := endpoint.begin(sender, "SpeakText"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
if text == "" || len(text) > maxTextBytes {
|
||||
endpoint.logResponse("SpeakText", false)
|
||||
return false, nil
|
||||
}
|
||||
accepted := endpoint.handler.SpeakText(text, interrupt)
|
||||
endpoint.logResponse("SpeakText", accepted)
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) BrailleMessage(sender dbus.Sender, text string) (bool, *dbus.Error) {
|
||||
endpoint.logRequest("BrailleMessage")
|
||||
if err := endpoint.begin(sender, "BrailleMessage"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
if text == "" || len(text) > maxTextBytes {
|
||||
endpoint.logResponse("BrailleMessage", false)
|
||||
return false, nil
|
||||
}
|
||||
accepted := endpoint.handler.BrailleMessage(text)
|
||||
endpoint.logResponse("BrailleMessage", accepted)
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) CancelSpeech(sender dbus.Sender) (bool, *dbus.Error) {
|
||||
endpoint.logRequest("CancelSpeech")
|
||||
if err := endpoint.begin(sender, "CancelSpeech"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
accepted := endpoint.handler.CancelSpeech()
|
||||
endpoint.logResponse("CancelSpeech", accepted)
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) SpeakSsml(sender dbus.Sender, requestID, ssml string) (int32, *dbus.Error) {
|
||||
endpoint.logRequest("SpeakSsml")
|
||||
if err := endpoint.begin(sender, "SpeakSsml"); err != nil {
|
||||
return windowsErrorInvalidParameter, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
if len(requestID) > maxRequestIDBytes || len(ssml) > maxTextBytes || !validSSML(ssml) {
|
||||
endpoint.logResponse("SpeakSsml", windowsErrorInvalidParameter)
|
||||
return windowsErrorInvalidParameter, nil
|
||||
}
|
||||
endpoint.logResponse("SpeakSsml", windowsErrorNotSupported)
|
||||
return windowsErrorNotSupported, nil
|
||||
}
|
||||
|
||||
func (endpoint *endpoint) PresentAccessibleEvent(sender dbus.Sender, sourceID, eventType, name, role, value, states, description string, position, count int32, windowTitle string) (bool, *dbus.Error) {
|
||||
endpoint.logRequest("PresentAccessibleEvent")
|
||||
if err := endpoint.begin(sender, "PresentAccessibleEvent"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
for _, field := range []string{sourceID, eventType, name, role, value, states, description, windowTitle} {
|
||||
if len(field) > 4096 {
|
||||
endpoint.logResponse("PresentAccessibleEvent", false)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if sourceID == "" {
|
||||
endpoint.logResponse("PresentAccessibleEvent", false)
|
||||
return false, nil
|
||||
}
|
||||
accepted := endpoint.handler.PresentAccessibleEvent(AccessibleEvent{
|
||||
SourceID: sourceID, EventType: eventType, Name: name, Role: role,
|
||||
Value: value, States: states, Description: description,
|
||||
Position: position, Count: count, WindowTitle: windowTitle,
|
||||
})
|
||||
endpoint.logResponse("PresentAccessibleEvent", accepted)
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
func validSSML(value string) bool {
|
||||
var root struct {
|
||||
XMLName xml.Name
|
||||
}
|
||||
if err := xml.Unmarshal([]byte(value), &root); err != nil {
|
||||
return false
|
||||
}
|
||||
return root.XMLName.Local == "speak"
|
||||
}
|
||||
|
||||
// speechManagerEndpoint shares authorization and shutdown accounting with the
|
||||
// main service. Python's notify_user argument does not affect speech cancellation.
|
||||
type speechManagerEndpoint struct {
|
||||
endpoint *endpoint
|
||||
}
|
||||
|
||||
func (manager *speechManagerEndpoint) InterruptSpeech(sender dbus.Sender, _ bool) (bool, *dbus.Error) {
|
||||
endpoint := manager.endpoint
|
||||
endpoint.logRequest("InterruptSpeech")
|
||||
if err := endpoint.begin(sender, "InterruptSpeech"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer endpoint.end()
|
||||
accepted := endpoint.handler.CancelSpeech()
|
||||
endpoint.logResponse("InterruptSpeech", accepted)
|
||||
return accepted, nil
|
||||
}
|
||||
Reference in new issue
Block a user