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