diff --git a/kittens/desktop_ui/portal.go b/kittens/desktop_ui/portal.go index 7a8099775..e75ff8632 100644 --- a/kittens/desktop_ui/portal.go +++ b/kittens/desktop_ui/portal.go @@ -10,6 +10,7 @@ import ( "github.com/kovidgoyal/dbus" "github.com/kovidgoyal/dbus/introspect" "github.com/kovidgoyal/dbus/prop" + "github.com/kovidgoyal/kitty/tools/utils" ) var _ = fmt.Print @@ -17,11 +18,12 @@ var _ = fmt.Print const PORTAL_APPEARANCE_NAMESPACE = "org.freedesktop.appearance" const PORTAL_COLOR_SCHEME_KEY = "color-scheme" const PORTAL_BUS_NAME = "org.freedesktop.impl.portal.desktop.kitty" -const PORTAL_OBJ_PATH = "/org/freedesktop/portal/desktop" - +const SETTINGS_OBJECT_PATH = "/org/freedesktop/portal/desktop" const SETTINGS_INTERFACE = "org.freedesktop.impl.portal.Settings" +const CHANGE_SETTINGS_OBJECT_PATH = "/net/kovidgoyal/kitty/portal" +const CHANGE_SETTINGS_INTERFACE = "net.kovidgoyal.kitty.settings" -// Special portal setting used to check if darkman is in used by the portal. +// Special portal setting used to check if we are being called by xdg-desktop-portal const SETTINGS_CANARY_NAMESPACE = "net.kovidgoyal.kitty" const SETTINGS_CANARY_KEY = "status" @@ -35,31 +37,27 @@ const ( type SettingsMap map[string]map[string]dbus.Variant -type Settings struct { - items SettingsMap - lock sync.Mutex -} - type Portal struct { bus *dbus.Conn - settings Settings + settings SettingsMap + lock sync.Mutex } func NewPortal(opts *Options) *Portal { ans := Portal{} - ans.settings.items = SettingsMap{ + ans.settings = SettingsMap{ SETTINGS_CANARY_NAMESPACE: map[string]dbus.Variant{ SETTINGS_CANARY_KEY: dbus.MakeVariant("running"), }, } - ans.settings.items[PORTAL_APPEARANCE_NAMESPACE] = map[string]dbus.Variant{} + ans.settings[PORTAL_APPEARANCE_NAMESPACE] = map[string]dbus.Variant{} switch opts.Color_scheme { case "dark": - ans.settings.items[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(DARK)) + ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(DARK)) case "light": - ans.settings.items[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(LIGHT)) + ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(LIGHT)) default: - ans.settings.items[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(NO_PREFERENCE)) + ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(NO_PREFERENCE)) } return &ans } @@ -68,10 +66,33 @@ type PropSpec map[string]*prop.Prop type SignalSpec map[string][]struct { Name, Type string } +type MethodSpec map[string][]struct { + Name, Type string + Out bool +} -func ExportInterface(conn *dbus.Conn, object any, interface_name, object_path string, prop_spec PropSpec, signal_spec SignalSpec) (err error) { +func ExportInterface(conn *dbus.Conn, object any, interface_name, object_path string, method_spec MethodSpec, prop_spec PropSpec, signal_spec SignalSpec) (err error) { op := dbus.ObjectPath(object_path) - if err = conn.Export(object, op, interface_name); err != nil { + method_map := make(map[string]string, len(method_spec)) + methods := []introspect.Method{} + if len(method_spec) > 0 { + for method_name, args := range method_spec { + method_map[method_name] = method_name + meth_args := make([]introspect.Arg, len(args)) + for i, a := range args { + meth_args[i] = introspect.Arg{ + Name: a.Name, + Type: a.Type, + Direction: utils.IfElse(a.Out, "out", "in"), + } + } + methods = append(methods, introspect.Method{ + Name: method_name, + Args: meth_args, + }) + } + } + if err = conn.ExportWithMap(object, method_map, op, interface_name); err != nil { return fmt.Errorf("failed to export interface: %s at object path: %s with error: %w", interface_name, object_path, err) } var properties []introspect.Property @@ -100,9 +121,10 @@ func ExportInterface(conn *dbus.Conn, object any, interface_name, object_path st }) } } + interface_data := introspect.Interface{ Name: interface_name, - Methods: introspect.Methods(object), + Methods: methods, Properties: properties, Signals: signals, } @@ -113,7 +135,7 @@ func ExportInterface(conn *dbus.Conn, object any, interface_name, object_path st interfaces = append(interfaces, prop.IntrospectData) } n := &introspect.Node{Name: object_path, Interfaces: interfaces} - if err = conn.Export(introspect.NewIntrospectable(n), op, "org.freedesktop.DBus.Introspectable"); err != nil { + if err = conn.Export(introspect.NewIntrospectable(n), op, introspect.IntrospectData.Name); err != nil { return fmt.Errorf("failed to export introspected methods with error: %w", err) } return @@ -130,82 +152,92 @@ func (self *Portal) Start() (err error) { if reply != dbus.RequestNameReplyPrimaryOwner { return fmt.Errorf("can't register D-Bus name: name already taken") } - props_spec := PropSpec{ + props := PropSpec{ "version": {Value: uint32(1), Writable: false, Emit: prop.EmitFalse}, } signals := SignalSpec{ "SettingChanged": {{"namespace", "s"}, {"key", "s"}, {"value", "v"}}, } - if err = ExportInterface(self.bus, &self.settings, SETTINGS_INTERFACE, PORTAL_OBJ_PATH, props_spec, signals); err != nil { + methods := MethodSpec{ + "Read": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", true}}, + "ReadAll": {{"namespaces", "as", false}, {"value", "a{sa{sv}}", true}}, + } + if err = ExportInterface(self.bus, self, SETTINGS_INTERFACE, SETTINGS_OBJECT_PATH, methods, props, signals); err != nil { + return + } + methods = MethodSpec{ + "ChangeSetting": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", false}}, + } + props["version"].Value = uint32(1) + if err = ExportInterface(self.bus, self, CHANGE_SETTINGS_INTERFACE, CHANGE_SETTINGS_OBJECT_PATH, methods, props, nil); err != nil { return } - return } -func (self *Portal) ChangeSetting(namespace, key, value, value_type_signature string) (err error) { - if self.bus == nil { - return fmt.Errorf("cannot emit portal signal; no connection to dbus") - } +func ParseValue(value, value_type_signature string) (dbus.Variant, error) { s, err := dbus.ParseSignature(value_type_signature) if err != nil { - return fmt.Errorf("%s is not a valid type signature: %w", value_type_signature, err) + return dbus.Variant{}, fmt.Errorf("%s is not a valid type signature: %w", value_type_signature, err) } v, err := dbus.ParseVariant(value, s) if err != nil { - return fmt.Errorf("%s is not a valid value for signature: %s with error: %w", value, value_type_signature, err) + return dbus.Variant{}, fmt.Errorf("%s is not a valid value for signature: %s with error: %w", value, value_type_signature, err) } - self.settings.lock.Lock() - defer self.settings.lock.Unlock() - if self.settings.items[namespace] == nil { - self.settings.items[namespace] = map[string]dbus.Variant{} - } - self.settings.items[namespace][key] = v - - if err = self.bus.Emit( - PORTAL_OBJ_PATH, - SETTINGS_INTERFACE+".SettingChanged", - PORTAL_APPEARANCE_NAMESPACE, - PORTAL_COLOR_SCHEME_KEY, - v, - ); err != nil { - fmt.Fprintf(os.Stderr, "Couldn't emit signal: %s", err) - err = nil - } - return + return v, nil } -func (self *Settings) Read(namespace string, key string) (dbus.Variant, *dbus.Error) { +func (self *Portal) ChangeSetting(namespace, key string, value dbus.Variant) *dbus.Error { self.lock.Lock() defer self.lock.Unlock() - if m, found := self.items[namespace]; found { + if self.settings[namespace] == nil { + self.settings[namespace] = map[string]dbus.Variant{} + } + self.settings[namespace][key] = value + + if e := self.bus.Emit( + SETTINGS_OBJECT_PATH, + SETTINGS_INTERFACE+".SettingChanged", + namespace, + key, + value, + ); e != nil { + fmt.Fprintf(os.Stderr, "Couldn't emit signal: %s", e) + } + return nil +} + +func (self *Portal) Read(namespace, key string) (dbus.Variant, *dbus.Error) { + self.lock.Lock() + defer self.lock.Unlock() + if m, found := self.settings[namespace]; found { if v, found := m[key]; found { - return dbus.MakeVariant(v), nil + return v, nil } } return dbus.Variant{}, dbus.NewError("org.freedesktop.portal.Error.NotFound", []any{fmt.Sprintf("the setting %s in the namespace %s is not supported", key, namespace)}) } -func (self *Settings) ReadAll(namespaces []string) (map[string]map[string]dbus.Variant, *dbus.Error) { +func (self *Portal) ReadAll(namespaces []string) (map[string]map[string]dbus.Variant, *dbus.Error) { self.lock.Lock() defer self.lock.Unlock() var matched_namespaces = SettingsMap{} if len(namespaces) == 0 { - matched_namespaces = self.items + matched_namespaces = self.settings } else { for _, namespace := range namespaces { if namespace == "" { - matched_namespaces = self.items + matched_namespaces = self.settings break } else { if strings.HasSuffix(namespace, ".*") { namespace = namespace[:len(namespace)-1] - for candidate := range self.items { + for candidate := range self.settings { if strings.HasPrefix(candidate, namespace) { matched_namespaces[candidate] = map[string]dbus.Variant{} } } - } else if _, found := self.items[namespace]; found { + } else if _, found := self.settings[namespace]; found { matched_namespaces[namespace] = map[string]dbus.Variant{} } } @@ -213,8 +245,8 @@ func (self *Settings) ReadAll(namespaces []string) (map[string]map[string]dbus.V } values := map[string]map[string]dbus.Variant{} for namespace := range matched_namespaces { - values[namespace] = make(map[string]dbus.Variant, len(self.items[namespace])) - maps.Copy(values[namespace], self.items[namespace]) + values[namespace] = make(map[string]dbus.Variant, len(self.settings[namespace])) + maps.Copy(values[namespace], self.settings[namespace]) } return values, nil }