387 lines
15 KiB
Go
387 lines
15 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package remote
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
"github.com/godbus/dbus/v5/introspect"
|
|
)
|
|
|
|
const (
|
|
typingEchoInterface = "org.stormux.Cthulhu1.TypingEchoManager"
|
|
orcaModuleInterface = "org.gnome.Orca.Module"
|
|
)
|
|
|
|
// ModuleHandler is implemented by the runtime settings and command owners.
|
|
// The remote layer never changes a settings snapshot directly.
|
|
type ModuleHandler interface {
|
|
RemoteGet(module, property string) (dbus.Variant, bool)
|
|
RemoteSet(module, property string, value dbus.Variant) bool
|
|
RemoteCommand(module, command string, notify bool) bool
|
|
}
|
|
|
|
// ModuleAvailability lets a handler suppress modules until its settings owner
|
|
// is wired. Handlers without this method are expected to provide their own
|
|
// complete module implementation.
|
|
type ModuleAvailability interface{ RemoteModulesAvailable() bool }
|
|
|
|
type propertySpec struct {
|
|
name, signature string
|
|
writable bool
|
|
}
|
|
type commandSpec struct{ name, description string }
|
|
type moduleSpec struct {
|
|
name string
|
|
properties []propertySpec
|
|
commands []commandSpec
|
|
}
|
|
|
|
var modules = []moduleSpec{
|
|
{name: "SpeechManager", properties: []propertySpec{
|
|
{"CurrentServer", "s", false}, {"AvailableServers", "as", false},
|
|
{"CurrentSynthesizer", "s", true}, {"CurrentVoice", "s", true},
|
|
{"Rate", "i", true}, {"Pitch", "d", true}, {"Volume", "d", true},
|
|
{"SpeechIsEnabled", "b", false}, {"PunctuationLevel", "s", true},
|
|
{"CapitalizationStyle", "s", true}, {"SpeakDescription", "b", true},
|
|
{"SpeakRowInGuiTable", "b", true},
|
|
}, commands: []commandSpec{
|
|
{"SelectPreviousSpeechSetting", "Selects the previous speech setting."},
|
|
{"SelectNextSpeechSetting", "Selects the next speech setting."},
|
|
{"DecreaseCurrentSpeechSetting", "Decreases the selected speech setting."},
|
|
{"IncreaseCurrentSpeechSetting", "Increases the selected speech setting."},
|
|
{"SaveSpeechSettings", "Saves the current speech settings."},
|
|
{"InterruptSpeech", "Stops current speech."},
|
|
{"DecreaseRate", "Decreases speech rate."}, {"IncreaseRate", "Increases speech rate."},
|
|
{"DecreasePitch", "Decreases speech pitch."}, {"IncreasePitch", "Increases speech pitch."},
|
|
{"DecreaseVolume", "Decreases speech volume."}, {"IncreaseVolume", "Increases speech volume."},
|
|
}},
|
|
{name: "TypingEchoManager", properties: []propertySpec{
|
|
{"KeyEchoEnabled", "b", true}, {"CharacterEchoEnabled", "b", true},
|
|
{"WordEchoEnabled", "b", true}, {"SentenceEchoEnabled", "b", true},
|
|
{"AlphabeticKeysEnabled", "b", true}, {"NumericKeysEnabled", "b", true},
|
|
{"PunctuationKeysEnabled", "b", true}, {"SpaceEnabled", "b", true},
|
|
{"ModifierKeysEnabled", "b", true}, {"FunctionKeysEnabled", "b", true},
|
|
{"ActionKeysEnabled", "b", true}, {"NavigationKeysEnabled", "b", true},
|
|
{"DiacriticalKeysEnabled", "b", true},
|
|
}, commands: []commandSpec{{"CycleKeyEcho", "Cycles typing echo mode."}}},
|
|
}
|
|
|
|
func modulePath(base dbus.ObjectPath, name string) dbus.ObjectPath {
|
|
return dbus.ObjectPath(string(base) + "/" + name)
|
|
}
|
|
|
|
func moduleFor(name string) *moduleSpec {
|
|
for i := range modules {
|
|
if modules[i].name == name {
|
|
return &modules[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (spec moduleSpec) property(name string) (propertySpec, bool) {
|
|
for _, item := range spec.properties {
|
|
if item.name == name {
|
|
return item, true
|
|
}
|
|
}
|
|
return propertySpec{}, false
|
|
}
|
|
|
|
func (spec moduleSpec) command(name string) bool {
|
|
for _, item := range spec.commands {
|
|
if item.name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (spec moduleSpec) introspection(orca bool) introspect.Interface {
|
|
iface := "org.stormux.Cthulhu1." + spec.name
|
|
if orca {
|
|
iface = orcaModuleInterface
|
|
}
|
|
result := introspect.Interface{Name: iface}
|
|
if orca {
|
|
result.Methods = orcaModuleMethods()
|
|
} else {
|
|
for _, command := range spec.commands {
|
|
result.Methods = append(result.Methods, introspect.Method{Name: command.name, Args: []introspect.Arg{
|
|
{Name: "notify_user", Type: "b", Direction: "in"}, {Name: "accepted", Type: "b", Direction: "out"},
|
|
}})
|
|
}
|
|
for _, property := range spec.properties {
|
|
access := "read"
|
|
if property.writable {
|
|
access = "readwrite"
|
|
}
|
|
result.Properties = append(result.Properties, introspect.Property{Name: property.name, Type: property.signature, Access: access})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func orcaModuleMethods() []introspect.Method {
|
|
return []introspect.Method{
|
|
{Name: "ExecuteRuntimeGetter", Args: []introspect.Arg{{Name: "getter_name", Type: "s", Direction: "in"}, {Name: "value", Type: "v", Direction: "out"}}},
|
|
{Name: "ExecuteRuntimeSetter", Args: []introspect.Arg{{Name: "setter_name", Type: "s", Direction: "in"}, {Name: "value", Type: "v", Direction: "in"}, {Name: "accepted", Type: "b", Direction: "out"}}},
|
|
{Name: "ExecuteCommand", Args: []introspect.Arg{{Name: "command_name", Type: "s", Direction: "in"}, {Name: "notify_user", Type: "b", Direction: "in"}, {Name: "accepted", Type: "b", Direction: "out"}}},
|
|
{Name: "ListCommands", Args: []introspect.Arg{{Name: "commands", Type: "a(ss)", Direction: "out"}}},
|
|
{Name: "ListRuntimeGetters", Args: []introspect.Arg{{Name: "getters", Type: "a(ss)", Direction: "out"}}},
|
|
{Name: "ListRuntimeSetters", Args: []introspect.Arg{{Name: "setters", Type: "a(ss)", Direction: "out"}}},
|
|
{Name: "ListParameterizedCommands", Args: []introspect.Arg{{Name: "commands", Type: "a(ssa(ss))", Direction: "out"}}},
|
|
{Name: "ExecuteParameterizedCommand", Args: []introspect.Arg{{Name: "command_name", Type: "s", Direction: "in"}, {Name: "parameters", Type: "a{sv}", Direction: "in"}, {Name: "notify_user", Type: "b", Direction: "in"}, {Name: "result", Type: "v", Direction: "out"}}},
|
|
}
|
|
}
|
|
|
|
type namedDescription struct{ Name, Description string }
|
|
type parameterizedDescription struct {
|
|
Name, Description string
|
|
Parameters []namedDescription
|
|
}
|
|
|
|
type moduleEndpoint struct {
|
|
endpoint *endpoint
|
|
spec moduleSpec
|
|
}
|
|
|
|
func (module *moduleEndpoint) control() ModuleHandler {
|
|
handler, _ := module.endpoint.handler.(ModuleHandler)
|
|
return handler
|
|
}
|
|
|
|
func unsupported(message string) *dbus.Error {
|
|
return &dbus.Error{Name: "org.freedesktop.DBus.Error.NotSupported", Body: []any{message}}
|
|
}
|
|
|
|
func (module *moduleEndpoint) ExecuteRuntimeGetter(sender dbus.Sender, name string) (dbus.Variant, *dbus.Error) {
|
|
if err := module.endpoint.begin(sender, "ExecuteRuntimeGetter"); err != nil {
|
|
return dbus.Variant{}, err
|
|
}
|
|
defer module.endpoint.end()
|
|
if len(name) > 256 {
|
|
return dbus.Variant{}, unsupported("getter name is too long")
|
|
}
|
|
if _, ok := module.spec.property(name); !ok || module.control() == nil {
|
|
return dbus.Variant{}, unsupported("getter is unavailable")
|
|
}
|
|
if value, ok := module.control().RemoteGet(module.spec.name, name); ok {
|
|
return value, nil
|
|
}
|
|
return dbus.Variant{}, unsupported("getter is unavailable")
|
|
}
|
|
|
|
func (module *moduleEndpoint) ExecuteRuntimeSetter(sender dbus.Sender, name string, value dbus.Variant) (bool, *dbus.Error) {
|
|
if err := module.endpoint.begin(sender, "ExecuteRuntimeSetter"); err != nil {
|
|
return false, err
|
|
}
|
|
defer module.endpoint.end()
|
|
property, ok := module.spec.property(name)
|
|
if !ok || !property.writable || module.control() == nil {
|
|
return false, unsupported("setter is unavailable")
|
|
}
|
|
if value.Signature().String() != property.signature {
|
|
return false, &dbus.Error{Name: "org.freedesktop.DBus.Error.InvalidArgs", Body: []any{"property type mismatch"}}
|
|
}
|
|
return module.control().RemoteSet(module.spec.name, name, value), nil
|
|
}
|
|
|
|
func (module *moduleEndpoint) ExecuteCommand(sender dbus.Sender, name string, notify bool) (bool, *dbus.Error) {
|
|
if err := module.endpoint.begin(sender, "ExecuteCommand"); err != nil {
|
|
return false, err
|
|
}
|
|
defer module.endpoint.end()
|
|
if !module.spec.command(name) || module.control() == nil {
|
|
return false, unsupported("command is unavailable")
|
|
}
|
|
return module.control().RemoteCommand(module.spec.name, name, notify), nil
|
|
}
|
|
|
|
func (module *moduleEndpoint) ListCommands(sender dbus.Sender) ([]namedDescription, *dbus.Error) {
|
|
if err := module.endpoint.begin(sender, "ListCommands"); err != nil {
|
|
return nil, err
|
|
}
|
|
defer module.endpoint.end()
|
|
list := make([]namedDescription, 0, len(module.spec.commands))
|
|
for _, item := range module.spec.commands {
|
|
list = append(list, namedDescription{item.name, item.description})
|
|
}
|
|
return list, nil
|
|
}
|
|
func (module *moduleEndpoint) ListRuntimeGetters(sender dbus.Sender) ([]namedDescription, *dbus.Error) {
|
|
if err := module.endpoint.begin(sender, "ListRuntimeGetters"); err != nil {
|
|
return nil, err
|
|
}
|
|
defer module.endpoint.end()
|
|
list := make([]namedDescription, 0, len(module.spec.properties))
|
|
for _, item := range module.spec.properties {
|
|
list = append(list, namedDescription{item.name, "Returns " + item.name + "."})
|
|
}
|
|
return list, nil
|
|
}
|
|
func (module *moduleEndpoint) ListRuntimeSetters(sender dbus.Sender) ([]namedDescription, *dbus.Error) {
|
|
if err := module.endpoint.begin(sender, "ListRuntimeSetters"); err != nil {
|
|
return nil, err
|
|
}
|
|
defer module.endpoint.end()
|
|
list := []namedDescription{}
|
|
for _, item := range module.spec.properties {
|
|
if item.writable {
|
|
list = append(list, namedDescription{item.name, "Sets " + item.name + " for this session."})
|
|
}
|
|
}
|
|
return list, nil
|
|
}
|
|
func (module *moduleEndpoint) ListParameterizedCommands(sender dbus.Sender) ([]parameterizedDescription, *dbus.Error) {
|
|
if err := module.endpoint.begin(sender, "ListParameterizedCommands"); err != nil {
|
|
return nil, err
|
|
}
|
|
defer module.endpoint.end()
|
|
return []parameterizedDescription{}, nil
|
|
}
|
|
func (module *moduleEndpoint) ExecuteParameterizedCommand(sender dbus.Sender, _ string, _ map[string]dbus.Variant, _ bool) (dbus.Variant, *dbus.Error) {
|
|
if err := module.endpoint.begin(sender, "ExecuteParameterizedCommand"); err != nil {
|
|
return dbus.Variant{}, err
|
|
}
|
|
defer module.endpoint.end()
|
|
return dbus.Variant{}, unsupported("parameterized command is unavailable")
|
|
}
|
|
|
|
type modulePropertiesEndpoint struct {
|
|
module *moduleEndpoint
|
|
iface string
|
|
}
|
|
|
|
func (properties *modulePropertiesEndpoint) Get(sender dbus.Sender, iface, name string) (dbus.Variant, *dbus.Error) {
|
|
module := properties.module
|
|
if err := module.endpoint.begin(sender, "Properties.Get"); err != nil {
|
|
return dbus.Variant{}, err
|
|
}
|
|
defer module.endpoint.end()
|
|
if iface != properties.iface {
|
|
return dbus.Variant{}, &dbus.Error{Name: "org.freedesktop.DBus.Error.UnknownInterface", Body: []any{iface}}
|
|
}
|
|
if _, ok := module.spec.property(name); !ok {
|
|
return dbus.Variant{}, &dbus.Error{Name: "org.freedesktop.DBus.Error.UnknownProperty", Body: []any{name}}
|
|
}
|
|
if module.control() == nil {
|
|
return dbus.Variant{}, unsupported("property is unavailable")
|
|
}
|
|
value, ok := module.control().RemoteGet(module.spec.name, name)
|
|
if !ok {
|
|
return dbus.Variant{}, unsupported("property is unavailable")
|
|
}
|
|
return value, nil
|
|
}
|
|
func (properties *modulePropertiesEndpoint) Set(sender dbus.Sender, iface, name string, value dbus.Variant) *dbus.Error {
|
|
module := properties.module
|
|
if err := module.endpoint.begin(sender, "Properties.Set"); err != nil {
|
|
return err
|
|
}
|
|
defer module.endpoint.end()
|
|
if iface != properties.iface {
|
|
return &dbus.Error{Name: "org.freedesktop.DBus.Error.UnknownInterface", Body: []any{iface}}
|
|
}
|
|
property, ok := module.spec.property(name)
|
|
if !ok {
|
|
return &dbus.Error{Name: "org.freedesktop.DBus.Error.UnknownProperty", Body: []any{name}}
|
|
}
|
|
if !property.writable {
|
|
return &dbus.Error{Name: "org.freedesktop.DBus.Error.PropertyReadOnly", Body: []any{name}}
|
|
}
|
|
if value.Signature().String() != property.signature {
|
|
return &dbus.Error{Name: "org.freedesktop.DBus.Error.InvalidArgs", Body: []any{fmt.Sprintf("%s requires %s", name, property.signature)}}
|
|
}
|
|
if module.control() == nil || !module.control().RemoteSet(module.spec.name, name, value) {
|
|
return unsupported("setting was not applied")
|
|
}
|
|
return nil
|
|
}
|
|
func (properties *modulePropertiesEndpoint) GetAll(sender dbus.Sender, iface string) (map[string]dbus.Variant, *dbus.Error) {
|
|
module := properties.module
|
|
if err := module.endpoint.begin(sender, "Properties.GetAll"); err != nil {
|
|
return nil, err
|
|
}
|
|
defer module.endpoint.end()
|
|
if iface != properties.iface {
|
|
return nil, &dbus.Error{Name: "org.freedesktop.DBus.Error.UnknownInterface", Body: []any{iface}}
|
|
}
|
|
values := make(map[string]dbus.Variant)
|
|
if module.control() == nil {
|
|
return nil, unsupported("properties are unavailable")
|
|
}
|
|
for _, property := range module.spec.properties {
|
|
if value, ok := module.control().RemoteGet(module.spec.name, property.name); ok {
|
|
values[property.name] = value
|
|
}
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func sortedModuleNames() []string {
|
|
names := make([]string, 0, len(modules))
|
|
for _, module := range modules {
|
|
names = append(names, module.name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func nativeCommand(endpoint *endpoint, module, name string, sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
if err := endpoint.begin(sender, name); err != nil {
|
|
return false, err
|
|
}
|
|
defer endpoint.end()
|
|
handler, ok := endpoint.handler.(ModuleHandler)
|
|
if !ok {
|
|
return false, unsupported("command is unavailable")
|
|
}
|
|
return handler.RemoteCommand(module, name, notify), nil
|
|
}
|
|
|
|
func (manager *speechManagerEndpoint) SelectPreviousSpeechSetting(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "SelectPreviousSpeechSetting", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) SelectNextSpeechSetting(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "SelectNextSpeechSetting", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) DecreaseCurrentSpeechSetting(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "DecreaseCurrentSpeechSetting", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) IncreaseCurrentSpeechSetting(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "IncreaseCurrentSpeechSetting", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) SaveSpeechSettings(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "SaveSpeechSettings", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) DecreaseRate(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "DecreaseRate", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) IncreaseRate(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "IncreaseRate", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) DecreasePitch(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "DecreasePitch", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) IncreasePitch(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "IncreasePitch", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) DecreaseVolume(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "DecreaseVolume", sender, notify)
|
|
}
|
|
func (manager *speechManagerEndpoint) IncreaseVolume(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "SpeechManager", "IncreaseVolume", sender, notify)
|
|
}
|
|
|
|
type typingEchoEndpoint struct{ endpoint *endpoint }
|
|
|
|
func (manager *typingEchoEndpoint) CycleKeyEcho(sender dbus.Sender, notify bool) (bool, *dbus.Error) {
|
|
return nativeCommand(manager.endpoint, "TypingEchoManager", "CycleKeyEcho", sender, notify)
|
|
}
|