summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions.go112
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions_test.go145
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/constants.go23
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers.go154
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers_test.go581
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager.go206
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager_test.go143
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver.go349
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver_test.go102
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types.go66
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types_test.go70
11 files changed, 1951 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions.go
new file mode 100644
index 0000000..52515d1
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions.go
@@ -0,0 +1,112 @@
+package freedesktop
+
+import (
+ "fmt"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
+ "github.com/godbus/dbus/v5"
+)
+
+func (m *Manager) SetIconFile(iconPath string) error {
+ if !m.state.Accounts.Available || m.accountsObj == nil {
+ return fmt.Errorf("accounts service not available")
+ }
+
+ err := m.accountsObj.Call(dbusAccountsUserInterface+".SetIconFile", 0, iconPath).Err
+ if err != nil {
+ return fmt.Errorf("failed to set icon file: %w", err)
+ }
+
+ m.updateAccountsState()
+ return nil
+}
+
+func (m *Manager) SetRealName(name string) error {
+ if !m.state.Accounts.Available || m.accountsObj == nil {
+ return fmt.Errorf("accounts service not available")
+ }
+
+ err := m.accountsObj.Call(dbusAccountsUserInterface+".SetRealName", 0, name).Err
+ if err != nil {
+ return fmt.Errorf("failed to set real name: %w", err)
+ }
+
+ m.updateAccountsState()
+ return nil
+}
+
+func (m *Manager) SetEmail(email string) error {
+ if !m.state.Accounts.Available || m.accountsObj == nil {
+ return fmt.Errorf("accounts service not available")
+ }
+
+ err := m.accountsObj.Call(dbusAccountsUserInterface+".SetEmail", 0, email).Err
+ if err != nil {
+ return fmt.Errorf("failed to set email: %w", err)
+ }
+
+ m.updateAccountsState()
+ return nil
+}
+
+func (m *Manager) SetLanguage(language string) error {
+ if !m.state.Accounts.Available || m.accountsObj == nil {
+ return fmt.Errorf("accounts service not available")
+ }
+
+ err := m.accountsObj.Call(dbusAccountsUserInterface+".SetLanguage", 0, language).Err
+ if err != nil {
+ return fmt.Errorf("failed to set language: %w", err)
+ }
+
+ m.updateAccountsState()
+ return nil
+}
+
+func (m *Manager) SetLocation(location string) error {
+ if !m.state.Accounts.Available || m.accountsObj == nil {
+ return fmt.Errorf("accounts service not available")
+ }
+
+ err := m.accountsObj.Call(dbusAccountsUserInterface+".SetLocation", 0, location).Err
+ if err != nil {
+ return fmt.Errorf("failed to set location: %w", err)
+ }
+
+ m.updateAccountsState()
+ return nil
+}
+
+func (m *Manager) GetUserIconFile(username string) (string, error) {
+ if m.systemConn == nil {
+ return "", fmt.Errorf("accounts service not available")
+ }
+
+ accountsManager := m.systemConn.Object(dbusAccountsDest, dbus.ObjectPath(dbusAccountsPath))
+
+ var userPath dbus.ObjectPath
+ err := accountsManager.Call(dbusAccountsInterface+".FindUserByName", 0, username).Store(&userPath)
+ if err != nil {
+ return "", fmt.Errorf("user not found: %w", err)
+ }
+
+ userObj := m.systemConn.Object(dbusAccountsDest, userPath)
+ variant, err := userObj.GetProperty(dbusAccountsUserInterface + ".IconFile")
+ if err != nil {
+ return "", err
+ }
+
+ var iconFile string
+ if err := variant.Store(&iconFile); err != nil {
+ return "", err
+ }
+
+ return iconFile, nil
+}
+
+func (m *Manager) SetIconTheme(iconTheme string) error {
+ if err := utils.GsettingsSet("org.gnome.desktop.interface", "icon-theme", iconTheme); err != nil {
+ return fmt.Errorf("failed to set icon theme: %w", err)
+ }
+ return nil
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions_test.go
new file mode 100644
index 0000000..e2c21e8
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/actions_test.go
@@ -0,0 +1,145 @@
+package freedesktop
+
+import (
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestManager_SetIconFile(t *testing.T) {
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ err := manager.SetIconFile("/path/to/icon.png")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "accounts service not available")
+ })
+}
+
+func TestManager_SetRealName(t *testing.T) {
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ err := manager.SetRealName("New Name")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "accounts service not available")
+ })
+}
+
+func TestManager_SetEmail(t *testing.T) {
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ err := manager.SetEmail("test@example.com")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "accounts service not available")
+ })
+}
+
+func TestManager_SetLanguage(t *testing.T) {
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ err := manager.SetLanguage("en_US.UTF-8")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "accounts service not available")
+ })
+}
+
+func TestManager_SetLocation(t *testing.T) {
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ err := manager.SetLocation("Test Location")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "accounts service not available")
+ })
+}
+
+func TestManager_GetUserIconFile(t *testing.T) {
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ iconFile, err := manager.GetUserIconFile("testuser")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "accounts service not available")
+ assert.Empty(t, iconFile)
+ })
+}
+
+func TestManager_UpdateAccountsState(t *testing.T) {
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ err := manager.updateAccountsState()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "accounts service not available")
+ })
+}
+
+func TestManager_UpdateSettingsState(t *testing.T) {
+ t.Run("settings not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Settings: SettingsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ err := manager.updateSettingsState()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "settings portal not available")
+ })
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/constants.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/constants.go
new file mode 100644
index 0000000..12db87b
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/constants.go
@@ -0,0 +1,23 @@
+package freedesktop
+
+const (
+ dbusAccountsDest = "org.freedesktop.Accounts"
+ dbusAccountsPath = "/org/freedesktop/Accounts"
+ dbusAccountsInterface = "org.freedesktop.Accounts"
+ dbusAccountsUserInterface = "org.freedesktop.Accounts.User"
+
+ dbusPortalDest = "org.freedesktop.portal.Desktop"
+ dbusPortalPath = "/org/freedesktop/portal/desktop"
+ dbusPortalSettingsInterface = "org.freedesktop.portal.Settings"
+
+ dbusPropsInterface = "org.freedesktop.DBus.Properties"
+
+ dbusScreensaverName = "org.freedesktop.ScreenSaver"
+ dbusScreensaverPath = "/ScreenSaver"
+ dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
+ dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
+
+ dbusGnomeScreensaverName = "org.gnome.ScreenSaver"
+ dbusGnomeScreensaverPath = "/org/gnome/ScreenSaver"
+ dbusGnomeScreensaverInterface = "org.gnome.ScreenSaver"
+)
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers.go
new file mode 100644
index 0000000..c3090fa
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers.go
@@ -0,0 +1,154 @@
+package freedesktop
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
+)
+
+func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
+ switch req.Method {
+ case "freedesktop.getState":
+ handleGetState(conn, req, manager)
+ case "freedesktop.accounts.setIconFile":
+ handleSetIconFile(conn, req, manager)
+ case "freedesktop.accounts.setRealName":
+ handleSetRealName(conn, req, manager)
+ case "freedesktop.accounts.setEmail":
+ handleSetEmail(conn, req, manager)
+ case "freedesktop.accounts.setLanguage":
+ handleSetLanguage(conn, req, manager)
+ case "freedesktop.accounts.setLocation":
+ handleSetLocation(conn, req, manager)
+ case "freedesktop.accounts.getUserIconFile":
+ handleGetUserIconFile(conn, req, manager)
+ case "freedesktop.settings.getColorScheme":
+ handleGetColorScheme(conn, req, manager)
+ case "freedesktop.settings.setIconTheme":
+ handleSetIconTheme(conn, req, manager)
+ default:
+ models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
+ }
+}
+
+func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
+ models.Respond(conn, req.ID, manager.GetState())
+}
+
+func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
+ iconPath, err := params.String(req.Params, "path")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ if err := manager.SetIconFile(iconPath); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon file set"})
+}
+
+func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
+ name, err := params.String(req.Params, "name")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ if err := manager.SetRealName(name); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "real name set"})
+}
+
+func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
+ email, err := params.String(req.Params, "email")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ if err := manager.SetEmail(email); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "email set"})
+}
+
+func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
+ language, err := params.String(req.Params, "language")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ if err := manager.SetLanguage(language); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "language set"})
+}
+
+func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
+ location, err := params.String(req.Params, "location")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ if err := manager.SetLocation(location); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
+}
+
+func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager) {
+ username, err := params.String(req.Params, "username")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ iconFile, err := manager.GetUserIconFile(username)
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Value: iconFile})
+}
+
+func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
+ if err := manager.updateSettingsState(); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ state := manager.GetState()
+ models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme})
+}
+
+func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) {
+ iconTheme, err := params.String(req.Params, "iconTheme")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ if err := manager.SetIconTheme(iconTheme); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon theme set"})
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers_test.go
new file mode 100644
index 0000000..f2cb5a2
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/handlers_test.go
@@ -0,0 +1,581 @@
+package freedesktop
+
+import (
+ "bytes"
+ "encoding/json"
+ "net"
+ "sync"
+ "testing"
+
+ mockdbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
+ "github.com/godbus/dbus/v5"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+)
+
+type mockNetConn struct {
+ net.Conn
+ readBuf *bytes.Buffer
+ writeBuf *bytes.Buffer
+ closed bool
+}
+
+func newMockNetConn() *mockNetConn {
+ return &mockNetConn{
+ readBuf: &bytes.Buffer{},
+ writeBuf: &bytes.Buffer{},
+ }
+}
+
+func (m *mockNetConn) Read(b []byte) (n int, err error) {
+ return m.readBuf.Read(b)
+}
+
+func (m *mockNetConn) Write(b []byte) (n int, err error) {
+ return m.writeBuf.Write(b)
+}
+
+func (m *mockNetConn) Close() error {
+ m.closed = true
+ return nil
+}
+
+func mockGetAllAccountsProperties() *dbus.Call {
+ props := map[string]dbus.Variant{
+ "IconFile": dbus.MakeVariant("/path/to/icon.png"),
+ "RealName": dbus.MakeVariant("Test"),
+ "UserName": dbus.MakeVariant("test"),
+ "AccountType": dbus.MakeVariant(int32(0)),
+ "HomeDirectory": dbus.MakeVariant("/home/test"),
+ "Shell": dbus.MakeVariant("/bin/bash"),
+ "Email": dbus.MakeVariant(""),
+ "Language": dbus.MakeVariant(""),
+ "Location": dbus.MakeVariant(""),
+ "Locked": dbus.MakeVariant(false),
+ "PasswordMode": dbus.MakeVariant(int32(1)),
+ }
+ return &dbus.Call{Err: nil, Body: []any{props}}
+}
+
+func TestRespondError_Freedesktop(t *testing.T) {
+ conn := newMockNetConn()
+ models.RespondError(conn, 123, "test error")
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Equal(t, "test error", resp.Error)
+ assert.Nil(t, resp.Result)
+}
+
+func TestRespond_Freedesktop(t *testing.T) {
+ conn := newMockNetConn()
+ result := models.SuccessResult{Success: true, Message: "test"}
+ models.Respond(conn, 123, result)
+
+ var resp models.Response[models.SuccessResult]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Empty(t, resp.Error)
+ require.NotNil(t, resp.Result)
+ assert.True(t, resp.Result.Success)
+ assert.Equal(t, "test", resp.Result.Message)
+}
+
+func TestHandleGetState(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ UserName: "testuser",
+ RealName: "Test User",
+ UID: 1000,
+ },
+ Settings: SettingsState{
+ Available: true,
+ ColorScheme: 1,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{ID: 123, Method: "freedesktop.getState"}
+
+ handleGetState(conn, req, manager)
+
+ var resp models.Response[FreedeskState]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Empty(t, resp.Error)
+ require.NotNil(t, resp.Result)
+ assert.True(t, resp.Result.Accounts.Available)
+ assert.Equal(t, "testuser", resp.Result.Accounts.UserName)
+ assert.True(t, resp.Result.Settings.Available)
+ assert.Equal(t, uint32(1), resp.Result.Settings.ColorScheme)
+}
+
+func TestHandleSetIconFile(t *testing.T) {
+ t.Run("missing path parameter", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{},
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setIconFile",
+ Params: map[string]any{},
+ }
+
+ handleSetIconFile(conn, req, manager)
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "missing or invalid 'path' parameter")
+ })
+
+ t.Run("successful set icon file", func(t *testing.T) {
+ mockAccountsObj := mockdbus.NewMockBusObject(t)
+ mockCall := &dbus.Call{Err: nil}
+ mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetIconFile", dbus.Flags(0), "/path/to/icon.png").Return(mockCall)
+ mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties())
+
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ accountsObj: mockAccountsObj,
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setIconFile",
+ Params: map[string]any{
+ "path": "/path/to/icon.png",
+ },
+ }
+
+ handleSetIconFile(conn, req, manager)
+
+ var resp models.Response[models.SuccessResult]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Empty(t, resp.Error)
+ require.NotNil(t, resp.Result)
+ assert.True(t, resp.Result.Success)
+ assert.Equal(t, "icon file set", resp.Result.Message)
+ })
+
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setIconFile",
+ Params: map[string]any{
+ "path": "/path/to/icon.png",
+ },
+ }
+
+ handleSetIconFile(conn, req, manager)
+
+ var resp models.Response[models.SuccessResult]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "accounts service not available")
+ })
+}
+
+func TestHandleSetRealName(t *testing.T) {
+ t.Run("missing name parameter", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{},
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setRealName",
+ Params: map[string]any{},
+ }
+
+ handleSetRealName(conn, req, manager)
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "missing or invalid 'name' parameter")
+ })
+
+ t.Run("successful set real name", func(t *testing.T) {
+ mockAccountsObj := mockdbus.NewMockBusObject(t)
+ mockCall := &dbus.Call{Err: nil}
+ mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetRealName", dbus.Flags(0), "New Name").Return(mockCall)
+ mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties())
+
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ accountsObj: mockAccountsObj,
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setRealName",
+ Params: map[string]any{
+ "name": "New Name",
+ },
+ }
+
+ handleSetRealName(conn, req, manager)
+
+ var resp models.Response[models.SuccessResult]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Empty(t, resp.Error)
+ require.NotNil(t, resp.Result)
+ assert.True(t, resp.Result.Success)
+ assert.Equal(t, "real name set", resp.Result.Message)
+ })
+}
+
+func TestHandleSetEmail(t *testing.T) {
+ t.Run("missing email parameter", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{},
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setEmail",
+ Params: map[string]any{},
+ }
+
+ handleSetEmail(conn, req, manager)
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "missing or invalid 'email' parameter")
+ })
+
+ t.Run("successful set email", func(t *testing.T) {
+ mockAccountsObj := mockdbus.NewMockBusObject(t)
+ mockCall := &dbus.Call{Err: nil}
+ mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetEmail", dbus.Flags(0), "test@example.com").Return(mockCall)
+ mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties())
+
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ accountsObj: mockAccountsObj,
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setEmail",
+ Params: map[string]any{
+ "email": "test@example.com",
+ },
+ }
+
+ handleSetEmail(conn, req, manager)
+
+ var resp models.Response[models.SuccessResult]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Empty(t, resp.Error)
+ require.NotNil(t, resp.Result)
+ assert.True(t, resp.Result.Success)
+ assert.Equal(t, "email set", resp.Result.Message)
+ })
+}
+
+func TestHandleSetLanguage(t *testing.T) {
+ t.Run("missing language parameter", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{},
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setLanguage",
+ Params: map[string]any{},
+ }
+
+ handleSetLanguage(conn, req, manager)
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "missing or invalid 'language' parameter")
+ })
+}
+
+func TestHandleSetLocation(t *testing.T) {
+ t.Run("missing location parameter", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{},
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.setLocation",
+ Params: map[string]any{},
+ }
+
+ handleSetLocation(conn, req, manager)
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "missing or invalid 'location' parameter")
+ })
+}
+
+func TestHandleGetUserIconFile(t *testing.T) {
+ t.Run("missing username parameter", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{},
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.getUserIconFile",
+ Params: map[string]any{},
+ }
+
+ handleGetUserIconFile(conn, req, manager)
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "missing or invalid 'username' parameter")
+ })
+
+ t.Run("accounts not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.accounts.getUserIconFile",
+ Params: map[string]any{
+ "username": "testuser",
+ },
+ }
+
+ handleGetUserIconFile(conn, req, manager)
+
+ var resp models.Response[models.SuccessResult]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "accounts service not available")
+ })
+}
+
+func TestHandleGetColorScheme(t *testing.T) {
+ t.Run("settings not available", func(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Settings: SettingsState{
+ Available: false,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
+
+ handleGetColorScheme(conn, req, manager)
+
+ var resp models.Response[map[string]uint32]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "settings portal not available")
+ })
+
+ t.Run("successful get color scheme", func(t *testing.T) {
+ mockSettingsObj := mockdbus.NewMockBusObject(t)
+ mockCall := &dbus.Call{
+ Err: nil,
+ Body: []any{dbus.MakeVariant(uint32(1))},
+ }
+ mockSettingsObj.EXPECT().Call("org.freedesktop.portal.Settings.ReadOne", dbus.Flags(0), "org.freedesktop.appearance", "color-scheme").Return(mockCall)
+
+ manager := &Manager{
+ state: &FreedeskState{
+ Settings: SettingsState{
+ Available: true,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ settingsObj: mockSettingsObj,
+ }
+
+ conn := newMockNetConn()
+ req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
+
+ handleGetColorScheme(conn, req, manager)
+
+ var resp models.Response[map[string]uint32]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Empty(t, resp.Error)
+ require.NotNil(t, resp.Result)
+ assert.Equal(t, uint32(1), (*resp.Result)["colorScheme"])
+ })
+}
+
+func TestHandleRequest(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ UserName: "testuser",
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ t.Run("unknown method", func(t *testing.T) {
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.unknown",
+ }
+
+ HandleRequest(conn, req, manager)
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Contains(t, resp.Error, "unknown method")
+ })
+
+ t.Run("valid method - getState", func(t *testing.T) {
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: "freedesktop.getState",
+ }
+
+ HandleRequest(conn, req, manager)
+
+ var resp models.Response[FreedeskState]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ assert.Empty(t, resp.Error)
+ })
+
+ t.Run("all method routes", func(t *testing.T) {
+ tests := []string{
+ "freedesktop.accounts.setIconFile",
+ "freedesktop.accounts.setRealName",
+ "freedesktop.accounts.setEmail",
+ "freedesktop.accounts.setLanguage",
+ "freedesktop.accounts.setLocation",
+ "freedesktop.accounts.getUserIconFile",
+ "freedesktop.settings.getColorScheme",
+ }
+
+ for _, method := range tests {
+ conn := newMockNetConn()
+ req := models.Request{
+ ID: 123,
+ Method: method,
+ Params: map[string]any{},
+ }
+
+ HandleRequest(conn, req, manager)
+
+ var resp models.Response[any]
+ err := json.NewDecoder(conn.writeBuf).Decode(&resp)
+ require.NoError(t, err)
+
+ assert.Equal(t, 123, resp.ID)
+ // Will have errors due to missing params or service unavailable
+ // but the method routing should work
+ }
+ })
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager.go
new file mode 100644
index 0000000..054f859
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager.go
@@ -0,0 +1,206 @@
+package freedesktop
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "sync"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
+ "github.com/godbus/dbus/v5"
+)
+
+func NewManager() (*Manager, error) {
+ systemConn, err := dbus.ConnectSystemBus()
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to system bus: %w", err)
+ }
+
+ sessionConn, err := dbus.ConnectSessionBus()
+ if err != nil {
+ sessionConn = nil
+ }
+
+ m := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{},
+ Settings: SettingsState{},
+ Screensaver: ScreensaverState{},
+ },
+ stateMutex: sync.RWMutex{},
+ systemConn: systemConn,
+ sessionConn: sessionConn,
+ currentUID: uint64(os.Getuid()),
+ }
+
+ m.initializeAccounts()
+ m.initializeSettings()
+ m.initializeScreensaver()
+
+ return m, nil
+}
+
+func (m *Manager) initializeAccounts() error {
+ accountsManager := m.systemConn.Object(dbusAccountsDest, dbus.ObjectPath(dbusAccountsPath))
+
+ var userPath dbus.ObjectPath
+ err := accountsManager.Call(dbusAccountsInterface+".FindUserById", 0, int64(m.currentUID)).Store(&userPath)
+ if err != nil {
+ m.stateMutex.Lock()
+ m.state.Accounts.Available = false
+ m.stateMutex.Unlock()
+ return err
+ }
+
+ m.accountsObj = m.systemConn.Object(dbusAccountsDest, userPath)
+
+ m.stateMutex.Lock()
+ m.state.Accounts.Available = true
+ m.state.Accounts.UserPath = string(userPath)
+ m.state.Accounts.UID = m.currentUID
+ m.stateMutex.Unlock()
+
+ if err := m.updateAccountsState(); err != nil {
+ return fmt.Errorf("failed to update accounts state: %w", err)
+ }
+
+ return nil
+}
+
+func (m *Manager) initializeSettings() error {
+ if m.sessionConn == nil {
+ m.stateMutex.Lock()
+ m.state.Settings.Available = false
+ m.stateMutex.Unlock()
+ return fmt.Errorf("no session bus connection")
+ }
+
+ m.settingsObj = m.sessionConn.Object(dbusPortalDest, dbus.ObjectPath(dbusPortalPath))
+
+ var variant dbus.Variant
+ err := m.settingsObj.Call(dbusPortalSettingsInterface+".ReadOne", 0, "org.freedesktop.appearance", "color-scheme").Store(&variant)
+ if err != nil {
+ m.stateMutex.Lock()
+ m.state.Settings.Available = false
+ m.stateMutex.Unlock()
+ return err
+ }
+
+ m.stateMutex.Lock()
+ m.state.Settings.Available = true
+ m.stateMutex.Unlock()
+
+ if err := m.updateSettingsState(); err != nil {
+ return fmt.Errorf("failed to update settings state: %w", err)
+ }
+
+ return nil
+}
+
+func (m *Manager) updateAccountsState() error {
+ if !m.state.Accounts.Available || m.accountsObj == nil {
+ return fmt.Errorf("accounts service not available")
+ }
+
+ ctx := context.Background()
+ props, err := m.getAccountProperties(ctx)
+ if err != nil {
+ return err
+ }
+
+ m.stateMutex.Lock()
+ defer m.stateMutex.Unlock()
+
+ m.state.Accounts.IconFile = dbusutil.GetOr(props, "IconFile", "")
+ m.state.Accounts.RealName = dbusutil.GetOr(props, "RealName", "")
+ m.state.Accounts.UserName = dbusutil.GetOr(props, "UserName", "")
+ m.state.Accounts.AccountType = dbusutil.GetOr(props, "AccountType", int32(0))
+ m.state.Accounts.HomeDirectory = dbusutil.GetOr(props, "HomeDirectory", "")
+ m.state.Accounts.Shell = dbusutil.GetOr(props, "Shell", "")
+ m.state.Accounts.Email = dbusutil.GetOr(props, "Email", "")
+ m.state.Accounts.Language = dbusutil.GetOr(props, "Language", "")
+ m.state.Accounts.Location = dbusutil.GetOr(props, "Location", "")
+ m.state.Accounts.Locked = dbusutil.GetOr(props, "Locked", false)
+ m.state.Accounts.PasswordMode = dbusutil.GetOr(props, "PasswordMode", int32(0))
+
+ return nil
+}
+
+func (m *Manager) updateSettingsState() error {
+ if !m.state.Settings.Available || m.settingsObj == nil {
+ return fmt.Errorf("settings portal not available")
+ }
+
+ var variant dbus.Variant
+ err := m.settingsObj.Call(dbusPortalSettingsInterface+".ReadOne", 0, "org.freedesktop.appearance", "color-scheme").Store(&variant)
+ if err != nil {
+ return err
+ }
+
+ if colorScheme, ok := dbusutil.As[uint32](variant); ok {
+ m.stateMutex.Lock()
+ m.state.Settings.ColorScheme = colorScheme
+ m.stateMutex.Unlock()
+ }
+
+ return nil
+}
+
+func (m *Manager) getAccountProperties(ctx context.Context) (map[string]dbus.Variant, error) {
+ var props map[string]dbus.Variant
+ err := m.accountsObj.CallWithContext(ctx, dbusPropsInterface+".GetAll", 0, dbusAccountsUserInterface).Store(&props)
+ if err != nil {
+ return nil, err
+ }
+ return props, nil
+}
+
+func (m *Manager) GetState() FreedeskState {
+ m.stateMutex.RLock()
+ defer m.stateMutex.RUnlock()
+ return *m.state
+}
+
+func (m *Manager) Subscribe(id string) chan FreedeskState {
+ ch := make(chan FreedeskState, 64)
+ m.subscribers.Store(id, ch)
+ return ch
+}
+
+func (m *Manager) Unsubscribe(id string) {
+ if val, ok := m.subscribers.LoadAndDelete(id); ok {
+ close(val)
+ }
+}
+
+func (m *Manager) NotifySubscribers() {
+ state := m.GetState()
+ m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
+ select {
+ case ch <- state:
+ default:
+ }
+ return true
+ })
+}
+
+func (m *Manager) Close() {
+ m.subscribers.Range(func(key string, ch chan FreedeskState) bool {
+ close(ch)
+ m.subscribers.Delete(key)
+ return true
+ })
+
+ m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
+ close(ch)
+ m.screensaverSubscribers.Delete(key)
+ return true
+ })
+
+ if m.systemConn != nil {
+ m.systemConn.Close()
+ }
+ if m.sessionConn != nil {
+ m.sessionConn.Close()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager_test.go
new file mode 100644
index 0000000..b3617f5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/manager_test.go
@@ -0,0 +1,143 @@
+package freedesktop
+
+import (
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestManager_GetState(t *testing.T) {
+ state := &FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ UserName: "testuser",
+ RealName: "Test User",
+ UID: 1000,
+ },
+ Settings: SettingsState{
+ Available: true,
+ ColorScheme: 1,
+ },
+ }
+
+ manager := &Manager{
+ state: state,
+ stateMutex: sync.RWMutex{},
+ }
+
+ result := manager.GetState()
+ assert.True(t, result.Accounts.Available)
+ assert.Equal(t, "testuser", result.Accounts.UserName)
+ assert.Equal(t, "Test User", result.Accounts.RealName)
+ assert.Equal(t, uint64(1000), result.Accounts.UID)
+ assert.True(t, result.Settings.Available)
+ assert.Equal(t, uint32(1), result.Settings.ColorScheme)
+}
+
+func TestManager_GetState_ThreadSafe(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ UserName: "testuser",
+ },
+ Settings: SettingsState{
+ Available: true,
+ ColorScheme: 1,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ done := make(chan bool)
+ for i := 0; i < 10; i++ {
+ go func() {
+ state := manager.GetState()
+ assert.True(t, state.Accounts.Available)
+ assert.Equal(t, "testuser", state.Accounts.UserName)
+ done <- true
+ }()
+ }
+
+ for i := 0; i < 10; i++ {
+ <-done
+ }
+}
+
+func TestManager_Close(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{},
+ stateMutex: sync.RWMutex{},
+ systemConn: nil,
+ sessionConn: nil,
+ }
+
+ assert.NotPanics(t, func() {
+ manager.Close()
+ })
+}
+
+func TestNewManager(t *testing.T) {
+ t.Run("attempts to create manager", func(t *testing.T) {
+ manager, err := NewManager()
+ if err != nil {
+ assert.Nil(t, manager)
+ } else {
+ assert.NotNil(t, manager)
+ assert.NotNil(t, manager.state)
+ assert.NotNil(t, manager.systemConn)
+
+ manager.Close()
+ }
+ })
+}
+
+func TestManager_GetState_EmptyState(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{},
+ stateMutex: sync.RWMutex{},
+ }
+
+ result := manager.GetState()
+ assert.False(t, result.Accounts.Available)
+ assert.Empty(t, result.Accounts.UserName)
+ assert.False(t, result.Settings.Available)
+ assert.Equal(t, uint32(0), result.Settings.ColorScheme)
+}
+
+func TestManager_AccountsState_Modification(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ UserName: "testuser",
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ state := manager.GetState()
+ state.Accounts.UserName = "modifieduser"
+
+ original := manager.GetState()
+ assert.Equal(t, "testuser", original.Accounts.UserName)
+}
+
+func TestManager_SettingsState_Modification(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Settings: SettingsState{
+ Available: true,
+ ColorScheme: 0,
+ },
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ state := manager.GetState()
+ state.Settings.ColorScheme = 1
+
+ original := manager.GetState()
+ assert.Equal(t, uint32(0), original.Settings.ColorScheme)
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver.go
new file mode 100644
index 0000000..095a4c0
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver.go
@@ -0,0 +1,349 @@
+package freedesktop
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
+ "github.com/godbus/dbus/v5"
+ "github.com/godbus/dbus/v5/introspect"
+)
+
+type screensaverHandler struct {
+ manager *Manager
+}
+
+func screensaverIntrospectIface(ifaceName string) introspect.Interface {
+ return introspect.Interface{
+ Name: ifaceName,
+ Methods: []introspect.Method{
+ {
+ Name: "Inhibit",
+ Args: []introspect.Arg{
+ {Name: "application_name", Type: "s", Direction: "in"},
+ {Name: "reason_for_inhibit", Type: "s", Direction: "in"},
+ {Name: "cookie", Type: "u", Direction: "out"},
+ },
+ },
+ {
+ Name: "UnInhibit",
+ Args: []introspect.Arg{
+ {Name: "cookie", Type: "u", Direction: "in"},
+ },
+ },
+ {
+ Name: "GetActive",
+ Args: []introspect.Arg{
+ {Name: "active", Type: "b", Direction: "out"},
+ },
+ },
+ {
+ Name: "SetActive",
+ Args: []introspect.Arg{
+ {Name: "active", Type: "b", Direction: "in"},
+ },
+ },
+ {
+ Name: "Lock",
+ },
+ },
+ Signals: []introspect.Signal{
+ {
+ Name: "ActiveChanged",
+ Args: []introspect.Arg{
+ {Name: "new_value", Type: "b"},
+ },
+ },
+ },
+ }
+}
+
+func (m *Manager) initializeScreensaver() error {
+ if m.sessionConn == nil {
+ m.stateMutex.Lock()
+ m.state.Screensaver.Available = false
+ m.stateMutex.Unlock()
+ return nil
+ }
+
+ handler := &screensaverHandler{manager: m}
+
+ m.screensaverFreedesktopClaimed = m.claimScreensaverName(handler,
+ dbusScreensaverName, dbusScreensaverInterface, dbusScreensaverPath, dbusScreensaverPath2)
+ m.screensaverGnomeClaimed = m.claimScreensaverName(handler,
+ dbusGnomeScreensaverName, dbusGnomeScreensaverInterface, dbusGnomeScreensaverPath)
+
+ if !m.screensaverFreedesktopClaimed && !m.screensaverGnomeClaimed {
+ log.Warn("No screensaver interface could be claimed")
+ m.stateMutex.Lock()
+ m.state.Screensaver.Available = false
+ m.stateMutex.Unlock()
+ return nil
+ }
+
+ go m.watchPeerDisconnects()
+
+ m.stateMutex.Lock()
+ m.state.Screensaver.Available = true
+ m.state.Screensaver.Active = false
+ m.state.Screensaver.Inhibited = false
+ m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
+ m.stateMutex.Unlock()
+
+ log.Info("Screensaver listener initialized")
+ return nil
+}
+
+func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface string, paths ...dbus.ObjectPath) bool {
+ reply, err := m.sessionConn.RequestName(name, dbus.NameFlagDoNotQueue)
+ if err != nil {
+ log.Warnf("Failed to request screensaver name %s: %v", name, err)
+ return false
+ }
+ if reply != dbus.RequestNameReplyPrimaryOwner {
+ log.Warnf("Screensaver name %s already owned by another process", name)
+ return false
+ }
+ if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
+ log.Warnf("Failed to export screensaver on %s: %v", name, err)
+ return false
+ }
+ log.Infof("Claimed %s on session bus", name)
+ return true
+}
+
+// exportScreensaverOnPaths exports the handler and introspection on the given
+// paths under the specified interface name.
+func (m *Manager) exportScreensaverOnPaths(handler *screensaverHandler, ifaceName string, paths ...dbus.ObjectPath) error {
+ iface := screensaverIntrospectIface(ifaceName)
+ for _, path := range paths {
+ if err := m.sessionConn.Export(handler, path, ifaceName); err != nil {
+ return fmt.Errorf("export handler on %s: %w", path, err)
+ }
+ node := &introspect.Node{
+ Name: string(path),
+ Interfaces: []introspect.Interface{
+ introspect.IntrospectData,
+ iface,
+ },
+ }
+ if err := m.sessionConn.Export(introspect.NewIntrospectable(node), path, "org.freedesktop.DBus.Introspectable"); err != nil {
+ log.Warnf("Failed to export introspectable on %s: %v", path, err)
+ }
+ }
+ return nil
+}
+
+func (h *screensaverHandler) Inhibit(sender dbus.Sender, appName, reason string) (uint32, *dbus.Error) {
+ if appName == "" {
+ return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"application name required"})
+ }
+
+ if reason == "" {
+ return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"reason required"})
+ }
+
+ if strings.Contains(strings.ToLower(reason), "audio") && !strings.Contains(strings.ToLower(reason), "video") {
+ log.Debugf("Ignoring audio-only inhibit from %s: %s", appName, reason)
+ return 0, nil
+ }
+
+ if idx := strings.LastIndex(appName, "/"); idx != -1 && idx < len(appName)-1 {
+ appName = appName[idx+1:]
+ }
+ appName = filepath.Base(appName)
+
+ cookie := atomic.AddUint32(&h.manager.screensaverCookieCounter, 1)
+
+ inhibitor := ScreensaverInhibitor{
+ Cookie: cookie,
+ AppName: appName,
+ Reason: reason,
+ Peer: string(sender),
+ StartTime: time.Now().Unix(),
+ }
+
+ h.manager.stateMutex.Lock()
+ h.manager.state.Screensaver.Inhibitors = append(h.manager.state.Screensaver.Inhibitors, inhibitor)
+ h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0
+ h.manager.stateMutex.Unlock()
+
+ log.Infof("Screensaver inhibited by %s (%s): %s -> cookie %08X", appName, sender, reason, cookie)
+
+ h.manager.NotifyScreensaverSubscribers()
+
+ return cookie, nil
+}
+
+func (h *screensaverHandler) UnInhibit(sender dbus.Sender, cookie uint32) *dbus.Error {
+ h.manager.stateMutex.Lock()
+ defer h.manager.stateMutex.Unlock()
+
+ found := false
+ inhibitors := h.manager.state.Screensaver.Inhibitors
+ for i, inh := range inhibitors {
+ if inh.Cookie != cookie {
+ continue
+ }
+ log.Infof("Screensaver uninhibited by %s (%s) cookie %08X", inh.AppName, sender, cookie)
+ h.manager.state.Screensaver.Inhibitors = append(inhibitors[:i], inhibitors[i+1:]...)
+ found = true
+ break
+ }
+
+ if !found {
+ log.Debugf("UnInhibit: no match for cookie %08X", cookie)
+ return nil
+ }
+
+ h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0
+
+ go h.manager.NotifyScreensaverSubscribers()
+
+ return nil
+}
+
+func (m *Manager) watchPeerDisconnects() {
+ if m.sessionConn == nil {
+ return
+ }
+
+ if err := m.sessionConn.AddMatchSignal(
+ dbus.WithMatchInterface("org.freedesktop.DBus"),
+ dbus.WithMatchMember("NameOwnerChanged"),
+ ); err != nil {
+ log.Warnf("Failed to watch peer disconnects: %v", err)
+ return
+ }
+
+ signals := make(chan *dbus.Signal, 64)
+ m.sessionConn.Signal(signals)
+
+ for sig := range signals {
+ if sig.Name != "org.freedesktop.DBus.NameOwnerChanged" {
+ continue
+ }
+ if len(sig.Body) < 3 {
+ continue
+ }
+
+ name, ok1 := sig.Body[0].(string)
+ newOwner, ok2 := sig.Body[2].(string)
+ if !ok1 || !ok2 {
+ continue
+ }
+ if newOwner != "" {
+ continue
+ }
+
+ m.removeInhibitorsByPeer(name)
+ }
+}
+
+func (m *Manager) removeInhibitorsByPeer(peer string) {
+ m.stateMutex.Lock()
+ defer m.stateMutex.Unlock()
+
+ var remaining []ScreensaverInhibitor
+ var removed []ScreensaverInhibitor
+ for _, inh := range m.state.Screensaver.Inhibitors {
+ if inh.Peer == peer {
+ removed = append(removed, inh)
+ continue
+ }
+ remaining = append(remaining, inh)
+ }
+
+ if len(removed) == 0 {
+ return
+ }
+
+ for _, inh := range removed {
+ log.Infof("Screensaver: peer %s died, removing inhibitor from %s (cookie %08X)", peer, inh.AppName, inh.Cookie)
+ }
+
+ m.state.Screensaver.Inhibitors = remaining
+ m.state.Screensaver.Inhibited = len(remaining) > 0
+
+ go m.NotifyScreensaverSubscribers()
+}
+
+func (m *Manager) GetScreensaverState() ScreensaverState {
+ m.stateMutex.RLock()
+ defer m.stateMutex.RUnlock()
+ return m.state.Screensaver
+}
+
+func (m *Manager) SubscribeScreensaver(id string) chan ScreensaverState {
+ ch := make(chan ScreensaverState, 64)
+ m.screensaverSubscribers.Store(id, ch)
+ return ch
+}
+
+func (m *Manager) UnsubscribeScreensaver(id string) {
+ if val, ok := m.screensaverSubscribers.LoadAndDelete(id); ok {
+ close(val)
+ }
+}
+
+func (m *Manager) NotifyScreensaverSubscribers() {
+ state := m.GetScreensaverState()
+ m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
+ select {
+ case ch <- state:
+ default:
+ }
+ return true
+ })
+}
+
+func (h *screensaverHandler) GetActive() (bool, *dbus.Error) {
+ h.manager.stateMutex.RLock()
+ active := h.manager.state.Screensaver.Active
+ h.manager.stateMutex.RUnlock()
+ return active, nil
+}
+
+func (h *screensaverHandler) SetActive(active bool) *dbus.Error {
+ h.manager.SetScreenLockActive(active)
+ return nil
+}
+
+func (h *screensaverHandler) Lock() *dbus.Error {
+ h.manager.SetScreenLockActive(true)
+ return nil
+}
+
+func (m *Manager) SetScreenLockActive(active bool) {
+ m.stateMutex.Lock()
+ changed := m.state.Screensaver.Active != active
+ m.state.Screensaver.Active = active
+ m.stateMutex.Unlock()
+
+ if !changed {
+ return
+ }
+
+ log.Infof("Screen lock active changed: %v", active)
+ defer m.NotifyScreensaverSubscribers()
+
+ if m.sessionConn == nil {
+ return
+ }
+ if m.screensaverFreedesktopClaimed {
+ if err := m.sessionConn.Emit(dbusScreensaverPath, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
+ log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath, err)
+ }
+ if err := m.sessionConn.Emit(dbusScreensaverPath2, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
+ log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath2, err)
+ }
+ }
+ if m.screensaverGnomeClaimed {
+ if err := m.sessionConn.Emit(dbusGnomeScreensaverPath, dbusGnomeScreensaverInterface+".ActiveChanged", active); err != nil {
+ log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusGnomeScreensaverPath, err)
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver_test.go
new file mode 100644
index 0000000..324c17e
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/screensaver_test.go
@@ -0,0 +1,102 @@
+package freedesktop
+
+import (
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSetScreenLockActive_ChangesState(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Screensaver: ScreensaverState{Available: true},
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ assert.False(t, manager.GetScreensaverState().Active)
+
+ manager.SetScreenLockActive(true)
+ assert.True(t, manager.GetScreensaverState().Active)
+
+ manager.SetScreenLockActive(false)
+ assert.False(t, manager.GetScreensaverState().Active)
+}
+
+func TestSetScreenLockActive_NoChangeNoDuplicate(t *testing.T) {
+ ch := make(chan ScreensaverState, 64)
+ manager := &Manager{
+ state: &FreedeskState{
+ Screensaver: ScreensaverState{Available: true, Active: false},
+ },
+ stateMutex: sync.RWMutex{},
+ }
+ manager.screensaverSubscribers.Store("test", ch)
+ defer manager.screensaverSubscribers.Delete("test")
+
+ // Setting to same value should not notify
+ manager.SetScreenLockActive(false)
+
+ select {
+ case <-ch:
+ t.Fatal("should not have received notification for no-change")
+ case <-time.After(50 * time.Millisecond):
+ // Expected: no notification
+ }
+}
+
+func TestSetScreenLockActive_NotifiesSubscribers(t *testing.T) {
+ ch := make(chan ScreensaverState, 64)
+ manager := &Manager{
+ state: &FreedeskState{
+ Screensaver: ScreensaverState{Available: true, Active: false},
+ },
+ stateMutex: sync.RWMutex{},
+ }
+ manager.screensaverSubscribers.Store("test", ch)
+ defer manager.screensaverSubscribers.Delete("test")
+
+ manager.SetScreenLockActive(true)
+
+ select {
+ case state := <-ch:
+ assert.True(t, state.Active)
+ case <-time.After(time.Second):
+ t.Fatal("timeout waiting for subscriber notification")
+ }
+}
+
+func TestSetScreenLockActive_NilSessionConn(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Screensaver: ScreensaverState{Available: true},
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ assert.NotPanics(t, func() {
+ manager.SetScreenLockActive(true)
+ })
+ assert.True(t, manager.GetScreensaverState().Active)
+}
+
+func TestGetActive_ReturnsCurrentState(t *testing.T) {
+ manager := &Manager{
+ state: &FreedeskState{
+ Screensaver: ScreensaverState{Available: true, Active: true},
+ },
+ stateMutex: sync.RWMutex{},
+ }
+
+ handler := &screensaverHandler{manager: manager}
+ active, dbusErr := handler.GetActive()
+ assert.Nil(t, dbusErr)
+ assert.True(t, active)
+}
+
+func TestScreensaverState_ActiveDefaultsFalse(t *testing.T) {
+ state := ScreensaverState{}
+ assert.False(t, state.Active)
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types.go
new file mode 100644
index 0000000..0a9517a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types.go
@@ -0,0 +1,66 @@
+package freedesktop
+
+import (
+ "sync"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
+ "github.com/godbus/dbus/v5"
+)
+
+type AccountsState struct {
+ Available bool `json:"available"`
+ UserPath string `json:"userPath"`
+ IconFile string `json:"iconFile"`
+ RealName string `json:"realName"`
+ UserName string `json:"userName"`
+ AccountType int32 `json:"accountType"`
+ HomeDirectory string `json:"homeDirectory"`
+ Shell string `json:"shell"`
+ Email string `json:"email"`
+ Language string `json:"language"`
+ Location string `json:"location"`
+ Locked bool `json:"locked"`
+ PasswordMode int32 `json:"passwordMode"`
+ UID uint64 `json:"uid"`
+}
+
+type SettingsState struct {
+ Available bool `json:"available"`
+ ColorScheme uint32 `json:"colorScheme"`
+}
+
+type ScreensaverInhibitor struct {
+ Cookie uint32 `json:"cookie"`
+ AppName string `json:"appName"`
+ Reason string `json:"reason"`
+ Peer string `json:"peer"`
+ StartTime int64 `json:"startTime"`
+}
+
+type ScreensaverState struct {
+ Available bool `json:"available"`
+ Active bool `json:"active"`
+ Inhibited bool `json:"inhibited"`
+ Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
+}
+
+type FreedeskState struct {
+ Accounts AccountsState `json:"accounts"`
+ Settings SettingsState `json:"settings"`
+ Screensaver ScreensaverState `json:"screensaver"`
+}
+
+type Manager struct {
+ state *FreedeskState
+ stateMutex sync.RWMutex
+ systemConn *dbus.Conn
+ sessionConn *dbus.Conn
+ accountsObj dbus.BusObject
+ settingsObj dbus.BusObject
+ currentUID uint64
+ subscribers syncmap.Map[string, chan FreedeskState]
+ screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
+ screensaverCookieCounter uint32
+ screensaverFreedesktopClaimed bool
+ screensaverGnomeClaimed bool
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types_test.go
new file mode 100644
index 0000000..deb0327
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/freedesktop/types_test.go
@@ -0,0 +1,70 @@
+package freedesktop
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAccountsState_Struct(t *testing.T) {
+ state := AccountsState{
+ Available: true,
+ UserPath: "/org/freedesktop/Accounts/User1000",
+ RealName: "Test User",
+ UserName: "testuser",
+ Locked: false,
+ UID: 1000,
+ }
+
+ assert.True(t, state.Available)
+ assert.Equal(t, "/org/freedesktop/Accounts/User1000", state.UserPath)
+ assert.Equal(t, "Test User", state.RealName)
+ assert.Equal(t, "testuser", state.UserName)
+ assert.Equal(t, uint64(1000), state.UID)
+ assert.False(t, state.Locked)
+}
+
+func TestSettingsState_Struct(t *testing.T) {
+ state := SettingsState{
+ Available: true,
+ ColorScheme: 1, // Dark mode
+ }
+
+ assert.True(t, state.Available)
+ assert.Equal(t, uint32(1), state.ColorScheme)
+}
+
+func TestFreedeskState_Struct(t *testing.T) {
+ state := FreedeskState{
+ Accounts: AccountsState{
+ Available: true,
+ UserName: "testuser",
+ UID: 1000,
+ },
+ Settings: SettingsState{
+ Available: true,
+ ColorScheme: 0, // Light mode
+ },
+ }
+
+ assert.True(t, state.Accounts.Available)
+ assert.Equal(t, "testuser", state.Accounts.UserName)
+ assert.True(t, state.Settings.Available)
+ assert.Equal(t, uint32(0), state.Settings.ColorScheme)
+}
+
+func TestAccountsState_DefaultValues(t *testing.T) {
+ state := AccountsState{}
+
+ assert.False(t, state.Available)
+ assert.Empty(t, state.UserPath)
+ assert.Empty(t, state.UserName)
+ assert.Equal(t, uint64(0), state.UID)
+}
+
+func TestSettingsState_DefaultValues(t *testing.T) {
+ state := SettingsState{}
+
+ assert.False(t, state.Available)
+ assert.Equal(t, uint32(0), state.ColorScheme)
+}