diff --git a/go.mod b/go.mod index b02a030d3..a4283e9f7 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/dlclark/regexp2 v1.11.5 github.com/edwvee/exiffix v0.0.0-20240229113213-0dbb146775be + github.com/godbus/dbus/v5 v5.1.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/kovidgoyal/imaging v1.6.4 diff --git a/go.sum b/go.sum index cd2ff5036..fb193962e 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/edwvee/exiffix v0.0.0-20240229113213-0dbb146775be h1:FNPYI8/ifKGW7kdB github.com/edwvee/exiffix v0.0.0-20240229113213-0dbb146775be/go.mod h1:G3dK5MziX9e4jUa8PWjowCOPCcyQwxsZ5a0oYA73280= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/kittens/desktop_ui/__init__.py b/kittens/desktop_ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kittens/desktop_ui/main.go b/kittens/desktop_ui/main.go new file mode 100644 index 000000000..4e0b77f5b --- /dev/null +++ b/kittens/desktop_ui/main.go @@ -0,0 +1,54 @@ +package desktop_ui + +import ( + "context" + "fmt" + + "github.com/kovidgoyal/kitty/tools/cli" + "github.com/kovidgoyal/kitty/tools/utils" +) + +var _ = fmt.Print + +type Options struct { + Color_scheme string +} + +func run_server(opts *Options) (err error) { + portal := NewPortal(opts) + ctx := context.Background() + err = portal.Start(ctx) + // Run until explicitly stopped. + <-ctx.Done() + return +} + +func EntryPoint(root *cli.Command) { + parent := root.AddSubCommand(&cli.Command{ + Name: "desktop-ui", + ShortDescription: "Implement various desktop components for use with lightweight compositors/window managers on Linux", + Run: func(cmd *cli.Command, args []string) (int, error) { + cmd.ShowHelp() + return 1, nil + }, + }) + rs := parent.AddSubCommand(&cli.Command{ + Name: "run-server", + ShortDescription: "Start the various servers used to integrate with the Linux desktop", + HelpText: "This should be run very early in the startup sequence of your window manager, before any other programs are run.", + Run: func(cmd *cli.Command, args []string) (rc int, err error) { + opts := Options{} + err = cmd.GetOptionValues(&opts) + if err == nil { + err = run_server(&opts) + } + return utils.IfElse(err == nil, 0, 1), err + }, + }) + rs.Add(cli.OptionSpec{ + Name: `--color-scheme`, Type: "choices", Dest: `Color_scheme`, Choices: "no-preference, light, dark", + Completer: cli.NamesCompleter("Choices for color-scheme", "no-preference", "light", "dark"), + Help: "The color scheme for your system. This sets the initial value of the color scheme. It can be changed subsequently by using the color-scheme sub-command.", + }) + +} diff --git a/kittens/desktop_ui/main.py b/kittens/desktop_ui/main.py new file mode 100644 index 000000000..d3a5d0452 --- /dev/null +++ b/kittens/desktop_ui/main.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2025, Kovid Goyal + + diff --git a/kittens/desktop_ui/portal.go b/kittens/desktop_ui/portal.go new file mode 100644 index 000000000..7487371ad --- /dev/null +++ b/kittens/desktop_ui/portal.go @@ -0,0 +1,228 @@ +package desktop_ui + +import ( + "context" + "fmt" + "os" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" +) + +var _ = fmt.Print + +const PORTAL_COLOR_SCHEME_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_INTERFACE = "org.freedesktop.impl.portal.Settings" + +// Special portal setting used to check if darkman is in used by the portal. +const SETTINGS_CANARY_NAMESPACE = "net.kovidgoyal.kitty" +const SETTINGS_CANARY_KEY = "status" + +type ColorScheme uint + +const ( + NO_PREFERENCE ColorScheme = iota + DARK + LIGHT +) + +type Portal struct { + Color_scheme ColorScheme + bus *dbus.Conn +} + +func NewPortal(opts *Options) *Portal { + ans := Portal{} + switch opts.Color_scheme { + case "dark": + ans.Color_scheme = DARK + case "light": + ans.Color_scheme = LIGHT + default: + ans.Color_scheme = NO_PREFERENCE + } + return &ans +} + +func (self *Portal) Start(ctx context.Context) (err error) { + dbus_opts := dbus.WithContext(ctx) + if self.bus, err = dbus.ConnectSessionBus(dbus_opts); err != nil { + return fmt.Errorf("could not connect to session D-Bus: %s", err) + } + + // Define the "Version" prop (its value will be static). + propsSpec := map[string]map[string]*prop.Prop{ + SETTINGS_INTERFACE: { + "Version": { + Value: 1, + Writable: false, + Emit: prop.EmitTrue, + }, + }, + } + // Export the "Version" prop. + versionProp, err := prop.Export(self.bus, PORTAL_OBJ_PATH, propsSpec) + if err != nil { + return fmt.Errorf("failed to export D-Bus prop: %v", err) + } + + // Exoprt the D-Bus object. + if err = self.bus.Export(self.bus, PORTAL_OBJ_PATH, SETTINGS_INTERFACE); err != nil { + return fmt.Errorf("failed to export interface: %v", err) + } + + // Declare change signal + settingChanged := introspect.Signal{ + Name: "SettingChanged", + Args: []introspect.Arg{ + { + Name: "namespace", + Type: "s", + }, + { + Name: "key", + Type: "s", + }, + { + Name: "value", + Type: "v", + }, + }, + } + + readMethod := introspect.Method{ + Name: "Read", + Args: []introspect.Arg{ + { + Name: "namespace", + Type: "s", + Direction: "in", + }, + { + Name: "key", + Type: "s", + Direction: "in", + }, + { + Name: "value", + Type: "v", + Direction: "out", + }, + }, + } + readAllMethod := introspect.Method{ + Name: "ReadAll", + Args: []introspect.Arg{ + { + Name: "namespaces", + Type: "as", + Direction: "in", + }, + { + Name: "value", + Type: "a{sa{sv}}", + Direction: "out", + }, + }, + } + + portalInterface := introspect.Interface{ + Name: SETTINGS_INTERFACE, + Signals: []introspect.Signal{settingChanged}, + Properties: versionProp.Introspection(SETTINGS_INTERFACE), + Methods: []introspect.Method{readMethod, readAllMethod}, + } + + n := &introspect.Node{ + Name: PORTAL_OBJ_PATH, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + portalInterface, + }, + } + + if err = self.bus.Export( + introspect.NewIntrospectable(n), + PORTAL_OBJ_PATH, + "org.freedesktop.DBus.Introspectable", + ); err != nil { + return fmt.Errorf("failed to export dbus name: %v", err) + } + + reply, err := self.bus.RequestName(PORTAL_BUS_NAME, dbus.NameFlagDoNotQueue) + if err != nil { + return fmt.Errorf("failed to register dbus name: %v", err) + } + if reply != dbus.RequestNameReplyPrimaryOwner { + return fmt.Errorf("can't register D-Bus name: name already taken") + } + + return +} + +func (self *Portal) ChangeMode(x string) (err error) { + if self.bus == nil { + return fmt.Errorf("cannot emit portal signal; no connection to dbus") + } + switch x { + case "toggle": + switch self.Color_scheme { + case LIGHT: + self.Color_scheme = DARK + case DARK: + self.Color_scheme = LIGHT + } + case "light": + self.Color_scheme = LIGHT + case "dark": + self.Color_scheme = DARK + case "no-preference": + self.Color_scheme = NO_PREFERENCE + default: + return fmt.Errorf("%s is not a valid value for color-scheme. Valid values are: light, dark, no-preference and toggle", x) + } + + if err = self.bus.Emit( + PORTAL_OBJ_PATH, + SETTINGS_INTERFACE+".SettingChanged", + PORTAL_COLOR_SCHEME_NAMESPACE, + PORTAL_COLOR_SCHEME_KEY, + dbus.MakeVariant(self.Color_scheme), + ); err != nil { + fmt.Fprintf(os.Stderr, "Couldn't emit signal: %s", err) + err = nil + } + return +} + +func (self *Portal) Read(namespace string, key string) (dbus.Variant, *dbus.Error) { + if namespace == PORTAL_COLOR_SCHEME_NAMESPACE && key == PORTAL_COLOR_SCHEME_KEY { + return dbus.MakeVariant(self.Color_scheme), nil + } + if namespace == SETTINGS_CANARY_NAMESPACE && key == SETTINGS_CANARY_KEY { + return dbus.MakeVariant("running"), 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 *Portal) ReadAll(namespaces []string) (map[string]map[string]dbus.Variant, *dbus.Error) { + values := map[string]map[string]dbus.Variant{} + for _, namespace := range namespaces { + if namespace == PORTAL_COLOR_SCHEME_NAMESPACE { + values[PORTAL_COLOR_SCHEME_NAMESPACE] = map[string]dbus.Variant{ + PORTAL_COLOR_SCHEME_KEY: dbus.MakeVariant(self.Color_scheme), + } + } else if namespace == SETTINGS_CANARY_NAMESPACE { + values[SETTINGS_CANARY_NAMESPACE] = map[string]dbus.Variant{ + SETTINGS_CANARY_KEY: dbus.MakeVariant("running"), + } + } + } + return values, nil +} diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index fdd87c19f..7a4725526 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -8,6 +8,7 @@ import ( "github.com/kovidgoyal/kitty/kittens/ask" "github.com/kovidgoyal/kitty/kittens/choose_fonts" "github.com/kovidgoyal/kitty/kittens/clipboard" + "github.com/kovidgoyal/kitty/kittens/desktop_ui" "github.com/kovidgoyal/kitty/kittens/diff" "github.com/kovidgoyal/kitty/kittens/hints" "github.com/kovidgoyal/kitty/kittens/hyperlinked_grep" @@ -63,6 +64,8 @@ func KittyToolEntryPoints(root *cli.Command) { unicode_input.EntryPoint(root) // show_key show_key.EntryPoint(root) + // desktop_ui + desktop_ui.EntryPoint(root) // mouse_demo root.AddSubCommand(&cli.Command{ Name: "mouse-demo",