diff --git a/assets/systemd/dms-tray-watcher.service b/assets/systemd/dms-tray-watcher.service new file mode 100644 index 000000000..d68164cf1 --- /dev/null +++ b/assets/systemd/dms-tray-watcher.service @@ -0,0 +1,17 @@ +[Unit] +Description=DMS Status Notifier Watcher (early tray) +# The tray watcher starts in parallel with the compositor and owns org.kde.StatusNotifierWatcher before +# graphical-session.target, i.e. before any XDG autostart app launches. +PartOf=graphical-session.target +Before=graphical-session.target + +[Service] +Type=dbus +BusName=org.kde.StatusNotifierWatcher +ExecStart=/usr/bin/dms tray-watcher +Restart=on-failure +RestartSec=1 +Slice=session.slice + +[Install] +WantedBy=graphical-session.target diff --git a/core/cmd/dms/commands_common.go b/core/cmd/dms/commands_common.go index b85918510..76649cea6 100644 --- a/core/cmd/dms/commands_common.go +++ b/core/cmd/dms/commands_common.go @@ -774,5 +774,6 @@ func getCommonCommands() []*cobra.Command { trashCmd, systemCmd, switchUserCmd, + trayWatcherCmd, } } diff --git a/core/cmd/dms/commands_traywatcher.go b/core/cmd/dms/commands_traywatcher.go new file mode 100644 index 000000000..271177232 --- /dev/null +++ b/core/cmd/dms/commands_traywatcher.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/traywatcher" + "github.com/spf13/cobra" +) + +var trayWatcherCmd = &cobra.Command{ + Use: "tray-watcher", + Short: "Run a minimal StatusNotifierWatcher for early tray registration", + Long: `Run a minimal org.kde.StatusNotifierWatcher daemon. + +Started early in the session (Before=graphical-session.target via +dms-tray-watcher.service), it lets tray apps launched by XDG autostart +register their items before the shell finishes loading, and keeps them +registered across shell restarts. The shell's tray host picks items up from +this watcher; if it is not running, the shell's built-in watcher takes over.`, + Run: func(cmd *cobra.Command, args []string) { + if err := traywatcher.Run(); err != nil { + log.Fatalf("%v", err) + } + }, +} diff --git a/core/internal/config/deployer.go b/core/internal/config/deployer.go index 82c1bcbf9..31b355660 100644 --- a/core/internal/config/deployer.go +++ b/core/internal/config/deployer.go @@ -880,8 +880,9 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars) - spawnDms := `spawn-at-startup "dms" "run"` - if !strings.Contains(config, spawnDms) { + // Watcher spawns first so it owns the SNI name before dms/tray apps start. + spawnDms := "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\"" + if !strings.Contains(config, `spawn-at-startup "dms" "run"`) { // Insert spawn-at-startup for dms after the environment block envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`) if loc := envBlockEnd.FindStringIndex(config); loc != nil { diff --git a/core/internal/config/deployer_test.go b/core/internal/config/deployer_test.go index ac3f5933a..ebf7a7e19 100644 --- a/core/internal/config/deployer_test.go +++ b/core/internal/config/deployer_test.go @@ -520,9 +520,21 @@ func TestHyprlandConfigStructure(t *testing.T) { assert.Contains(t, HyprlandLuaConfig, "input =") } +// In non-systemd mode dms is launched from the compositor config, so the tray +// watcher must be launched there too, before dms, to own the SNI name first. +func TestNonSystemdLaunchesTrayWatcherBeforeShell(t *testing.T) { + hypr := transformHyprlandLuaForNonSystemd(HyprlandLuaConfig, "ghostty") + assert.Contains(t, hypr, "hl.exec_cmd(\"dms tray-watcher\")\n\thl.exec_cmd(\"dms run\")") + + niri := (&ConfigDeployer{}).transformNiriConfigForNonSystemd(NiriConfig, "ghostty") + assert.Contains(t, niri, "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\"") +} + func TestMangoConfigStructure(t *testing.T) { assert.Contains(t, MangoConfig, "exec-once=dms run") assert.NotContains(t, MangoConfig, "exec_once=dms run") + // Tray watcher must start before the shell so it owns the SNI name first. + assert.Contains(t, MangoConfig, "exec-once=dms tray-watcher\nexec-once=dms run") assert.Contains(t, MangoConfig, "source=./dms/binds.conf") assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left") assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down") diff --git a/core/internal/config/embedded/mango.conf b/core/internal/config/embedded/mango.conf index 3cec23220..abef9fb6c 100644 --- a/core/internal/config/embedded/mango.conf +++ b/core/internal/config/embedded/mango.conf @@ -7,7 +7,9 @@ env=XDG_SESSION_TYPE,wayland # exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs # every exec= on each config reload, and DMS reloads the config, which would -# spawn a new shell on every reload. +# spawn a new shell on every reload. The tray watcher starts first so it owns +# the SNI name before dms and the tray apps come up. +exec-once=dms tray-watcher exec-once=dms run source=./dms/colors.conf diff --git a/core/internal/config/hyprland_lua.go b/core/internal/config/hyprland_lua.go index 2af667617..b70b203ab 100644 --- a/core/internal/config/hyprland_lua.go +++ b/core/internal/config/hyprland_lua.go @@ -116,6 +116,7 @@ func transformHyprlandLuaForNonSystemd(config, terminalCommand string) string { `hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" + fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" + `hl.on("hyprland.start", function()` + "\n" + + ` hl.exec_cmd("dms tray-watcher")` + "\n" + ` hl.exec_cmd("dms run")` + "\n" + `end)` + "\n" + hyprlandStartupEnd diff --git a/core/internal/distros/base.go b/core/internal/distros/base.go index 194ad8bbc..bbb3e22a0 100644 --- a/core/internal/distros/base.go +++ b/core/internal/distros/base.go @@ -587,13 +587,15 @@ TERMINAL=%s } func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error { + // Pull in the tray watcher alongside dms; its Before=graphical-session.target + // ordering makes it claim the SNI name before autostart apps start. switch wm { case deps.WindowManagerNiri: - if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil { + if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms", "dms-tray-watcher").Run(); err != nil { b.log("Warning: failed to add dms as a want for niri.service") } case deps.WindowManagerHyprland: - if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil { + if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms", "dms-tray-watcher").Run(); err != nil { b.log("Warning: failed to add dms as a want for hyprland-session.target") } } diff --git a/core/internal/traywatcher/traywatcher.go b/core/internal/traywatcher/traywatcher.go new file mode 100644 index 000000000..9811014bf --- /dev/null +++ b/core/internal/traywatcher/traywatcher.go @@ -0,0 +1,205 @@ +// Package traywatcher implements a minimal org.kde.StatusNotifierWatcher that +// claims the SNI name early in the session so autostart tray apps can register +// before the shell finishes loading. See docs/TRAY_WATCHER.md. +package traywatcher + +import ( + "fmt" + "sort" + "strings" + "sync" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" +) + +const ( + busName = "org.kde.StatusNotifierWatcher" + objPath = dbus.ObjectPath("/StatusNotifierWatcher") + iface = "org.kde.StatusNotifierWatcher" +) + +const introXML = ` + + + + + + + + + + + + + + + + + + + +` + introspect.IntrospectDataString + prop.IntrospectDataString + `` + +type watcher struct { + conn *dbus.Conn + props *prop.Properties + mu sync.Mutex + items map[string]string // item id ("name/path") -> bus name to watch + hosts map[string]bool +} + +func (w *watcher) itemListLocked() []string { + list := make([]string, 0, len(w.items)) + for item := range w.items { + list = append(list, item) + } + sort.Strings(list) + return list +} + +func (w *watcher) emit(signal string, args ...any) { + if err := w.conn.Emit(objPath, iface+"."+signal, args...); err != nil { + log.Warnf("failed to emit %s: %v", signal, err) + } +} + +// Clients pass either an object path (item on the caller) or a bus name. +func (w *watcher) RegisterStatusNotifierItem(sender dbus.Sender, service string) *dbus.Error { + var item, watch string + if strings.HasPrefix(service, "/") { + item, watch = string(sender)+service, string(sender) + } else { + item, watch = service+"/StatusNotifierItem", service + } + + w.mu.Lock() + if _, exists := w.items[item]; exists { + w.mu.Unlock() + return nil + } + w.items[item] = watch + list := w.itemListLocked() + w.mu.Unlock() + + log.Infof("item registered: %s", item) + w.emit("StatusNotifierItemRegistered", item) + w.props.SetMust(iface, "RegisteredStatusNotifierItems", list) + return nil +} + +func (w *watcher) RegisterStatusNotifierHost(sender dbus.Sender, service string) *dbus.Error { + watch := string(sender) + if service != "" && !strings.HasPrefix(service, "/") { + watch = service + } + + w.mu.Lock() + if w.hosts[watch] { + w.mu.Unlock() + return nil + } + w.hosts[watch] = true + w.mu.Unlock() + + log.Infof("host registered: %s", watch) + w.emit("StatusNotifierHostRegistered") + return nil +} + +func (w *watcher) pruneOwner(name string) { + w.mu.Lock() + var removed []string + for item, watch := range w.items { + if watch == name { + delete(w.items, item) + removed = append(removed, item) + } + } + list := w.itemListLocked() + hostGone := w.hosts[name] + delete(w.hosts, name) + w.mu.Unlock() + + for _, item := range removed { + log.Infof("item gone: %s", item) + w.emit("StatusNotifierItemUnregistered", item) + } + if len(removed) > 0 { + w.props.SetMust(iface, "RegisteredStatusNotifierItems", list) + } + if hostGone { + log.Infof("host gone: %s", name) + w.emit("StatusNotifierHostUnregistered") + } +} + +// Run serves the watcher until the session bus connection closes. +func Run() error { + conn, err := dbus.ConnectSessionBus() + if err != nil { + return fmt.Errorf("connect to session bus: %w", err) + } + defer conn.Close() + + w := &watcher{conn: conn, items: map[string]string{}, hosts: map[string]bool{}} + + if err := conn.Export(w, objPath, iface); err != nil { + return fmt.Errorf("export watcher: %w", err) + } + w.props, err = prop.Export(conn, objPath, map[string]map[string]*prop.Prop{ + iface: { + "RegisteredStatusNotifierItems": {Value: []string{}, Emit: prop.EmitTrue}, + // Always true: libappindicator clients drop the icon if it is false. + "IsStatusNotifierHostRegistered": {Value: true, Emit: prop.EmitFalse}, + "ProtocolVersion": {Value: int32(0), Emit: prop.EmitFalse}, + }, + }) + if err != nil { + return fmt.Errorf("export properties: %w", err) + } + if err := conn.Export(introspect.Introspectable(introXML), objPath, "org.freedesktop.DBus.Introspectable"); err != nil { + return fmt.Errorf("export introspection: %w", err) + } + + if err := conn.AddMatchSignal( + dbus.WithMatchSender("org.freedesktop.DBus"), + dbus.WithMatchInterface("org.freedesktop.DBus"), + dbus.WithMatchMember("NameOwnerChanged"), + dbus.WithMatchObjectPath("/org/freedesktop/DBus"), + ); err != nil { + return fmt.Errorf("match NameOwnerChanged: %w", err) + } + sigs := make(chan *dbus.Signal, 128) + conn.Signal(sigs) + + // No flags: queue for the name and take it when the current owner exits. + reply, err := conn.RequestName(busName, 0) + if err != nil { + return fmt.Errorf("request %s: %w", busName, err) + } + if reply == dbus.RequestNameReplyInQueue { + log.Infof("name busy, queued for %s", busName) + } + + for sig := range sigs { + switch sig.Name { + case "org.freedesktop.DBus.NameAcquired": + if len(sig.Body) == 1 && sig.Body[0] == busName { + log.Infof("acquired %s", busName) + } + case "org.freedesktop.DBus.NameOwnerChanged": + if len(sig.Body) != 3 { + continue + } + name, _ := sig.Body[0].(string) + newOwner, _ := sig.Body[2].(string) + if name != busName && newOwner == "" { + w.pruneOwner(name) + } + } + } + return fmt.Errorf("session bus connection closed") +} diff --git a/distro/debian/dms-git/debian/rules b/distro/debian/dms-git/debian/rules index 0ca79f87b..b2b1d103b 100755 --- a/distro/debian/dms-git/debian/rules +++ b/distro/debian/dms-git/debian/rules @@ -72,6 +72,7 @@ override_dh_auto_install: if [ -d quickshell ]; then \ cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \ install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \ + install -Dm644 assets/systemd/dms-tray-watcher.service debian/dms-git/usr/lib/systemd/user/dms-tray-watcher.service; \ install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \ install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \ install -Dm644 assets/com.danklinux.dms.notepad.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop; \ diff --git a/distro/debian/dms/debian/rules b/distro/debian/dms/debian/rules index ad0abdc48..df00c952a 100755 --- a/distro/debian/dms/debian/rules +++ b/distro/debian/dms/debian/rules @@ -65,6 +65,7 @@ override_dh_auto_install: if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \ cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \ + install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms-tray-watcher.service debian/dms/usr/lib/systemd/user/dms-tray-watcher.service; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.notepad.desktop debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop; \ diff --git a/distro/fedora/dms-git.spec b/distro/fedora/dms-git.spec index 841747303..fcf316a65 100644 --- a/distro/fedora/dms-git.spec +++ b/distro/fedora/dms-git.spec @@ -119,6 +119,7 @@ core/bin/${DMS_BINARY} completion fish > %{buildroot}%{_datadir}/fish/vendor_com # Install systemd user service install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service +install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop @@ -145,6 +146,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || : %doc quickshell/README.md %{_datadir}/quickshell/dms/ %{_userunitdir}/dms.service +%{_userunitdir}/dms-tray-watcher.service %{_datadir}/applications/dms-open.desktop %{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.notepad.desktop diff --git a/distro/fedora/dms.spec b/distro/fedora/dms.spec index 807065894..608cae7c3 100644 --- a/distro/fedora/dms.spec +++ b/distro/fedora/dms.spec @@ -87,6 +87,7 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d %{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service +install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop @@ -112,6 +113,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || : %doc README.md CONTRIBUTING.md %{_datadir}/quickshell/dms/ %{_userunitdir}/dms.service +%{_userunitdir}/dms-tray-watcher.service %{_datadir}/applications/dms-open.desktop %{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.notepad.desktop diff --git a/distro/nix/home.nix b/distro/nix/home.nix index f7715264b..ea4c4dcd7 100644 --- a/distro/nix/home.nix +++ b/distro/nix/home.nix @@ -101,6 +101,24 @@ in Install.WantedBy = [ cfg.systemd.target ]; }; + # Early SNI watcher so autostart tray apps register before the shell loads. + systemd.user.services.dms-tray-watcher = lib.mkIf cfg.systemd.enable { + Unit = { + Description = "DMS Status Notifier Watcher (early tray)"; + PartOf = [ cfg.systemd.target ]; + Before = [ cfg.systemd.target ]; + }; + + Service = { + Type = "dbus"; + BusName = "org.kde.StatusNotifierWatcher"; + ExecStart = lib.getExe cfg.package + " tray-watcher"; + Restart = "on-failure"; + }; + + Install.WantedBy = [ cfg.systemd.target ]; + }; + xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) { source = jsonFormat.generate "session.json" cfg.session; }; diff --git a/distro/nix/nixos.nix b/distro/nix/nixos.nix index 682ad8b11..e28bdf071 100644 --- a/distro/nix/nixos.nix +++ b/distro/nix/nixos.nix @@ -39,6 +39,24 @@ in }; }; + # Early SNI watcher so autostart tray apps register before the shell loads. + systemd.user.services.dms-tray-watcher = lib.mkIf cfg.systemd.enable { + description = "DMS Status Notifier Watcher (early tray)"; + path = lib.mkForce [ ]; + + partOf = [ cfg.systemd.target ]; + before = [ cfg.systemd.target ]; + wantedBy = [ cfg.systemd.target ]; + restartIfChanged = cfg.systemd.restartIfChanged; + + serviceConfig = { + Type = "dbus"; + BusName = "org.kde.StatusNotifierWatcher"; + ExecStart = lib.getExe cfg.package + " tray-watcher"; + Restart = "on-failure"; + }; + }; + environment.systemPackages = [ cfg.quickshell.package ] ++ common.packages; environment.etc = lib.mapAttrs' (name: value: { diff --git a/distro/opensuse/dms-git.spec b/distro/opensuse/dms-git.spec index fd5766800..ee08bc3ce 100644 --- a/distro/opensuse/dms-git.spec +++ b/distro/opensuse/dms-git.spec @@ -114,6 +114,7 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d ./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service +install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop @@ -152,6 +153,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || : %dir %{_datadir}/quickshell %{_datadir}/quickshell/dms/ %{_userunitdir}/dms.service +%{_userunitdir}/dms-tray-watcher.service %{_datadir}/applications/dms-open.desktop %{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.notepad.desktop diff --git a/distro/opensuse/dms.spec b/distro/opensuse/dms.spec index 5be054cb1..fb54b8ae6 100644 --- a/distro/opensuse/dms.spec +++ b/distro/opensuse/dms.spec @@ -63,6 +63,7 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d ./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service +install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop @@ -99,6 +100,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || : %dir %{_datadir}/quickshell %{_datadir}/quickshell/dms/ %{_userunitdir}/dms.service +%{_userunitdir}/dms-tray-watcher.service %{_datadir}/applications/dms-open.desktop %{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.notepad.desktop diff --git a/distro/ubuntu/dms-git/debian/rules b/distro/ubuntu/dms-git/debian/rules index 92e19c8df..316a2650c 100755 --- a/distro/ubuntu/dms-git/debian/rules +++ b/distro/ubuntu/dms-git/debian/rules @@ -69,6 +69,8 @@ override_dh_auto_install: # Install systemd user service install -Dm644 dms-git-repo/assets/systemd/dms.service \ debian/dms-git/usr/lib/systemd/user/dms.service + install -Dm644 dms-git-repo/assets/systemd/dms-tray-watcher.service \ + debian/dms-git/usr/lib/systemd/user/dms-tray-watcher.service # Install desktop file and icon install -Dm644 dms-git-repo/assets/dms-open.desktop \ diff --git a/distro/ubuntu/dms/debian/rules b/distro/ubuntu/dms/debian/rules index a01280acc..8a7ffec2b 100755 --- a/distro/ubuntu/dms/debian/rules +++ b/distro/ubuntu/dms/debian/rules @@ -50,6 +50,8 @@ override_dh_auto_install: # Install systemd user service (before copying everything else) install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms.service \ debian/dms/usr/lib/systemd/user/dms.service + install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms-tray-watcher.service \ + debian/dms/usr/lib/systemd/user/dms-tray-watcher.service # Install desktop file and icon install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/dms-open.desktop \