mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
0cdb065739
binary to mount at runtime, -c or DMS_SHELL_DIR overrides required to explicitly override embedded configuration
330 lines
8.2 KiB
Go
330 lines
8.2 KiB
Go
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()
|
|
}
|
|
}
|