package freedesktop import ( "context" "fmt" "os" "sync" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/dankgo/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) } go m.watchSettingsChanges() return nil } // ExpectColorSchemeEcho registers a self-write so the watcher swallows its SettingChanged echo instead of forwarding it as an external change. func (m *Manager) ExpectColorSchemeEcho(scheme string) { var value uint32 switch scheme { case "prefer-dark": value = 1 case "prefer-light": value = 2 } m.selfEchoMu.Lock() m.selfEchoes = append(m.selfEchoes, colorSchemeEcho{value: value, expires: time.Now().Add(10 * time.Second)}) m.selfEchoMu.Unlock() } func (m *Manager) consumeSelfEcho(value uint32) bool { m.selfEchoMu.Lock() defer m.selfEchoMu.Unlock() now := time.Now() kept := m.selfEchoes[:0] consumed := false for _, echo := range m.selfEchoes { if now.After(echo.expires) { continue } if !consumed && echo.value == value { consumed = true continue } kept = append(kept, echo) } m.selfEchoes = kept return consumed } func (m *Manager) watchSettingsChanges() { // reuse the shared session connection; a dedicated one was unreachable // from Close() and leaked with this goroutine if m.sessionConn == nil { return } conn := m.sessionConn if err := conn.AddMatchSignal( dbus.WithMatchInterface(dbusPortalSettingsInterface), dbus.WithMatchMember("SettingChanged"), ); err != nil { log.Warnf("Failed to watch portal settings changes: %v", err) return } signals := make(chan *dbus.Signal, 64) m.stateMutex.Lock() m.settingsSignals = signals m.stateMutex.Unlock() conn.Signal(signals) for sig := range signals { if sig.Name != dbusPortalSettingsInterface+".SettingChanged" { continue } if len(sig.Body) < 3 { continue } namespace, _ := sig.Body[0].(string) key, _ := sig.Body[1].(string) if namespace != "org.freedesktop.appearance" || key != "color-scheme" { continue } variant, ok := sig.Body[2].(dbus.Variant) if !ok { continue } colorScheme, ok := dbusutil.As[uint32](variant) if !ok { continue } selfInflicted := m.consumeSelfEcho(colorScheme) m.stateMutex.Lock() changed := m.state.Settings.ColorScheme != colorScheme || !m.state.Settings.Available m.state.Settings.ColorScheme = colorScheme m.state.Settings.Available = true m.stateMutex.Unlock() if changed && !selfInflicted { m.NotifySubscribers() } } } 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 { // Older xdg-desktop-portal versions only expose the deprecated Read. var nested dbus.Variant if rerr := m.settingsObj.Call(dbusPortalSettingsInterface+".Read", 0, "org.freedesktop.appearance", "color-scheme").Store(&nested); rerr != nil { log.Warnf("color-scheme: ReadOne (%v) and Read (%v) both failed", err, rerr) return err } variant = nested } colorScheme, ok := dbusutil.As[uint32](variant) if !ok { // Read double-wraps the value in a variant. if inner, innerOk := variant.Value().(dbus.Variant); innerOk { colorScheme, ok = dbusutil.As[uint32](inner) } } if 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.RemoveMatchSignal( dbus.WithMatchInterface(dbusPortalSettingsInterface), dbus.WithMatchMember("SettingChanged"), ) m.stateMutex.Lock() signals := m.settingsSignals m.settingsSignals = nil m.stateMutex.Unlock() if signals != nil { m.sessionConn.RemoveSignal(signals) close(signals) } m.sessionConn.Close() } }