mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-05 23:51:29 +02:00
Start work on Linux desktop portal kitten
This commit is contained in:
1
go.mod
1
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
0
kittens/desktop_ui/__init__.py
Normal file
0
kittens/desktop_ui/__init__.py
Normal file
54
kittens/desktop_ui/main.go
Normal file
54
kittens/desktop_ui/main.go
Normal 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.",
|
||||
})
|
||||
|
||||
}
|
||||
4
kittens/desktop_ui/main.py
Normal file
4
kittens/desktop_ui/main.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
228
kittens/desktop_ui/portal.go
Normal file
228
kittens/desktop_ui/portal.go
Normal 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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user