Skip to content

Commit

Permalink
Add support for Konsole
Browse files Browse the repository at this point in the history
This patch extends the themer to support KDE's Konsole as the adapter.
The implementation utilizes multiple Konsole profiles, each of which is
set with the desired theme, and the DBus integration to iterate over all
active Konsole sessions to set it.

When used in KDE environment, it's important to let the "no preference"
value fallback to "light". That's because [the daemon][1] handling theme
synchronization between KDE and GTK toolkits uses a different heuristics
to translate the KDE theme to its GTK counterpart. In such case, themer
receives two signals, and the table below represents the emitted values:

preference | xdg-desktop-portal-kde | xdg-desktop-portal-gtk
------------------------------------------------------------
none       | no-preference          | no-preference
dark       | prefers-dark           | prefers-dark
light      | prefers-light          | no-preference

During development, the signal emitted from the GTK's has been coming
after the one generated by KDE, essentially overriding it.

[1]: https://invent.kde.org/plasma/kde-gtk-config/-/blob/master/kded/gtkconfig.cpp?ref_type=heads#L209
  • Loading branch information
d1823 committed Jan 13, 2024
1 parent 2b11c05 commit 2e34129
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 0 deletions.
5 changes: 5 additions & 0 deletions cmd/themer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ func main() {
if err != nil {
log.Printf("Executing the AlacrittyAdapter: %v", err)
}
case config.KonsoleAdapter:
err := adapter.ExecuteKonsoleAdapter(colorSchemePreference, a.(config.KonsoleAdapter))
if err != nil {
log.Printf("Executing the KonsoleAdapter: %v", err)
}
default:
log.Fatalf("Unknown adapter: %T", a)
}
Expand Down
72 changes: 72 additions & 0 deletions internal/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package adapter

import (
"bytes"
"encoding/xml"
"fmt"
"github.com/d1823/themer/internal/config"
"github.com/d1823/themer/internal/freedesktop"
"github.com/godbus/dbus/v5"
"log"
"os"
"os/exec"
"strings"
"time"
)

Expand Down Expand Up @@ -63,6 +67,74 @@ func ExecuteTmuxAdapter(preference freedesktop.ColorSchemePreference, config con
return nil
}

type Node struct {
XMLName xml.Name `xml:"node"`
Name string `xml:"name,attr"`
Nodes []Node `xml:"node"`
}

func ExecuteKonsoleAdapter(preference freedesktop.ColorSchemePreference, config config.KonsoleAdapter) error {
// NOTE: The connection returned by the dbus.SessionBus is shared.
// Closing it here would mean the main package would no longer receive the signals it's waiting for.
conn, err := dbus.SessionBus()
if err != nil {
log.Fatalf("Failed to connect to the D-Bus session bus: %v", err)
}

var listedServices []string
err = conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&listedServices)
if err != nil {
log.Fatalf("Failed to list service names: %v", err)
}

var profileName string
switch preference {
case freedesktop.NoPreference:
profileName = config.NoPreferenceProfileName
case freedesktop.PreferDarkAppearance:
profileName = config.DarkProfileName
case freedesktop.PreferLightAppearance:
profileName = config.LightProfileName
default:
return fmt.Errorf("invalid preference: %v", preference)
}

var introspection string
for _, service := range listedServices {
if !strings.HasPrefix(service, "org.kde.konsole-") {
continue
}

err = conn.Object(service, "/Sessions").
Call("org.freedesktop.DBus.Introspectable.Introspect", 0).
Store(&introspection)
if err != nil {
log.Fatalf("Failed to introspect the service: %v", err)
}

var root Node
err = xml.Unmarshal([]byte(introspection), &root)
if err != nil {
log.Fatalf("Failed to parse the introspection: %v", err)
}

sessions := make(map[string]struct{})
for _, node := range root.Nodes {
sessions[fmt.Sprintf("/Sessions/%s", node.Name)] = struct{}{}
}

for session, _ := range sessions {
call := conn.Object(service, dbus.ObjectPath(session)).
Call("org.kde.konsole.Session.setProfile", 0, profileName)
if call.Err != nil {
log.Fatalf("Failed to set profile: %v", call.Err)
}
}
}

return nil
}

func runCmd(args []string) (string, string, error) {
tmux, err := exec.LookPath("tmux")
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ type AlacrittyAdapter struct {
AlacrittyConfigFile string `mapstructure:"alacritty_config_file"`
}

// KonsoleAdapter represents the Konsole adapter configuration.
type KonsoleAdapter struct {
SymlinkAdapter `mapstructure:",squash"`
NoPreferenceProfileName string `mapstructure:"no_preference_profile_name"`
DarkProfileName string `mapstructure:"dark_profile_name"`
LightProfileName string `mapstructure:"light_profile_name"`
}

// Configuration represents the top-level configuration structure.
type Configuration struct {
Adapters []interface{} `json:"adapters"`
Expand Down Expand Up @@ -68,6 +76,12 @@ func (c *Configuration) UnmarshalJSON(data []byte) error {
return fmt.Errorf("failed to decode 'alacritty' adapter: %v", err)
}
c.Adapters = append(c.Adapters, alacritty)
case "konsole":
konsole := KonsoleAdapter{}
if err := mapstructure.Decode(adapter, &konsole); err != nil {
return fmt.Errorf("failed to decode 'konsole' adapter: %v", err)
}
c.Adapters = append(c.Adapters, konsole)
default:
return fmt.Errorf("unknown adapter type: %v", adapter["adapter"])
}
Expand Down

0 comments on commit 2e34129

Please sign in to comment.