diff --git a/go.mod b/go.mod index 5cbbf20..07c5d96 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module luxtools-client go 1.22 + +require github.com/godbus/dbus/v5 v5.2.2 + +require golang.org/x/sys v0.27.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5334971 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go index d16829e..e5777bc 100644 --- a/main.go +++ b/main.go @@ -10,12 +10,15 @@ import ( "log" "net" "net/http" + "net/url" "os" "os/exec" "path/filepath" "runtime" "strings" "time" + + "github.com/godbus/dbus/v5" ) type allowList []string @@ -267,6 +270,17 @@ func openFolder(path string) error { cmd := exec.Command("explorer.exe", p) return cmd.Start() case "linux": + if isKDESession() { + if err := openFolderKDEDBus(path); err == nil { + return nil + } + // Fallback: launching dolphin directly typically forwards to an existing + // instance (opening a tab) and avoids portal/session-restore oddities. + if err := exec.Command("dolphin", path).Start(); err == nil { + return nil + } + } + cmd := exec.Command("xdg-open", path) return cmd.Start() default: @@ -274,6 +288,56 @@ func openFolder(path string) error { } } +func isKDESession() bool { + // Plasma sets XDG_CURRENT_DESKTOP=KDE. Some setups provide multiple entries. + cd := strings.ToLower(strings.TrimSpace(os.Getenv("XDG_CURRENT_DESKTOP"))) + return strings.Contains(cd, "kde") +} + +// openFolderKDEDBus asks the running file manager (Dolphin on Plasma) to show +// the folder via D-Bus. This tends to reuse an existing Dolphin window and open +// a new tab instead of spawning a new window and restoring unrelated session +// tabs. +// +// Note: On Plasma Wayland, reliably forcing the window to the foreground is +// gated by XDG activation tokens; we do a best-effort activation call but it may +// be ignored by the compositor. +func openFolderKDEDBus(path string) error { + conn, err := dbus.SessionBus() + if err != nil { + return err + } + defer conn.Close() + + uri := (&url.URL{Scheme: "file", Path: filepath.ToSlash(path)}).String() + obj := conn.Object("org.freedesktop.FileManager1", dbus.ObjectPath("/org/freedesktop/FileManager1")) + call := obj.Call("org.freedesktop.FileManager1.ShowFolders", 0, []string{uri}, "") + if call.Err != nil { + return call.Err + } + + // Best-effort activation of an existing Dolphin window. + if dolphinSvc, _ := findFirstDBusName(conn, "org.kde.dolphin-"); dolphinSvc != "" { + app := conn.Object(dolphinSvc, dbus.ObjectPath("/org/kde/dolphin")) + _ = app.Call("org.freedesktop.Application.Activate", 0, map[string]dbus.Variant{}).Err + } + + return nil +} + +func findFirstDBusName(conn *dbus.Conn, prefix string) (string, error) { + var names []string + if err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names); err != nil { + return "", err + } + for _, n := range names { + if strings.HasPrefix(n, prefix) { + return n, nil + } + } + return "", nil +} + func writeJSON(w http.ResponseWriter, status int, resp openResponse) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status)