1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 02:22:06 -04:00

Compare commits

...

2 Commits

Author SHA1 Message Date
bbedward
97fa86d8f0 loginctl: simplify event handling 2026-04-22 10:32:05 -04:00
Kristijan Ribarić
b87c36d29e fix(quickshell): restore night mode and OSD surfaces after resume (#2254) 2026-04-22 10:08:50 -04:00
8 changed files with 253 additions and 187 deletions

View File

@@ -35,12 +35,7 @@ type SessionState struct {
type EventType string type EventType string
const ( const (
EventStateChanged EventType = "state_changed" EventStateChanged EventType = "state_changed"
EventLock EventType = "lock"
EventUnlock EventType = "unlock"
EventPrepareForSleep EventType = "prepare_for_sleep"
EventIdleHintChanged EventType = "idle_hint_changed"
EventLockedHintChanged EventType = "locked_hint_changed"
) )
type SessionEvent struct { type SessionEvent struct {

View File

@@ -8,11 +8,6 @@ import (
func TestEventType_Constants(t *testing.T) { func TestEventType_Constants(t *testing.T) {
assert.Equal(t, EventType("state_changed"), EventStateChanged) assert.Equal(t, EventType("state_changed"), EventStateChanged)
assert.Equal(t, EventType("lock"), EventLock)
assert.Equal(t, EventType("unlock"), EventUnlock)
assert.Equal(t, EventType("prepare_for_sleep"), EventPrepareForSleep)
assert.Equal(t, EventType("idle_hint_changed"), EventIdleHintChanged)
assert.Equal(t, EventType("locked_hint_changed"), EventLockedHintChanged)
} }
func TestSessionState_Struct(t *testing.T) { func TestSessionState_Struct(t *testing.T) {
@@ -40,11 +35,11 @@ func TestSessionEvent_Struct(t *testing.T) {
} }
event := SessionEvent{ event := SessionEvent{
Type: EventLock, Type: EventStateChanged,
Data: state, Data: state,
} }
assert.Equal(t, EventLock, event.Type) assert.Equal(t, EventStateChanged, event.Type)
assert.Equal(t, "1", event.Data.SessionID) assert.Equal(t, "1", event.Data.SessionID)
assert.True(t, event.Data.Locked) assert.True(t, event.Data.Locked)
} }

View File

@@ -2,12 +2,10 @@ package version
import ( import (
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"testing" "testing"
mocks_version "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/version" mocks_version "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/version"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestCompareVersions(t *testing.T) { func TestCompareVersions(t *testing.T) {
@@ -150,76 +148,6 @@ func TestGetCurrentDMSVersion_NotInstalled(t *testing.T) {
} }
} }
func TestGetCurrentDMSVersion_GitTag(t *testing.T) {
if !utils.CommandExists("git") {
t.Skip("git not available")
}
tempDir := t.TempDir()
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
os.MkdirAll(dmsPath, 0o755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", tempDir)
exec.Command("git", "init", dmsPath).Run()
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
testFile := filepath.Join(dmsPath, "test.txt")
os.WriteFile(testFile, []byte("test"), 0o644)
exec.Command("git", "-C", dmsPath, "add", ".").Run()
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
exec.Command("git", "-C", dmsPath, "tag", "v0.1.0").Run()
version, err := GetCurrentDMSVersion()
if err != nil {
t.Fatalf("GetCurrentDMSVersion() failed: %v", err)
}
if version != "v0.1.0" {
t.Errorf("Expected version v0.1.0, got %s", version)
}
}
func TestGetCurrentDMSVersion_GitBranch(t *testing.T) {
if !utils.CommandExists("git") {
t.Skip("git not available")
}
tempDir := t.TempDir()
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
os.MkdirAll(dmsPath, 0o755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", tempDir)
exec.Command("git", "init", dmsPath).Run()
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
testFile := filepath.Join(dmsPath, "test.txt")
os.WriteFile(testFile, []byte("test"), 0o644)
exec.Command("git", "-C", dmsPath, "add", ".").Run()
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
version, err := GetCurrentDMSVersion()
if err != nil {
t.Fatalf("GetCurrentDMSVersion() failed: %v", err)
}
if version == "" {
t.Error("Expected non-empty version")
}
if len(version) < 7 {
t.Errorf("Expected version with branch@commit format, got %s", version)
}
}
func TestVersionInfo_IsGit(t *testing.T) { func TestVersionInfo_IsGit(t *testing.T) {
tests := []struct { tests := []struct {
current string current string

View File

@@ -341,19 +341,6 @@ Singleton {
Connections { Connections {
target: DMSService target: DMSService
enabled: typeof DMSService !== "undefined" && typeof SessionData !== "undefined"
function onLoginctlEvent(event) {
if (!SessionData.themeModeAutoEnabled)
return;
if (event.event === "unlock" || event.event === "resume") {
if (!themeAutoBackendAvailable()) {
root.evaluateThemeMode();
return;
}
DMSService.sendRequest("theme.auto.trigger", {});
}
}
function onThemeAutoStateUpdate(data) { function onThemeAutoStateUpdate(data) {
if (!SessionData.themeModeAutoEnabled) { if (!SessionData.themeModeAutoEnabled) {
@@ -414,6 +401,27 @@ Singleton {
} }
} }
Connections {
target: SessionService
enabled: SessionData.themeModeAutoEnabled
function onSessionUnlocked() {
root.triggerThemeAutomationRefresh();
}
function onSessionResumed() {
root.triggerThemeAutomationRefresh();
}
}
function triggerThemeAutomationRefresh() {
if (!themeAutoBackendAvailable()) {
root.evaluateThemeMode();
return;
}
DMSService.sendRequest("theme.auto.trigger", {});
}
function applyGreeterTheme(themeName) { function applyGreeterTheme(themeName) {
switchTheme(themeName, false, false); switchTheme(themeName, false, false);
if (themeName === dynamic && dynamicColorsFileView.path) { if (themeName === dynamic && dynamicColorsFileView.path) {

View File

@@ -27,6 +27,15 @@ import qs.Services
Item { Item {
id: root id: root
property bool osdSurfacesLoaded: true
property int pendingOsdResumeReloads: 0
function recreateOsdSurfaces() {
OSDManager.currentOSDsByScreen = ({});
osdSurfacesLoaded = false;
osdSurfaceReloadTimer.restart();
}
Instantiator { Instantiator {
id: daemonPluginInstantiator id: daemonPluginInstantiator
asynchronous: true asynchronous: true
@@ -232,6 +241,32 @@ Item {
} }
} }
Timer {
id: osdResumeRecreateTimer
interval: 400
repeat: false
onTriggered: {
root.recreateOsdSurfaces();
root.pendingOsdResumeReloads--;
if (root.pendingOsdResumeReloads <= 0) {
root.pendingOsdResumeReloads = 0;
interval = 400;
return;
}
interval = 1400;
restart();
}
}
Timer {
id: osdSurfaceReloadTimer
interval: 120
repeat: false
onTriggered: root.osdSurfacesLoaded = true
}
Component.onCompleted: { Component.onCompleted: {
dockRecreateDebounce.start(); dockRecreateDebounce.start();
// Force PolkitService singleton to initialize // Force PolkitService singleton to initialize
@@ -749,6 +784,16 @@ Item {
} }
} }
Connections {
target: SessionService
function onSessionResumed() {
root.pendingOsdResumeReloads = 2;
osdResumeRecreateTimer.interval = 400;
osdResumeRecreateTimer.restart();
}
}
DankColorPickerModal { DankColorPickerModal {
id: colorPickerModal id: colorPickerModal
@@ -923,51 +968,85 @@ Item {
} }
} }
Variants { Loader {
model: SettingsData.getFilteredScreens("osd") id: osdSurfacesLoader
active: root.osdSurfacesLoaded
asynchronous: false
delegate: VolumeOSD { sourceComponent: Component {
modelData: item Item {
} Variants {
} model: SettingsData.getFilteredScreens("osd")
Variants { delegate: VolumeOSD {
model: SettingsData.getFilteredScreens("osd") modelData: item
}
}
delegate: MediaVolumeOSD { Variants {
modelData: item model: SettingsData.getFilteredScreens("osd")
}
}
Variants { delegate: MediaVolumeOSD {
model: SettingsData.getFilteredScreens("osd") modelData: item
}
}
delegate: MediaPlaybackOSD { Variants {
modelData: item model: SettingsData.getFilteredScreens("osd")
}
}
Variants { delegate: MediaPlaybackOSD {
model: SettingsData.getFilteredScreens("osd") modelData: item
}
}
delegate: MicMuteOSD { Variants {
modelData: item model: SettingsData.getFilteredScreens("osd")
}
}
Variants { delegate: MicMuteOSD {
model: SettingsData.getFilteredScreens("osd") modelData: item
}
}
delegate: BrightnessOSD { Variants {
modelData: item model: SettingsData.getFilteredScreens("osd")
}
}
Variants { delegate: BrightnessOSD {
model: SettingsData.getFilteredScreens("osd") modelData: item
}
}
delegate: IdleInhibitorOSD { Variants {
modelData: item model: SettingsData.getFilteredScreens("osd")
delegate: IdleInhibitorOSD {
modelData: item
}
}
Variants {
model: SettingsData.osdPowerProfileEnabled ? SettingsData.getFilteredScreens("osd") : []
delegate: PowerProfileOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: CapsLockOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: AudioOutputOSD {
modelData: item
}
}
}
} }
} }
@@ -977,30 +1056,6 @@ Item {
source: "Services/PowerProfileWatcher.qml" source: "Services/PowerProfileWatcher.qml"
} }
Variants {
model: SettingsData.osdPowerProfileEnabled ? SettingsData.getFilteredScreens("osd") : []
delegate: PowerProfileOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: CapsLockOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: AudioOutputOSD {
modelData: item
}
}
LazyLoader { LazyLoader {
id: hyprlandOverviewLoader id: hyprlandOverviewLoader
active: CompositorService.isHyprland active: CompositorService.isHyprland

View File

@@ -45,7 +45,6 @@ Singleton {
signal networkStateUpdate(var data) signal networkStateUpdate(var data)
signal cupsStateUpdate(var data) signal cupsStateUpdate(var data)
signal loginctlStateUpdate(var data) signal loginctlStateUpdate(var data)
signal loginctlEvent(var event)
signal capabilitiesReceived signal capabilitiesReceived
signal credentialsRequest(var data) signal credentialsRequest(var data)
signal bluetoothPairingRequest(var data) signal bluetoothPairingRequest(var data)
@@ -348,11 +347,7 @@ Singleton {
} else if (service === "network.credentials") { } else if (service === "network.credentials") {
credentialsRequest(data); credentialsRequest(data);
} else if (service === "loginctl") { } else if (service === "loginctl") {
if (data.event) { loginctlStateUpdate(data);
loginctlEvent(data);
} else {
loginctlStateUpdate(data);
}
} else if (service === "bluetooth.pairing") { } else if (service === "bluetooth.pairing") {
bluetoothPairingRequest(data); bluetoothPairingRequest(data);
} else if (service === "cups") { } else if (service === "cups") {

View File

@@ -40,6 +40,7 @@ Singleton {
property bool nightModeEnabled: false property bool nightModeEnabled: false
property bool automationAvailable: false property bool automationAvailable: false
property bool gammaControlAvailable: false property bool gammaControlAvailable: false
property int resumeRecoveryAttempt: 0
property var gammaState: ({}) property var gammaState: ({})
property int gammaCurrentTemp: gammaState?.currentTemp ?? 0 property int gammaCurrentTemp: gammaState?.currentTemp ?? 0
@@ -672,6 +673,15 @@ Singleton {
} }
} }
function runResumeRecoveryPass() {
checkGammaControlAvailability();
rescanDevices();
if (nightModeEnabled) {
evaluateNightMode();
}
}
function checkGammaControlAvailability() { function checkGammaControlAvailability() {
if (!DMSService.isConnected) { if (!DMSService.isConnected) {
return; return;
@@ -730,6 +740,31 @@ Singleton {
} }
} }
Timer {
id: resumeRecoveryTimer
interval: 400
repeat: false
onTriggered: {
runResumeRecoveryPass();
resumeRecoveryAttempt++;
switch (resumeRecoveryAttempt) {
case 1:
interval = 1400;
restart();
return;
case 2:
interval = 2600;
restart();
return;
}
resumeRecoveryAttempt = 0;
interval = 400;
}
}
function rescanDevices() { function rescanDevices() {
if (!DMSService.isConnected) { if (!DMSService.isConnected) {
return; return;
@@ -815,19 +850,23 @@ Singleton {
updateSingleDevice(device); updateSingleDevice(device);
} }
function onLoginctlEvent(event) {
if (event.event === "unlock" || event.event === "resume") {
suppressOsd = true;
osdSuppressTimer.restart();
evaluateNightMode();
}
}
function onGammaStateUpdate(data) { function onGammaStateUpdate(data) {
root.gammaState = data; root.gammaState = data;
} }
} }
Connections {
target: SessionService
function onSessionResumed() {
suppressOsd = true;
osdSuppressTimer.restart();
resumeRecoveryAttempt = 0;
resumeRecoveryTimer.interval = 400;
resumeRecoveryTimer.restart();
}
}
// Session Data Connections // Session Data Connections
Connections { Connections {
target: SessionData target: SessionData

View File

@@ -48,6 +48,9 @@ Singleton {
signal loginctlStateChanged signal loginctlStateChanged
property bool stateInitialized: false property bool stateInitialized: false
property string prepareForSleepSubscriptionId: ""
property bool prepareForSleepSubscriptionPending: false
property double lastResumeSignalTimestamp: 0
readonly property string socketPath: Quickshell.env("DMS_SOCKET") readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -463,6 +466,8 @@ Singleton {
function onConnectionStateChanged() { function onConnectionStateChanged() {
if (DMSService.isConnected) { if (DMSService.isConnected) {
checkDMSCapabilities(); checkDMSCapabilities();
} else {
clearPrepareForSleepSubscriptionState();
} }
} }
@@ -478,6 +483,13 @@ Singleton {
function onCapabilitiesChanged() { function onCapabilitiesChanged() {
checkDMSCapabilities(); checkDMSCapabilities();
} }
function onDbusSignalReceived(subscriptionId, data) {
if (subscriptionId !== prepareForSleepSubscriptionId) {
return;
}
handlePrepareForSleepSignal(data);
}
} }
Connections { Connections {
@@ -513,10 +525,6 @@ Singleton {
function onLoginctlStateUpdate(data) { function onLoginctlStateUpdate(data) {
updateLoginctlState(data); updateLoginctlState(data);
} }
function onLoginctlEvent(event) {
handleLoginctlEvent(event);
}
} }
function checkDMSCapabilities() { function checkDMSCapabilities() {
@@ -539,6 +547,61 @@ Singleton {
loginctlAvailable = false; loginctlAvailable = false;
console.log("SessionService: loginctl capability not available in DMS"); console.log("SessionService: loginctl capability not available in DMS");
} }
if (DMSService.capabilities.includes("dbus")) {
ensurePrepareForSleepSubscription();
} else {
clearPrepareForSleepSubscriptionState();
}
}
function clearPrepareForSleepSubscriptionState() {
prepareForSleepSubscriptionId = "";
prepareForSleepSubscriptionPending = false;
}
function ensurePrepareForSleepSubscription() {
if (!DMSService.isConnected || !DMSService.capabilities.includes("dbus")) {
return;
}
if (prepareForSleepSubscriptionId || prepareForSleepSubscriptionPending) {
return;
}
prepareForSleepSubscriptionPending = true;
DMSService.dbusSubscribe("system", "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "PrepareForSleep", response => {
prepareForSleepSubscriptionPending = false;
if (response.error) {
console.warn("SessionService: Failed to subscribe to PrepareForSleep:", response.error);
return;
}
prepareForSleepSubscriptionId = response.result?.subscriptionId || "";
});
}
function emitSessionResumedOnce() {
const now = Date.now();
if ((now - lastResumeSignalTimestamp) < 1000) {
return;
}
lastResumeSignalTimestamp = now;
sessionResumed();
}
function handlePrepareForSleepSignal(data) {
if (!data?.body || data.body.length === 0) {
return;
}
const wasSleeping = preparingForSleep;
preparingForSleep = data.body[0] === true;
if (wasSleeping && !preparingForSleep) {
emitSessionResumedOnce();
}
} }
function getLoginctlState() { function getLoginctlState() {
@@ -604,21 +667,9 @@ Singleton {
} }
if (wasSleeping && !preparingForSleep) { if (wasSleeping && !preparingForSleep) {
sessionResumed(); emitSessionResumedOnce();
} }
loginctlStateChanged(); loginctlStateChanged();
} }
function handleLoginctlEvent(event) {
if (event.event === "Lock") {
locked = true;
lockedHint = true;
sessionLocked();
} else if (event.event === "Unlock") {
locked = false;
lockedHint = false;
sessionUnlocked();
}
}
} }