Start work on Linux desktop portal kitten

This commit is contained in:
Kovid Goyal
2025-05-18 19:49:03 +05:30
parent 31d7dc43b0
commit c94844b220
7 changed files with 292 additions and 0 deletions

1
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

View File

@@ -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.",
})
}

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>

View File

@@ -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
}

View File

@@ -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",