From c36815a380ba471d7e23ae15ddc96b3c5995c5fc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 19 May 2025 13:10:32 +0530 Subject: [PATCH] Command to auto-create config files needed to get xdg-desktop-portal to use the kitty portal --- kittens/desktop_ui/main.go | 8 ++ kittens/desktop_ui/portal.go | 153 +++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/kittens/desktop_ui/main.go b/kittens/desktop_ui/main.go index ab26e920a..f66b95ecf 100644 --- a/kittens/desktop_ui/main.go +++ b/kittens/desktop_ui/main.go @@ -50,6 +50,14 @@ func EntryPoint(root *cli.Command) { 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.", }) + parent.AddSubCommand(&cli.Command{ + Name: "enable-portal", + ShortDescription: "This will create or edit the various files needed so that the portal from this kitten is used by xdg-desktop-portal", + Run: func(cmd *cli.Command, args []string) (rc int, err error) { + err = enable_portal() + return utils.IfElse(err == nil, 0, 1), err + }, + }) parent.AddSubCommand(&cli.Command{ Name: "set-color-scheme", ShortDescription: "Change the color scheme", diff --git a/kittens/desktop_ui/portal.go b/kittens/desktop_ui/portal.go index b43615530..6b5c7d59e 100644 --- a/kittens/desktop_ui/portal.go +++ b/kittens/desktop_ui/portal.go @@ -5,6 +5,7 @@ import ( "fmt" "maps" "os" + "path/filepath" "strings" "sync" @@ -12,6 +13,7 @@ import ( "github.com/kovidgoyal/dbus/introspect" "github.com/kovidgoyal/dbus/prop" "github.com/kovidgoyal/kitty/tools/utils" + "golang.org/x/sys/unix" ) var _ = fmt.Print @@ -255,6 +257,157 @@ func show_settings(opts *ShowSettingsOptions) (err error) { return } +var DataDirs = sync.OnceValue(func() (ans []string) { + d := os.Getenv("XDG_DATA_DIRS") + if d == "" { + d = "/usr/local/share/:/usr/share/" + } + all := []string{os.Getenv("XDG_DATA_HOME")} + all = append(all, strings.Split(d, ":")...) + seen := map[string]bool{} + for _, x := range all { + if !seen[x] { + seen[x] = true + ans = append(ans, x) + } + } + return +}) + +func IsDir(x string) bool { + s, err := os.Stat(x) + return err == nil && s.IsDir() +} + +var WritableDataDirs = sync.OnceValue(func() (ans []string) { + for _, x := range DataDirs() { + if err := os.MkdirAll(x, 0o755); err == nil && unix.Access(x, unix.W_OK) == nil { + ans = append(ans, x) + } + } + return +}) + +var AllPortalInterfaces = sync.OnceValue(func() (ans []string) { + return []string{SETTINGS_INTERFACE} +}) + +func patch_portals_conf(text []byte) []byte { + lines := []string{} + in_preferred := false + for _, line := range utils.Splitlines(utils.UnsafeBytesToString(text)) { + sl := strings.TrimSpace(line) + if strings.HasPrefix(sl, "[") { + in_preferred = sl == "[preferred]" + lines = append(lines, line) + for _, iface := range AllPortalInterfaces() { + lines = append(lines, iface+"=kitty") + } + } else if in_preferred { + remove := false + for _, iface := range AllPortalInterfaces() { + if strings.HasPrefix(sl, iface) { + remove = true + break + } + } + if !remove { + lines = append(lines, line) + } + } + } + return utils.UnsafeStringToBytes(strings.Join(lines, "\n")) +} + +func enable_portal() (err error) { + if len(WritableDataDirs()) == 0 { + return fmt.Errorf("Could not find any writable data directories. Make sure XDG_DATA_DIRS is set and contains at least one directory for which you have write permission") + } + portals_dir := "" + for _, x := range WritableDataDirs() { + q := filepath.Join(x, "xdg-desktop-portal", "portals") + if unix.Access(q, unix.W_OK) == nil && IsDir(q) { + portals_dir = q + break + } + } + if portals_dir == "" { + for _, x := range WritableDataDirs() { + q := filepath.Join(x, "xdg-desktop-portal", "portals") + if err := os.MkdirAll(q, 0o755); err == nil { + portals_dir = q + break + } + } + } + if portals_dir == "" { + return fmt.Errorf("Could not find any writable portals directories. Make sure XDG_DATA_HOME is set and point to a directory for which you have write permission.") + } + portals_defn := filepath.Join(portals_dir, "kitty.portal") + if err = os.WriteFile(portals_defn, utils.UnsafeStringToBytes(fmt.Sprintf( + `[portal] +DBusName=%s +Interfaces=%s; +`, PORTAL_BUS_NAME, strings.Join(AllPortalInterfaces(), ";"))), 0o644); err != nil { + return err + } + fmt.Println("Wrote kitty portal definition to:", portals_defn) + dbus_service_dir := "" + for _, x := range WritableDataDirs() { + q := filepath.Join(x, "dbus-1", "services") + if err := os.MkdirAll(q, 0o755); err == nil { + dbus_service_dir = q + break + } + } + if dbus_service_dir == "" { + return fmt.Errorf("Could not find any writable portals directories. Make sure XDG_DATA_HOME is set and point to a directory for which you have write permission.") + } + dbus_service_defn := filepath.Join(dbus_service_dir, PORTAL_BUS_NAME+".desktop") + if err = os.WriteFile(dbus_service_defn, utils.UnsafeStringToBytes(fmt.Sprintf( + `[D-BUS Service] +Name=%s +Exec=kitten run-server +`, PORTAL_BUS_NAME)), 0o644); err != nil { + return err + } + fmt.Println("Wrote kitty DBUS activation service file to:", dbus_service_defn) + + d := os.Getenv("XDG_CURRENT_DESKTOP") + cf := os.Getenv("XDG_CONFIG_HOME") + if cf == "" { + cf = utils.Expanduser("~/.config") + } + cf = filepath.Join(cf, "xdg-desktop-portal") + if err = os.MkdirAll(cf, 0o755); err != nil { + return fmt.Errorf("failed to create %s to store the portals.conf file with error: %w", cf, err) + } + patched_file := "" + desktops := utils.Filter(strings.Split(d, ":"), func(x string) bool { return x != "" }) + desktops = append(desktops, "") + for _, x := range strings.Split(d, ":") { + q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x)))) + if text, err := os.ReadFile(q); err == nil { + text := patch_portals_conf(text) + if err = os.WriteFile(q, text, 0o644); err == nil { + patched_file = q + break + } + } + } + if patched_file == "" { + x := desktops[0] + q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x)))) + text := patch_portals_conf([]byte{}) + if err = os.WriteFile(q, text, 0o644); err != nil { + return err + } + patched_file = q + } + fmt.Printf("Patched %s to use the kitty portals\n", patched_file) + return +} + func (self *Portal) ChangeSetting(namespace, key string, value dbus.Variant) *dbus.Error { self.lock.Lock() defer self.lock.Unlock()