mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
idle: implement screensaver interface
- Mainly used to create the idle inhibitor when an app requests screensaver inhibit
This commit is contained in:
@@ -11,4 +11,9 @@ const (
|
|||||||
dbusPortalSettingsInterface = "org.freedesktop.portal.Settings"
|
dbusPortalSettingsInterface = "org.freedesktop.portal.Settings"
|
||||||
|
|
||||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||||
|
|
||||||
|
dbusScreensaverName = "org.freedesktop.ScreenSaver"
|
||||||
|
dbusScreensaverPath = "/ScreenSaver"
|
||||||
|
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
|
||||||
|
dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ func NewManager() (*Manager, error) {
|
|||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
state: &FreedeskState{
|
state: &FreedeskState{
|
||||||
Accounts: AccountsState{},
|
Accounts: AccountsState{},
|
||||||
Settings: SettingsState{},
|
Settings: SettingsState{},
|
||||||
|
Screensaver: ScreensaverState{},
|
||||||
},
|
},
|
||||||
stateMutex: sync.RWMutex{},
|
stateMutex: sync.RWMutex{},
|
||||||
systemConn: systemConn,
|
systemConn: systemConn,
|
||||||
@@ -33,6 +34,7 @@ func NewManager() (*Manager, error) {
|
|||||||
|
|
||||||
m.initializeAccounts()
|
m.initializeAccounts()
|
||||||
m.initializeSettings()
|
m.initializeSettings()
|
||||||
|
m.initializeScreensaver()
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|||||||
250
core/internal/server/freedesktop/screensaver.go
Normal file
250
core/internal/server/freedesktop/screensaver.go
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package freedesktop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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 (m *Manager) initializeScreensaver() error {
|
||||||
|
if m.sessionConn == nil {
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
m.state.Screensaver.Available = false
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to request screensaver name: %v", err)
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
m.state.Screensaver.Available = false
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||||
|
log.Warnf("Screensaver name already owned by another process")
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
m.state.Screensaver.Available = false
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := &screensaverHandler{manager: m}
|
||||||
|
|
||||||
|
if err := m.sessionConn.Export(handler, dbusScreensaverPath, dbusScreensaverInterface); err != nil {
|
||||||
|
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.sessionConn.Export(handler, dbusScreensaverPath2, dbusScreensaverInterface); err != nil {
|
||||||
|
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath2, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
introNode := &introspect.Node{
|
||||||
|
Name: dbusScreensaverPath,
|
||||||
|
Interfaces: []introspect.Interface{
|
||||||
|
introspect.IntrospectData,
|
||||||
|
{Name: dbusScreensaverInterface},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||||
|
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
introNode2 := &introspect.Node{
|
||||||
|
Name: dbusScreensaverPath2,
|
||||||
|
Interfaces: []introspect.Interface{
|
||||||
|
introspect.IntrospectData,
|
||||||
|
{Name: dbusScreensaverInterface},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||||
|
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go m.watchPeerDisconnects()
|
||||||
|
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
m.state.Screensaver.Available = true
|
||||||
|
m.state.Screensaver.Inhibited = false
|
||||||
|
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
|
log.Info("Screensaver inhibit listener initialized")
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -29,18 +29,35 @@ type SettingsState struct {
|
|||||||
ColorScheme uint32 `json:"colorScheme"`
|
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"`
|
||||||
|
Inhibited bool `json:"inhibited"`
|
||||||
|
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
|
||||||
|
}
|
||||||
|
|
||||||
type FreedeskState struct {
|
type FreedeskState struct {
|
||||||
Accounts AccountsState `json:"accounts"`
|
Accounts AccountsState `json:"accounts"`
|
||||||
Settings SettingsState `json:"settings"`
|
Settings SettingsState `json:"settings"`
|
||||||
|
Screensaver ScreensaverState `json:"screensaver"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
state *FreedeskState
|
state *FreedeskState
|
||||||
stateMutex sync.RWMutex
|
stateMutex sync.RWMutex
|
||||||
systemConn *dbus.Conn
|
systemConn *dbus.Conn
|
||||||
sessionConn *dbus.Conn
|
sessionConn *dbus.Conn
|
||||||
accountsObj dbus.BusObject
|
accountsObj dbus.BusObject
|
||||||
settingsObj dbus.BusObject
|
settingsObj dbus.BusObject
|
||||||
currentUID uint64
|
currentUID uint64
|
||||||
subscribers syncmap.Map[string, chan FreedeskState]
|
subscribers syncmap.Map[string, chan FreedeskState]
|
||||||
|
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
|
||||||
|
screensaverCookieCounter uint32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const APIVersion = 23
|
const APIVersion = 24
|
||||||
|
|
||||||
var CLIVersion = "dev"
|
var CLIVersion = "dev"
|
||||||
|
|
||||||
@@ -702,6 +702,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldSubscribe("freedesktop.screensaver") && freedesktopManager != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
screensaverChan := freedesktopManager.SubscribeScreensaver(clientID + "-screensaver")
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer freedesktopManager.UnsubscribeScreensaver(clientID + "-screensaver")
|
||||||
|
|
||||||
|
initialState := freedesktopManager.GetScreensaverState()
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: initialState}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state, ok := <-screensaverChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: state}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if shouldSubscribe("gamma") && waylandManager != nil {
|
if shouldSubscribe("gamma") && waylandManager != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
waylandChan := waylandManager.Subscribe(clientID + "-gamma")
|
waylandChan := waylandManager.Subscribe(clientID + "-gamma")
|
||||||
|
|||||||
@@ -53,10 +53,13 @@ Singleton {
|
|||||||
signal gammaStateUpdate(var data)
|
signal gammaStateUpdate(var data)
|
||||||
signal openUrlRequested(string url)
|
signal openUrlRequested(string url)
|
||||||
signal appPickerRequested(var data)
|
signal appPickerRequested(var data)
|
||||||
|
signal screensaverStateUpdate(var data)
|
||||||
|
|
||||||
property bool capsLockState: false
|
property bool capsLockState: false
|
||||||
|
property bool screensaverInhibited: false
|
||||||
|
property var screensaverInhibitors: []
|
||||||
|
|
||||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser"]
|
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser"]
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (socketPath && socketPath.length > 0) {
|
if (socketPath && socketPath.length > 0) {
|
||||||
@@ -371,6 +374,10 @@ Singleton {
|
|||||||
} else if (data.url) {
|
} else if (data.url) {
|
||||||
openUrlRequested(data.url);
|
openUrlRequested(data.url);
|
||||||
}
|
}
|
||||||
|
} else if (service === "freedesktop.screensaver") {
|
||||||
|
screensaverInhibited = data.inhibited || false;
|
||||||
|
screensaverInhibitors = data.inhibitors || [];
|
||||||
|
screensaverStateUpdate(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ Singleton {
|
|||||||
property bool respectInhibitors: true
|
property bool respectInhibitors: true
|
||||||
property bool _enableGate: true
|
property bool _enableGate: true
|
||||||
|
|
||||||
|
readonly property bool externalInhibitActive: DMSService.screensaverInhibited
|
||||||
|
|
||||||
readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn
|
readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn
|
||||||
readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout
|
readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout
|
||||||
readonly property int lockTimeout: isOnBattery ? SettingsData.batteryLockTimeout : SettingsData.acLockTimeout
|
readonly property int lockTimeout: isOnBattery ? SettingsData.batteryLockTimeout : SettingsData.acLockTimeout
|
||||||
@@ -141,6 +143,19 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onExternalInhibitActiveChanged: {
|
||||||
|
if (externalInhibitActive) {
|
||||||
|
const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", ");
|
||||||
|
console.info("IdleService: External idle inhibit active from:", apps || "unknown");
|
||||||
|
SessionService.idleInhibited = true;
|
||||||
|
SessionService.inhibitReason = "External app: " + (apps || "unknown");
|
||||||
|
} else {
|
||||||
|
console.info("IdleService: External idle inhibit released");
|
||||||
|
SessionService.idleInhibited = false;
|
||||||
|
SessionService.inhibitReason = "Keep system awake";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!idleMonitorAvailable) {
|
if (!idleMonitorAvailable) {
|
||||||
console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.");
|
console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.");
|
||||||
@@ -148,5 +163,11 @@ Singleton {
|
|||||||
console.info("IdleService: Initialized with idle monitoring support");
|
console.info("IdleService: Initialized with idle monitoring support");
|
||||||
createIdleMonitors();
|
createIdleMonitors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (externalInhibitActive) {
|
||||||
|
const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", ");
|
||||||
|
SessionService.idleInhibited = true;
|
||||||
|
SessionService.inhibitReason = "External app: " + (apps || "unknown");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user