diff --git a/dank-qml-common b/dank-qml-common index acc16579a..44058322a 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit acc16579a112fd1f0c65e7ad297962682cd6b22a +Subproject commit 44058322a4d3261293fa0f8d34933b1256cd5ba0 diff --git a/flake.lock b/flake.lock index b6f0cfae8..dda2b9130 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1791213890, - "narHash": "sha256-zVQmB7Bv4r7jeoWOOoMsOziqQTO9Owab0xD4pacRXQQ=", + "lastModified": 1791221960, + "narHash": "sha256-TWWR8Gv0ZBT/+byucBMNKdcjotsymgIBysE2qn2sces=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "acc16579a112fd1f0c65e7ad297962682cd6b22a", + "rev": "44058322a4d3261293fa0f8d34933b1256cd5ba0", "type": "github" }, "original": { diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index 1fab3ebc1..93a88305c 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -294,6 +294,7 @@ Singleton { property var lockScreenWidgetGridSettings: ({}) property var desktopWidgetInstancePositions: ({}) property var lockScreenAutoPositions: ({}) + property var greeterAutoPositions: ({}) property var islandFreePositions: ({}) property var builtInPluginState: ({}) property bool greeterSyncPending: false @@ -657,6 +658,14 @@ Singleton { saveSettings(); } + function setGreeterAutoPositions(screenKey, placement) { + if (JSON.stringify(greeterAutoPositions[screenKey] ?? null) === JSON.stringify(placement)) + return; + const updated = Object.assign({}, greeterAutoPositions); + updated[screenKey] = placement; + greeterAutoPositions = updated; + saveSettings(); + } function setLockScreenAutoPositions(screenKey, placement) { if (JSON.stringify(lockScreenAutoPositions[screenKey] ?? null) === JSON.stringify(placement)) return; @@ -666,6 +675,43 @@ Singleton { saveSettings(); } + function copyDesktopWidgetInstancePositions(fromId, toId) { + const source = desktopWidgetInstancePositions[fromId]; + if (!source) + return; + const updated = JSON.parse(JSON.stringify(desktopWidgetInstancePositions)); + updated[toId] = JSON.parse(JSON.stringify(source)); + desktopWidgetInstancePositions = updated; + saveSettings(); + } + // Detached greeter clocks keep the spot the lock screen picked for them. + function pinPublishedLockPosition(fromId, toId, synced) { + const updated = JSON.parse(JSON.stringify(desktopWidgetInstancePositions)); + let pinned = false; + for (const screenKey in lockScreenAutoPositions) { + const entry = lockScreenAutoPositions[screenKey]; + const position = entry?.positions?.[fromId]; + if (!position || !(entry.width > 0) || !(entry.height > 0)) + continue; + const key = synced ? "_synced" : screenKey; + if (!updated[toId]) + updated[toId] = {}; + updated[toId][key] = Object.assign({}, updated[toId][key] || {}, synced ? { + x: position.x / entry.width, + y: position.y / entry.height + } : { + x: position.x, + y: position.y + }); + pinned = true; + if (synced) + break; + } + if (!pinned) + return; + desktopWidgetInstancePositions = updated; + saveSettings(); + } function removeDesktopWidgetInstancePositions(instanceId) { if (!(instanceId in desktopWidgetInstancePositions)) return; diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index fa14800a0..e36de74e7 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -1113,13 +1113,15 @@ Singleton { property var desktopWidgetInstances: Spec.SPEC.desktopWidgetInstances.def property var desktopWidgetGroups: Spec.SPEC.desktopWidgetGroups.def property var lockScreenWidgetInstances: Spec.SPEC.lockScreenWidgetInstances.def - readonly property var widgetInstanceListKeys: ["desktopWidgetInstances", "lockScreenWidgetInstances"] + property var greeterWidgetInstances: Spec.SPEC.greeterWidgetInstances.def + property bool greeterFollowLockScreen: Spec.SPEC.greeterFollowLockScreen.def + readonly property var widgetInstanceListKeys: ["desktopWidgetInstances", "lockScreenWidgetInstances", "greeterWidgetInstances"] - // The greeter still reads these three shared keys, so they follow the lock widgets. + // Released greeters still read these three shared keys, so they follow the lock widgets. onLockScreenWidgetInstancesChanged: { - const status = lockWidgetInstance("lockStatus"); - const auth = lockWidgetInstance("lockAuth"); - const power = lockWidgetInstance("lockPower"); + const status = widgetInstanceOfType("lockScreenWidgetInstances", "lockStatus"); + const auth = widgetInstanceOfType("lockScreenWidgetInstances", "lockAuth"); + const power = widgetInstanceOfType("lockScreenWidgetInstances", "lockPower"); const mirror = (key, value) => { if (root[key] !== value) set(key, value); @@ -1127,10 +1129,52 @@ Singleton { mirror("lockScreenShowWeather", !!status && status.enabled !== false && (status.config?.showWeather ?? true)); mirror("lockScreenShowProfileImage", !!auth && (auth.config?.showProfileImage ?? true)); mirror("lockScreenShowPowerActions", !!power && power.enabled !== false); + syncGreeterWidgets(); + } + + function widgetInstanceOfType(listKey, widgetType) { + return (root[listKey] || []).find(inst => inst.widgetType === widgetType) ?? null; } function lockWidgetInstance(widgetType) { - return (lockScreenWidgetInstances || []).find(inst => inst.widgetType === widgetType) ?? null; + return widgetInstanceOfType("lockScreenWidgetInstances", widgetType); + } + + function syncGreeterWidgets() { + if (!greeterFollowLockScreen) + return; + const next = Spec.greeterWidgetsFromLock(lockScreenWidgetInstances, greeterWidgetInstances); + if (JSON.stringify(next) === JSON.stringify(greeterWidgetInstances)) + return; + set("greeterWidgetInstances", next); + } + + function setGreeterFollowLockScreen(follow) { + if (follow === greeterFollowLockScreen) + return; + if (follow) { + for (const inst of greeterWidgetInstances || []) { + if (inst.id.startsWith("gw_")) + SessionData.removeDesktopWidgetInstancePositions(inst.id); + } + set("greeterFollowLockScreen", true); + syncGreeterWidgets(); + return; + } + const detached = (greeterWidgetInstances || []).map(inst => { + if (inst.widgetType === "greeterSession") + return inst; + const copy = JSON.parse(JSON.stringify(inst)); + copy.id = "gw_" + inst.id; + SessionData.copyDesktopWidgetInstancePositions(inst.id, copy.id); + if (inst.widgetType !== "desktopClock" || inst.config?.autoPosition === false) + return copy; + copy.config.autoPosition = false; + SessionData.pinPublishedLockPosition(inst.id, copy.id, inst.config?.syncPositionAcrossScreens ?? false); + return copy; + }); + set("greeterWidgetInstances", detached); + set("greeterFollowLockScreen", false); } function resetLockScreenWidgets() { @@ -1141,6 +1185,22 @@ Singleton { resetToDefault(["lockScreenWidgetInstances"]); } + // Following means the lock layout is the greeter layout, so that is what resets. + function resetGreeterWidgets() { + for (const inst of greeterWidgetInstances || []) { + if (!greeterFollowLockScreen || inst.widgetType === "greeterSession") + SessionData.removeDesktopWidgetInstancePositions(inst.id); + } + if (greeterFollowLockScreen) { + resetLockScreenWidgets(); + set("greeterWidgetInstances", Spec.greeterWidgetsFromLock(lockScreenWidgetInstances, [])); + return; + } + set("greeterWidgetInstances", Spec.greeterWidgetDefaults().map(inst => inst.widgetType === "greeterSession" ? inst : Object.assign(inst, { + id: "gw_" + inst.id + }))); + } + function getDefaultSystemMonitorConfig() { return { showHeader: true, @@ -1174,10 +1234,16 @@ Singleton { return widgetInstanceListKeys.find(key => (root[key] || []).some(inst => inst.id === instanceId)) ?? "desktopWidgetInstances"; } + readonly property var widgetInstanceIdPrefixes: ({ + desktopWidgetInstances: "dw_", + lockScreenWidgetInstances: "lw_", + greeterWidgetInstances: "gw_" + }) + function createDesktopWidgetInstance(widgetType, name, config, listKey = "desktopWidgetInstances") { - const lockScreen = listKey === "lockScreenWidgetInstances"; + const lockScreen = listKey !== "desktopWidgetInstances"; const instance = { - id: (lockScreen ? "lw_" : "dw_") + Date.now() + "_" + Math.random().toString(36).substr(2, 9), + id: widgetInstanceIdPrefixes[listKey] + Date.now() + "_" + Math.random().toString(36).substr(2, 9), widgetType: widgetType, name: name || widgetType, enabled: true, @@ -1566,6 +1632,22 @@ Singleton { SessionData.saveSettings(); } + // Older builds flagged keys the linked slot now serves live; drop them so Apply does not nag forever. + function pruneGreeterSyncPending() { + const baseline = SessionData.greeterSyncBaseline || {}; + const keys = Object.keys(baseline); + const live = keys.filter(key => Spec.SPEC[key]?.onChange === "markGreeterSyncPending"); + if (live.length === keys.length) + return; + const pruned = {}; + for (const key of live) + pruned[key] = baseline[key]; + SessionData.greeterSyncBaseline = pruned; + if (live.length === 0) + SessionData.greeterSyncPending = false; + SessionData.saveSettings(); + } + function clearGreeterSyncPending() { SessionData.greeterSyncBaseline = {}; SessionData.greeterSyncPending = false; @@ -1778,6 +1860,7 @@ Singleton { function _mergeSessionState() { if (!_hasLoaded || !SessionData._hasLoaded) return; + pruneGreeterSyncPending(); const pluginState = SessionData.builtInPluginState || {}; if (Object.keys(pluginState).length > 0) { diff --git a/quickshell/Common/settings/Processes.qml b/quickshell/Common/settings/Processes.qml index 47f0a6d57..3a3c01f6b 100644 --- a/quickshell/Common/settings/Processes.qml +++ b/quickshell/Common/settings/Processes.qml @@ -369,7 +369,7 @@ Singleton { function deferGreeterAutoLoginSyncToPill(details) { toastCategoryDismissed("greeter-autologin-sync"); SessionData.set("greeterSyncPending", true); - toastRequested(1, I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync"); + toastRequested(1, I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Press Apply changes in Settings → Greeter.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync"); finishGreeterAutoLoginSync(); } diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 4aae0c632..22b0de867 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -7,6 +7,18 @@ function lockWidgetDefaults() { return Shared.lockWidgetDefaults(); } +function greeterWidgetsFromLock(lockInstances, previous) { + return Shared.greeterWidgetsFromLock(lockInstances, previous); +} + +function greeterSessionDefault() { + return Shared.greeterSessionDefault(); +} + +function greeterWidgetDefaults() { + return Shared.greeterWidgetDefaults(); +} + var LOCAL_SPEC = { dockConfigs: { def: [DockConfig.create("dock", "Dock")] @@ -645,6 +657,9 @@ var LOCAL_SPEC = { def: false, onChange: "scheduleGreeterAutoLoginSync" }, + greeterFollowLockScreen: { + def: true + }, greeterPamExternallyManaged: { def: false, onChange: "markGreeterSyncPending" diff --git a/quickshell/Modules/BuiltinDesktopPlugins/DesktopClockWidget.qml b/quickshell/Modules/BuiltinDesktopPlugins/DesktopClockWidget.qml index fb38bdb56..1e85933c0 100644 --- a/quickshell/Modules/BuiltinDesktopPlugins/DesktopClockWidget.qml +++ b/quickshell/Modules/BuiltinDesktopPlugins/DesktopClockWidget.qml @@ -3,150 +3,25 @@ import Quickshell import qs.Common import qs.Services import qs.Widgets -import "../../DankCommon/Common/Hct.js" as Hct -Item { +DankClockWidget { id: root property real widgetWidth: 280 property real widgetHeight: 200 - property string instanceId: "" property var instanceData: null - property bool lockScreen: false property var lockHost: null - readonly property var cfg: instanceData?.config ?? ({}) - - readonly property string face: { - switch (cfg.style ?? "analog") { - case "analog": - case "stacked": - case "expressive": - case "overlap": - return cfg.style; - } - return "digital"; - } - readonly property bool analog: face === "analog" - readonly property bool resizable: true - property bool forceSquare: analog - readonly property real defaultScale: lockScreen ? 1.75 : 1 - - function baseWidth() { - switch (face) { - case "analog": - return 200; - case "stacked": - return 100; - case "overlap": - return 160; - case "expressive": - return 240; - } - return 160; - } - - function baseHeight() { - switch (face) { - case "analog": - case "overlap": - return 200; - case "stacked": - return 160; - case "expressive": - return 120; - } - return 70; - } - - property real defaultWidth: baseWidth() * defaultScale - property real defaultHeight: baseHeight() * defaultScale - property real minWidth: { - switch (face) { - case "analog": - return 120; - case "stacked": - return 70; - case "overlap": - return 80; - } - return 100; - } - property real minHeight: { - switch (face) { - case "analog": - return 120; - case "stacked": - case "overlap": - return 100; - } - return 45; - } + cfg: instanceData?.config ?? ({}) enabled: instanceData?.enabled ?? true - readonly property real transparency: cfg.transparency ?? (lockScreen ? 0 : 0.8) - readonly property string colorMode: cfg.colorMode ?? (lockScreen ? "default" : "primary") - readonly property color customColor: cfg.customColor ?? "#ffffff" - readonly property bool showDate: cfg.showDate ?? !lockScreen - readonly property bool twoTone: cfg.twoTone ?? lockScreen - readonly property bool showAnalogNumbers: cfg.showAnalogNumbers ?? false - readonly property bool showAnalogSeconds: cfg.showAnalogSeconds ?? true - readonly property bool showDigitalSeconds: cfg.showDigitalSeconds ?? false - readonly property bool italic: face === "digital" && (cfg.italic ?? false) - readonly property string fontFamily: cfg.fontFamily || (lockScreen ? SettingsData.lockScreenFontFamily : "") - readonly property int weight: cfg.weight ?? 0 - readonly property int digitWeight: weight > 0 ? weight : Theme.fontWeightMedium - - readonly property color accentColor: { - if (lockHost) - return lockHost.contentColor(colorMode, customColor); - switch (colorMode) { - case "secondary": - return Theme.secondary; - case "custom": - return customColor; - } - return Theme.primary; - } - readonly property color minutesColor: twoTone ? Theme.primary : accentColor - readonly property color dimColor: Theme.withAlpha(accentColor, 0.65) - readonly property color backgroundColor: Theme.withAlpha(Theme.hostSurface, transparency) - readonly property bool themedOverlap: lockScreen && colorMode === "default" - readonly property var primaryHct: Hct.toHct(Theme.primary) - readonly property color overlapFaceColor: { - if (!themedOverlap) - return accentColor; - return Theme.isLightMode ? Hct.fromHct(primaryHct.hue, primaryHct.chroma, 65) : Theme.accentOnPrimaryContainer; - } - readonly property color overlapShadowColor: { - if (themedOverlap) - return Theme.isLightMode ? Hct.fromHct(primaryHct.hue, primaryHct.chroma, 30) : Theme.primaryContainer; - const source = Hct.toHct(accentColor); - return Hct.fromHct(source.hue, source.chroma, source.tone > 50 ? 30 : 80); - } - readonly property var contrastColors: { - switch (face) { - case "overlap": - return [overlapFaceColor, overlapShadowColor]; - case "digital": - case "stacked": - case "expressive": - return [accentColor, minutesColor]; - } - return [accentColor]; - } - - readonly property bool needsSeconds: analog ? showAnalogSeconds : showDigitalSeconds - readonly property string hoursText: { - const hours = systemClock.date?.getHours() ?? 0; - const display = SettingsData.use24HourClock ? hours : (hours % 12 || 12); - return SettingsData.use24HourClock || SettingsData.padHours12Hour ? String(display).padStart(2, "0") : String(display); - } - readonly property string minutesText: String(systemClock.date?.getMinutes() ?? 0).padStart(2, "0") - readonly property string secondsText: String(systemClock.date?.getSeconds() ?? 0).padStart(2, "0") - readonly property string meridiem: (systemClock.date?.getHours() ?? 0) >= 12 ? "PM" : "AM" - readonly property string dateFormat: lockScreen ? SettingsData.lockDateFormat : SettingsData.clockDateFormat - readonly property string formattedDate: systemClock.date?.toLocaleDateString(I18n.locale(), dateFormat || "ddd, MMM d") ?? "" + now: systemClock.date + locale: I18n.locale() + use24HourClock: SettingsData.use24HourClock + padHours12Hour: SettingsData.padHours12Hour + dateFormat: lockScreen ? SettingsData.lockDateFormat : SettingsData.clockDateFormat + fallbackFontFamily: SettingsData.lockScreenFontFamily + accentColor: lockHost ? lockHost.contentColor(colorMode, customColor) : themeAccent SystemClock { id: systemClock @@ -161,277 +36,4 @@ Item { systemClock.enabled = true; } } - - component DigitText: StyledText { - font.weight: root.digitWeight - font.italic: root.italic - font.family: root.fontFamily !== "" ? root.fontFamily : resolvedFontFamily - font.features: ({ - "tnum": 1 - }) - color: root.accentColor - horizontalAlignment: Text.AlignHCenter - } - - Rectangle { - anchors.fill: parent - radius: Theme.cornerRadius - color: root.backgroundColor - visible: !root.analog - } - - Loader { - anchors.fill: parent - anchors.margins: root.analog ? 0 : Theme.spacingM - sourceComponent: { - switch (root.face) { - case "analog": - return analogClock; - case "stacked": - return stackedClock; - case "expressive": - return expressiveClock; - case "overlap": - return overlapClock; - } - return digitalClock; - } - } - - Component { - id: analogClock - - DankAnalogClock { - hours: systemClock.date?.getHours() ?? 0 - minutes: systemClock.date?.getMinutes() ?? 0 - seconds: systemClock.date?.getSeconds() ?? 0 - showSeconds: root.showAnalogSeconds - showNumbers: root.showAnalogNumbers - dateText: root.showDate ? root.formattedDate : "" - color: root.accentColor - backgroundColor: root.backgroundColor - } - } - - Component { - id: expressiveClock - - DankClockFace { - readonly property bool tallBox: height > width * 0.6 - - hours: root.hoursText - minutes: root.minutesText - seconds: root.showDigitalSeconds && !tallBox ? root.secondsText : "" - dateText: root.showDate ? root.formattedDate : "" - stacked: tallBox - color: root.accentColor - minutesColor: root.minutesColor - supportingColor: root.dimColor - } - } - - Component { - id: overlapClock - - Item { - DankOverlapClockFace { - anchors.centerIn: parent - width: implicitWidth - height: implicitHeight - hours: root.hoursText - minutes: root.minutesText - color: root.overlapFaceColor - shadowColor: root.overlapShadowColor - fontFamily: root.fontFamily - weight: root.weight > 0 ? root.weight : 1000 - scale: implicitWidth > 0 && implicitHeight > 0 ? Math.min(parent.width / implicitWidth, parent.height / implicitHeight) : 1 - renderScale: scale - } - } - } - - Component { - id: digitalClock - - Item { - id: digitalRoot - - readonly property bool hasAmPm: !SettingsData.use24HourClock - readonly property real verticalScale: root.showDate && hasAmPm ? 0.55 : (root.showDate || hasAmPm ? 0.65 : 0.8) - readonly property real baseSize: Math.min(height * verticalScale, width * 0.22) - readonly property real digitWidth: baseSize * 0.62 - readonly property real smallSize: baseSize * 0.35 - - Column { - anchors.centerIn: parent - spacing: 0 - - DigitText { - visible: root.showDate - anchors.horizontalCenter: parent.horizontalCenter - text: root.formattedDate - font.pixelSize: digitalRoot.smallSize - color: root.dimColor - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - spacing: 0 - - Repeater { - model: root.hoursText.split("") - - DigitText { - required property string modelData - width: digitalRoot.digitWidth - text: modelData - font.pixelSize: digitalRoot.baseSize - } - } - - DigitText { - text: ":" - font.pixelSize: digitalRoot.baseSize - } - - Repeater { - model: root.minutesText.split("") - - DigitText { - required property string modelData - width: digitalRoot.digitWidth - text: modelData - font.pixelSize: digitalRoot.baseSize - color: root.minutesColor - } - } - - DigitText { - visible: root.showDigitalSeconds - text: ":" - font.pixelSize: digitalRoot.baseSize - color: root.dimColor - } - - Repeater { - model: root.showDigitalSeconds ? root.secondsText.split("") : [] - - DigitText { - required property string modelData - width: digitalRoot.digitWidth - text: modelData - font.pixelSize: digitalRoot.baseSize - color: root.dimColor - } - } - } - - DigitText { - visible: digitalRoot.hasAmPm - anchors.horizontalCenter: parent.horizontalCenter - text: root.meridiem - font.pixelSize: digitalRoot.smallSize - color: root.dimColor - } - } - } - } - - Component { - id: stackedClock - - Item { - id: stackedRoot - - readonly property bool hasAmPm: !SettingsData.use24HourClock - readonly property real extraContent: (root.showDigitalSeconds ? 0.12 : 0) + (root.showDate ? 0.08 : 0) + (hasAmPm ? 0.08 : 0) - readonly property real baseSize: height * (0.42 - extraContent * 0.5) - readonly property real digitWidth: baseSize * 0.58 - readonly property real smallSize: baseSize * 0.5 - readonly property real rowSpacing: -baseSize * 0.17 - - Column { - anchors.centerIn: parent - spacing: 0 - - Column { - spacing: stackedRoot.rowSpacing - anchors.horizontalCenter: parent.horizontalCenter - - Row { - spacing: 0 - anchors.horizontalCenter: parent.horizontalCenter - - Repeater { - model: root.hoursText.padStart(2, "0").split("") - - DigitText { - required property string modelData - width: stackedRoot.digitWidth - text: modelData - font.pixelSize: stackedRoot.baseSize - } - } - } - - Row { - spacing: 0 - anchors.horizontalCenter: parent.horizontalCenter - - Repeater { - model: root.minutesText.split("") - - DigitText { - required property string modelData - width: stackedRoot.digitWidth - text: modelData - font.pixelSize: stackedRoot.baseSize - color: root.minutesColor - } - } - } - } - - Row { - visible: root.showDigitalSeconds - spacing: 0 - anchors.horizontalCenter: parent.horizontalCenter - - Repeater { - model: root.showDigitalSeconds ? root.secondsText.split("") : [] - - DigitText { - required property string modelData - width: stackedRoot.smallSize * 0.58 - text: modelData - font.pixelSize: stackedRoot.smallSize - color: root.dimColor - } - } - } - - Item { - width: 1 - height: stackedRoot.baseSize * 0.1 - visible: root.showDate - } - - DigitText { - visible: root.showDate - anchors.horizontalCenter: parent.horizontalCenter - text: systemClock.date?.toLocaleDateString(I18n.locale(), "MMM dd") ?? "" - font.pixelSize: stackedRoot.smallSize * 0.7 - color: root.dimColor - } - - DigitText { - visible: stackedRoot.hasAmPm - anchors.horizontalCenter: parent.horizontalCenter - text: root.meridiem - font.pixelSize: stackedRoot.smallSize * 0.7 - color: root.dimColor - } - } - } - } } diff --git a/quickshell/Modules/Lock/Lock.qml b/quickshell/Modules/Lock/Lock.qml index b873aa9d2..398013dc3 100644 --- a/quickshell/Modules/Lock/Lock.qml +++ b/quickshell/Modules/Lock/Lock.qml @@ -242,6 +242,12 @@ Scope { demoWindow.showDemo(); } + function onGreeterEditorRequested() { + if (shouldLock) + return; + demoWindow.showDemo(true); + } + function onLoginctlStateChanged() { if (SessionService.active && pendingLock) { pendingLock = false; diff --git a/quickshell/Modules/Lock/LockScreenContent.qml b/quickshell/Modules/Lock/LockScreenContent.qml index d9853b2b4..4b0e33220 100644 --- a/quickshell/Modules/Lock/LockScreenContent.qml +++ b/quickshell/Modules/Lock/LockScreenContent.qml @@ -21,6 +21,7 @@ FocusScope { inputRevealRequested(revealed); } property bool demoMode: false + property bool greeterPreview: false property var pam: demoPam property string screenName: "" property bool unlocking: false @@ -155,6 +156,7 @@ FocusScope { focus: true screenName: root.screenName lockScreen: true + greeter: root.greeterPreview lockHost: root editMode: root.demoMode bottomInset: editorLoader.item?.fabReserved ?? 0 diff --git a/quickshell/Modules/Lock/LockScreenDemo.qml b/quickshell/Modules/Lock/LockScreenDemo.qml index f9f98aff7..604600f04 100644 --- a/quickshell/Modules/Lock/LockScreenDemo.qml +++ b/quickshell/Modules/Lock/LockScreenDemo.qml @@ -10,9 +10,11 @@ Scope { readonly property var log: Log.scoped("LockScreenDemo") property bool demoActive: false + property bool greeterPreview: false - function showDemo(): void { - log.debug("Showing lock screen demo"); + function showDemo(greeter = false): void { + log.debug("Showing lock screen demo", greeter ? "(greeter)" : ""); + greeterPreview = greeter; demoActive = true; } @@ -49,6 +51,7 @@ Scope { LockScreenContent { anchors.fill: parent demoMode: true + greeterPreview: root.greeterPreview screenName: demoWindow.screen?.name ?? "" onUnlockRequested: root.hideDemo() } diff --git a/quickshell/Modules/Lock/Widgets/GreeterSessionWidget.qml b/quickshell/Modules/Lock/Widgets/GreeterSessionWidget.qml new file mode 100644 index 000000000..fe14dc4f3 --- /dev/null +++ b/quickshell/Modules/Lock/Widgets/GreeterSessionWidget.qml @@ -0,0 +1,39 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.DankCommon.Session + +// Settings preview of the greeter session picker. The greeter renders the real dropdown. +Item { + id: root + + property var instanceData: null + property var lockHost: null + readonly property bool resizable: false + readonly property real minWidth: implicitWidth + readonly property real minHeight: implicitHeight + readonly property string sessionName: CompositorService.displayName || I18n.tr("Session", "greeter session picker widget") + + implicitWidth: Math.max(Theme.fieldDefaultWidth, sessionMetrics.width + Theme.buttonHeightM + Theme.spacingXL) + implicitHeight: LockMetrics.fieldHeight + + StyledTextMetrics { + id: sessionMetrics + text: root.sessionName + } + + DankDropdown { + anchors.fill: parent + text: "" + description: "" + backgroundColor: Theme.cardSurface + hoverBackgroundColor: Theme.blend(Theme.cardSurface, Theme.onSurface, Theme.stateLayerHover) + normalBorderColor: Theme.outlineMedium + currentValue: root.sessionName + options: [root.sessionName] + popupWidthOffset: 0 + openUpwards: true + alignPopupRight: true + } +} diff --git a/quickshell/Modules/Lock/Widgets/LockStatusWidget.qml b/quickshell/Modules/Lock/Widgets/LockStatusWidget.qml index d5cb87501..bf1f8b2bc 100644 --- a/quickshell/Modules/Lock/Widgets/LockStatusWidget.qml +++ b/quickshell/Modules/Lock/Widgets/LockStatusWidget.qml @@ -32,7 +32,7 @@ Item { anchors.right: parent.right anchors.margins: root.pad interactive: !(root.lockHost?.demoMode ?? true) - showMediaPlayer: root.cfg.showMediaPlayer ?? true + showMediaPlayer: (root.cfg.showMediaPlayer ?? true) && !(root.lockHost?.greeterPreview ?? false) showWeather: root.cfg.showWeather ?? true useFahrenheit: SettingsData.useFahrenheit } diff --git a/quickshell/Modules/Plugins/DesktopWidgetContent.qml b/quickshell/Modules/Plugins/DesktopWidgetContent.qml index 720e07097..8be21e092 100644 --- a/quickshell/Modules/Plugins/DesktopWidgetContent.qml +++ b/quickshell/Modules/Plugins/DesktopWidgetContent.qml @@ -33,6 +33,8 @@ Loader { return lockStatusComponent; case "lockPower": return lockPowerComponent; + case "greeterSession": + return greeterSessionComponent; } return PluginService.pluginDesktopComponents[pluginId] ?? null; } @@ -72,6 +74,10 @@ Loader { LockPowerWidget {} } + property Component greeterSessionComponent: Component { + GreeterSessionWidget {} + } + onInstanceDataChanged: { if (!item || item.instanceData === undefined) return; diff --git a/quickshell/Modules/Plugins/WidgetEditItem.qml b/quickshell/Modules/Plugins/WidgetEditItem.qml index bb201cfad..1a45b3ca2 100644 --- a/quickshell/Modules/Plugins/WidgetEditItem.qml +++ b/quickshell/Modules/Plugins/WidgetEditItem.qml @@ -14,7 +14,7 @@ Item { readonly property string instanceId: instanceData?.id ?? "" readonly property string widgetType: instanceData?.widgetType ?? "" readonly property bool lockScreen: hostLayer.lockScreen - readonly property bool removable: widgetType !== "lockAuth" + readonly property bool removable: widgetType !== "lockAuth" && widgetType !== "greeterSession" readonly property bool selected: hostLayer.selectedInstanceId === instanceId readonly property bool interacting: editChrome.item?.interacting ?? false readonly property var contrastColors: content.item?.contrastColors ?? [] diff --git a/quickshell/Modules/Plugins/WidgetEditLayer.qml b/quickshell/Modules/Plugins/WidgetEditLayer.qml index 1f5123026..dbcbc2d2c 100644 --- a/quickshell/Modules/Plugins/WidgetEditLayer.qml +++ b/quickshell/Modules/Plugins/WidgetEditLayer.qml @@ -12,13 +12,14 @@ FocusScope { required property string screenName property bool lockScreen: false + property bool greeter: false property var lockHost: null property bool editMode: false property string selectedInstanceId: "" readonly property var screen: Quickshell.screens.find(s => s.name === screenName) ?? null - readonly property string listKey: lockScreen ? "lockScreenWidgetInstances" : "desktopWidgetInstances" - readonly property var instances: (lockScreen ? SettingsData.lockScreenWidgetInstances : SettingsData.desktopWidgetInstances) || [] + readonly property string listKey: greeter ? "greeterWidgetInstances" : (lockScreen ? "lockScreenWidgetInstances" : "desktopWidgetInstances") + readonly property var instances: (greeter ? SettingsData.greeterWidgetInstances : (lockScreen ? SettingsData.lockScreenWidgetInstances : SettingsData.desktopWidgetInstances)) || [] readonly property string screenKey: SettingsData.getScreenDisplayName(screen) property var _gridSettingsTrigger: lockScreen ? SessionData.lockScreenWidgetGridSettings : SessionData.desktopWidgetGridSettings readonly property int gridSize: { @@ -133,6 +134,17 @@ FocusScope { return LockPlacementService.layoutFor(screenName, LockPlacementService.sampleFor(screenName, backgroundKey), placementKey, placeClocks); } + // The greeter list never renders outside the editor, so the editor publishes its auto placement. + onAutoPositionsChanged: { + if (!greeter || !editMode || !completed || interactingItem || width <= 0 || height <= 0) + return; + SessionData.setGreeterAutoPositions(screenKey, { + width: width, + height: height, + positions: autoPositions + }); + } + function refreshPlacement() { if (!lockScreen || !completed || editMode || width <= 0 || height <= 0) return; @@ -204,6 +216,11 @@ FocusScope { x: Theme.spacingXL, y: height - Theme.spacingXL - item.height }; + case "greeterSession": + return { + x: width - Theme.spacingXL - item.width, + y: height - Theme.spacingXL - item.height + }; } return null; } diff --git a/quickshell/Modules/Plugins/WidgetEditorOverlay.qml b/quickshell/Modules/Plugins/WidgetEditorOverlay.qml index 820e2b149..d763f3927 100644 --- a/quickshell/Modules/Plugins/WidgetEditorOverlay.qml +++ b/quickshell/Modules/Plugins/WidgetEditorOverlay.qml @@ -14,12 +14,14 @@ FocusScope { required property WidgetEditLayer editLayer readonly property bool lockScreen: editLayer.lockScreen + readonly property bool greeter: editLayer.greeter property bool libraryOpen: false readonly property real fabReserved: fabBar.reservedHeight - readonly property var libraryWidgets: DesktopWidgetRegistry.registeredWidgetsList.filter(widget => { + readonly property var singletonTypes: ["lockAuth", "greeterSession"] + readonly property var libraryWidgets: DesktopWidgetRegistry.getListWidgets(editLayer.listKey).filter(widget => { if (!lockScreen) - return !widget.lockOnly; - return widget.id !== "lockAuth" || !SettingsData.lockWidgetInstance("lockAuth"); + return true; + return !singletonTypes.includes(widget.id) || !SettingsData.widgetInstanceOfType(editLayer.listKey, widget.id); }).map(widget => ({ id: widget.id, text: widget.name, @@ -27,6 +29,14 @@ FocusScope { description: widget.description })) + readonly property string surfaceLabel: { + if (greeter) + return SettingsData.greeterFollowLockScreen ? I18n.tr("Editing the login screen, shared with the lock screen", "widget editor banner") : I18n.tr("Editing the login screen", "widget editor banner"); + if (!lockScreen || !GreeterService.available) + return ""; + return SettingsData.greeterFollowLockScreen && GreeterService.slotState !== "" ? I18n.tr("Editing the lock screen, the login screen follows it", "widget editor banner") : I18n.tr("Editing the lock screen", "widget editor banner"); + } + signal finished function closeLibrary() { @@ -46,6 +56,10 @@ FocusScope { } function resetLayout() { + if (greeter) { + SettingsData.resetGreeterWidgets(); + return; + } if (lockScreen) { SettingsData.resetLockScreenWidgets(); return; @@ -119,7 +133,42 @@ FocusScope { } } + Rectangle { + visible: root.surfaceLabel !== "" && fabBar.shown + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: fabBar.reservedHeight + Theme.spacingL + (gridHint.visible ? gridHint.height + Theme.spacingS : 0) + width: surfaceRow.implicitWidth + Theme.spacingL * 2 + height: Theme.buttonHeightM + radius: Theme.fullRadius(width, height) + color: Theme.readableSurface + border.width: Theme.layerOutlineWidth + border.color: Theme.outlineMedium + + Row { + id: surfaceRow + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: root.greeter ? "login" : "lock" + size: Theme.iconSizeMedium + color: Theme.primary + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: root.surfaceLabel + font.pixelSize: Theme.fontSizeMedium + font.weight: Theme.fontWeightMedium + color: Theme.surfaceText + } + } + } + DesktopWidgetGridHint { + id: gridHint anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: fabBar.reservedHeight + Theme.spacingL diff --git a/quickshell/Modules/Settings/DesktopWidgetBrowser.qml b/quickshell/Modules/Settings/DesktopWidgetBrowser.qml index 67cc5dcd8..a89bb0d82 100644 --- a/quickshell/Modules/Settings/DesktopWidgetBrowser.qml +++ b/quickshell/Modules/Settings/DesktopWidgetBrowser.qml @@ -23,7 +23,7 @@ WidgetPickerWindow { objectName: "desktopWidgetBrowser" title: I18n.tr("Add Desktop Widget") - widgets: (DesktopWidgetRegistry.registeredWidgetsList || []).filter(widget => !widget.lockOnly) + widgets: DesktopWidgetRegistry.getListWidgets(listKey).filter(widget => listKey === "desktopWidgetInstances" ? true : (widget.id !== "lockAuth" && widget.id !== "greeterSession")) featuredFirst: true showEmptyState: true widgetDelegate: tileDelegate diff --git a/quickshell/Modules/Settings/DesktopWidgetSettings/ClockSettings.qml b/quickshell/Modules/Settings/DesktopWidgetSettings/ClockSettings.qml index 87381eba5..32f7d435c 100644 --- a/quickshell/Modules/Settings/DesktopWidgetSettings/ClockSettings.qml +++ b/quickshell/Modules/Settings/DesktopWidgetSettings/ClockSettings.qml @@ -41,6 +41,13 @@ DesktopWidgetInstanceSettings { onSliderValueChanged: newValue => root.updateConfig("weight", newValue) } + SettingsToggleRow { + visible: root.style === "expressive" + text: I18n.tr("Rounded") + checked: root.cfg.rounded ?? true + onToggled: checked => root.updateConfig("rounded", checked) + } + SettingsToggleRow { visible: root.style === "digital" text: I18n.tr("Italic", "clock font style toggle") diff --git a/quickshell/Modules/Settings/DesktopWidgetSettings/DesktopWidgetInstanceSettings.qml b/quickshell/Modules/Settings/DesktopWidgetSettings/DesktopWidgetInstanceSettings.qml index e54748b8f..315f83fd4 100644 --- a/quickshell/Modules/Settings/DesktopWidgetSettings/DesktopWidgetInstanceSettings.qml +++ b/quickshell/Modules/Settings/DesktopWidgetSettings/DesktopWidgetInstanceSettings.qml @@ -10,7 +10,9 @@ Column { property bool showAppearance: true property bool showPlacement: true readonly property var cfg: instanceData?.config ?? {} - readonly property bool lockScreenInstance: SettingsData.widgetInstanceListKey(instanceId) === "lockScreenWidgetInstances" + readonly property string listKey: SettingsData.widgetInstanceListKey(instanceId) + readonly property bool lockScreenInstance: listKey !== "desktopWidgetInstances" + readonly property bool greeterInstance: listKey === "greeterWidgetInstances" default property alias rows: optionsCard.content function updateConfig(key, value) { diff --git a/quickshell/Modules/Settings/DesktopWidgetSettings/DesktopWidgetTypeSettings.qml b/quickshell/Modules/Settings/DesktopWidgetSettings/DesktopWidgetTypeSettings.qml index 12f358428..90cbe12ab 100644 --- a/quickshell/Modules/Settings/DesktopWidgetSettings/DesktopWidgetTypeSettings.qml +++ b/quickshell/Modules/Settings/DesktopWidgetSettings/DesktopWidgetTypeSettings.qml @@ -27,6 +27,8 @@ Loader { return lockDateSettings; case "lockPower": return lockPowerSettings; + case "greeterSession": + return null; default: return pluginSettings; } diff --git a/quickshell/Modules/Settings/DesktopWidgetTab.qml b/quickshell/Modules/Settings/DesktopWidgetTab.qml index feae90d4c..e3af72646 100644 --- a/quickshell/Modules/Settings/DesktopWidgetTab.qml +++ b/quickshell/Modules/Settings/DesktopWidgetTab.qml @@ -14,7 +14,9 @@ Item { property var parentModal: null readonly property string instanceId: SettingsUiState.selectedDesktopWidgetId - readonly property bool lockScreenInstance: SettingsData.widgetInstanceListKey(instanceId) === "lockScreenWidgetInstances" + readonly property string listKey: SettingsData.widgetInstanceListKey(instanceId) + readonly property bool lockScreenInstance: listKey !== "desktopWidgetInstances" + readonly property bool greeterInstance: listKey === "greeterWidgetInstances" readonly property var instanceData: SettingsData.getDesktopWidgetInstance(instanceId) readonly property string widgetType: instanceData?.widgetType ?? "" readonly property var widgetDef: DesktopWidgetRegistry.getWidget(widgetType) diff --git a/quickshell/Modules/Settings/GreeterAuthTab.qml b/quickshell/Modules/Settings/GreeterAuthTab.qml index a124baa99..82f049e8e 100644 --- a/quickshell/Modules/Settings/GreeterAuthTab.qml +++ b/quickshell/Modules/Settings/GreeterAuthTab.qml @@ -20,7 +20,7 @@ Item { switch (SettingsData.greeterFingerprintReason) { case "ready": - return I18n.tr("Applies on the next greeter sync", "greeter auth setting description"); + return I18n.tr("Press Apply changes to update the login screen", "greeter auth setting description"); case "missing_enrollment": return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.", "greeter fingerprint login setting"); case "missing_reader": @@ -40,7 +40,7 @@ Item { switch (SettingsData.greeterU2fReason) { case "ready": - return I18n.tr("Applies on the next greeter sync", "greeter auth setting description"); + return I18n.tr("Press Apply changes to update the login screen", "greeter auth setting description"); case "missing_key_registration": return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status"); case "missing_pam_support": diff --git a/quickshell/Modules/Settings/GreeterSyncFabBar.qml b/quickshell/Modules/Settings/GreeterSyncFabBar.qml index 30094cc7f..d0ec531ea 100644 --- a/quickshell/Modules/Settings/GreeterSyncFabBar.qml +++ b/quickshell/Modules/Settings/GreeterSyncFabBar.qml @@ -12,15 +12,16 @@ SettingsFabBar { shown: SessionData.greeterSyncPending && GreeterService.binaryExists DankFab { - iconName: "close" + text: I18n.tr("Revert") + iconName: "undo" colorRole: "secondaryContainer" - Accessible.name: I18n.tr("Dismiss") + enabled: !GreeterService.syncing onClicked: SettingsData.revertGreeterSyncPending() } DankFab { - text: GreeterService.syncing ? I18n.tr("Syncing...", "greeter settings status while sync is running") : I18n.tr("Sync to apply") - iconName: "sync" + text: GreeterService.syncing ? I18n.tr("Syncing...", "greeter settings status while sync is running") : I18n.tr("Apply changes") + iconName: "check" colorRole: "primary" busy: GreeterService.syncing enabled: !GreeterService.syncing && !root.blocked diff --git a/quickshell/Modules/Settings/GreeterTab.qml b/quickshell/Modules/Settings/GreeterTab.qml index 7518a9d21..ac7b312d5 100644 --- a/quickshell/Modules/Settings/GreeterTab.qml +++ b/quickshell/Modules/Settings/GreeterTab.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Quickshell import Quickshell.Io import qs.Common import qs.Modals.Common @@ -20,6 +21,38 @@ Item { id: greeterActionConfirm } + ConfirmModal { + id: unlinkConfirm + } + + ConfirmModal { + id: unlinkFinalConfirm + } + + function promptUnlink() { + unlinkConfirm.showWithOptions({ + "title": I18n.tr("Unlink login screen", "greeter advanced action"), + "message": I18n.tr("The login screen stops following your DMS theme, wallpaper, widgets and settings. greetd, authentication and other users are not affected.", "greeter unlink confirmation"), + "confirmText": I18n.tr("Unlink", "verb, greeter unlink button"), + "cancelText": I18n.tr("Cancel"), + "confirmColor": Theme.error, + "onConfirm": () => Qt.callLater(root.promptUnlinkFinal), + "onCancel": () => {} + }); + } + + function promptUnlinkFinal() { + unlinkFinalConfirm.showWithOptions({ + "title": I18n.tr("Unlink now?", "greeter unlink second confirmation"), + "message": I18n.tr("Your greeter slot and any cache links to your home are removed. Administrator access may be required. Set up links them again.", "greeter unlink second confirmation"), + "confirmText": I18n.tr("Unlink", "verb, greeter unlink button"), + "cancelText": I18n.tr("Cancel"), + "confirmColor": Theme.error, + "onConfirm": () => GreeterService.unlink(), + "onCancel": () => {} + }); + } + property string greeterStatusText: "" property bool greeterStatusRunning: false readonly property bool greeterSyncRunning: GreeterService.syncing @@ -28,31 +61,93 @@ Item { property string greeterStatusStdout: "" property string greeterStatusStderr: "" readonly property bool greeterBinaryExists: GreeterService.binaryExists - property bool greeterEnabled: false + readonly property bool greeterEnabled: GreeterService.enabled property bool embeddedGreeterConfigured: false readonly property bool embeddedGreeterOnly: embeddedGreeterConfigured && !greeterBinaryExists - readonly property string greeterAction: greeterBinaryExists && !greeterEnabled ? "activate" : "" - readonly property bool greeterActionAvailable: greeterAction !== "" + readonly property bool busy: greeterSyncRunning || greeterInstallActionRunning - readonly property string greeterActionLabel: greeterAction === "activate" ? I18n.tr("Activate") : "" - readonly property string greeterActionIcon: greeterAction === "activate" ? "login" : "" - readonly property var greeterActionCommand: greeterAction === "activate" ? ["dms-greeter", "enable", "--terminal"] : [] - readonly property string greeterStatusOutput: { - if (greeterStatusRunning) - return I18n.tr("Checking...", "greeter status loading"); - if (greeterStatusText !== "") - return greeterStatusText; - if (embeddedGreeterOnly) + readonly property string greeterState: { + if (!greeterBinaryExists) + return embeddedGreeterOnly ? "embedded" : "missing"; + if (!greeterEnabled) + return "inactive"; + switch (GreeterService.slotState) { + case "live": + return "linked"; + case "snapshot": + return "snapshot"; + } + return "unlinked"; + } + readonly property bool greeterStateWarn: greeterState !== "linked" + readonly property string greeterStateIcon: { + switch (greeterState) { + case "linked": + return "link"; + case "snapshot": + return "history"; + case "inactive": + return "login"; + case "unlinked": + return "link_off"; + } + return "info"; + } + readonly property string greeterStateTitle: { + switch (greeterState) { + case "linked": + return I18n.tr("Linked", "greeter status, login screen follows DMS settings live"); + case "snapshot": + return I18n.tr("Linked as a snapshot", "greeter status, login screen reads a copy of DMS settings"); + case "inactive": + return I18n.tr("Not active", "greeter status, greetd does not run the DMS greeter"); + case "unlinked": + return I18n.tr("Not linked", "greeter status, no greeter slot for this user"); + case "embedded": + return I18n.tr("Bundled greeter active", "greeter status"); + } + return I18n.tr("Not installed", "greeter status"); + } + readonly property string greeterStateSubtitle: { + switch (greeterState) { + case "linked": + return I18n.tr("The login screen follows your theme, wallpaper, widgets and behavior live. Only authentication changes need applying.", "greeter status"); + case "snapshot": + return I18n.tr("This system cannot link settings live. Sync again after changing them.", "greeter status"); + case "inactive": + return I18n.tr("greetd is not using the DMS greeter yet.", "greeter status"); + case "unlinked": + return GreeterService.profileSyncSufficient ? I18n.tr("Link your DMS settings so the login screen follows them.", "greeter status") : I18n.tr("Link your DMS settings so the login screen follows them. Administrator access is required.", "greeter status"); + case "embedded": return I18n.tr("The greeter bundled with DMS is active (archinstall setup). It keeps working as is, but syncing theme and settings needs the standalone greeter. Install greetd-dms-greeter-bin from the AUR, then run Sync to migrate the login screen.", "embedded greeter status"); - if (!greeterBinaryExists && greeterEnabled) - return I18n.tr("dms-greeter is not installed. Install the dms-greeter package to manage the greeter.", "greeter status placeholder"); + } + return I18n.tr("dms-greeter is not installed. Install the dms-greeter package to manage the greeter.", "greeter status placeholder"); + } + readonly property string greeterStateAction: { + switch (greeterState) { + case "inactive": + return I18n.tr("Activate"); + case "unlinked": + return I18n.tr("Set up", "greeter setup button, links DMS settings to the login screen"); + case "snapshot": + return I18n.tr("Sync", "verb, button that copies settings to the login greeter"); + } return ""; } + function runStateAction() { + if (greeterState === "inactive") { + promptGreeterActionConfirm(); + return; + } + GreeterService.link(); + } + + readonly property string greeterStatusOutput: greeterStatusRunning ? I18n.tr("Checking...", "greeter status loading") : greeterStatusText + onGreeterSyncStatusChanged: greeterStatusText = greeterSyncStatus function checkGreeterInstallState() { - greetdEnabledCheckProcess.running = true; GreeterService.refresh(); embeddedGreeterCheckProcess.running = true; } @@ -66,15 +161,12 @@ Item { } function runGreeterInstallAction() { - greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "..."; + greeterStatusText = I18n.tr("Opening terminal: ") + I18n.tr("Activate") + "..."; greeterInstallActionRunning = true; greeterInstallActionProcess.running = true; } function promptGreeterActionConfirm() { - if (!root.greeterActionAvailable) - return; - greeterActionConfirm.showWithOptions({ "title": I18n.tr("Activate Greeter", "greeter action confirmation"), "message": I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings."), @@ -90,13 +182,20 @@ Item { Qt.callLater(checkGreeterInstallState); } - Process { - id: greetdEnabledCheckProcess - command: ["systemctl", "is-enabled", "greetd"] - running: false + function showWidgetBrowser() { + greeterWidgetBrowserLoader.active = true; + greeterWidgetBrowserLoader.item?.show(); + } - stdout: StdioCollector { - onStreamFinished: root.greeterEnabled = text.trim() === "enabled" + LazyLoader { + id: greeterWidgetBrowserLoader + active: false + + DesktopWidgetBrowser { + parentModal: root.parentModal + listKey: "greeterWidgetInstances" + title: I18n.tr("Add widget") + onWidgetAdded: ToastService.showInfo(I18n.tr("Widget added")) } } @@ -155,7 +254,7 @@ Item { Process { id: greeterInstallActionProcess - command: root.greeterActionCommand + command: ["dms-greeter", "enable", "--terminal"] running: false onExited: exitCode => { @@ -177,42 +276,21 @@ Item { iconName: "info" title: I18n.tr("Status") settingKey: "greeterStatus" + tags: ["greeter", "login", "sync", "status", "link"] SettingsRow { - subtitle: I18n.tr("Sync applies your theme and settings to the login screen. Shared users should run dms-greeter sync --profile instead of a primary user sync.") + iconName: root.greeterStateIcon + iconColor: root.greeterStateWarn ? Theme.warning : Theme.primary + title: root.greeterStateTitle + subtitle: root.greeterStateSubtitle - body: Flow { - width: parent.width - spacing: Theme.spacingS - layoutDirection: Qt.RightToLeft - - DankButton { - text: I18n.tr("Sync", "verb, button that copies settings to the login greeter") - iconName: "sync" - busy: root.greeterSyncRunning - enabled: root.greeterBinaryExists && !root.greeterSyncRunning && !root.greeterInstallActionRunning - onClicked: GreeterService.sync() - } - - DankButton { - text: I18n.tr("Check status", "greeter settings button, runs dms-greeter status") - iconName: "fact_check" - backgroundColor: Theme.secondaryContainer - textColor: Theme.onSecondaryContainer - busy: root.greeterStatusRunning - enabled: !root.greeterStatusRunning - onClicked: root.runGreeterStatus() - } - - DankButton { - visible: root.greeterActionAvailable - text: root.greeterActionLabel - iconName: root.greeterActionIcon - backgroundColor: Theme.secondaryContainer - textColor: Theme.onSecondaryContainer - enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning - onClicked: root.promptGreeterActionConfirm() - } + DankButton { + visible: root.greeterStateAction !== "" + anchors.verticalCenter: parent.verticalCenter + text: root.greeterStateAction + busy: root.greeterSyncRunning + enabled: !root.busy + onClicked: root.runStateAction() } } @@ -220,6 +298,7 @@ Item { visible: root.greeterStatusOutput !== "" noteIconName: "" monospace: true + maxHeight: SettingsMetrics.noteMaxHeight text: root.greeterStatusOutput tint: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText tintBackground: SettingsMetrics.controlColor @@ -236,6 +315,57 @@ Item { } } + SettingsCard { + width: parent.width + iconName: "widgets" + title: I18n.tr("Widgets") + settingKey: "greeterWidgets" + tags: ["greeter", "login", "widgets", "clock", "session", "layout"] + + SettingsToggleRow { + settingKey: "greeterFollowLockScreen" + tags: ["greeter", "login", "lock", "widgets", "layout", "follow"] + text: I18n.tr("Follow lock screen", "greeter widgets toggle") + description: I18n.tr("The login screen shows the lock screen widgets it supports and its own session picker. Turn off to arrange it separately.", "greeter widgets toggle") + checked: SettingsData.greeterFollowLockScreen + onToggled: checked => SettingsData.setGreeterFollowLockScreen(checked) + } + + SettingsReorderList { + id: greeterWidgetList + + model: SettingsData.greeterWidgetInstances || [] + + delegate: DesktopWidgetInstanceCard { + required property var modelData + + reorderList: greeterWidgetList + reorderEnabled: false + instanceData: modelData + fixed: SettingsData.greeterFollowLockScreen || modelData.widgetType === "lockAuth" || modelData.widgetType === "greeterSession" + + onConfigureRequested: { + SettingsUiState.selectedDesktopWidgetId = instanceId; + SettingsUiState.selectedWidgetTitle = widgetName; + root.parentModal?.navigateTo("desktop_widget"); + } + onDuplicateRequested: SettingsData.duplicateDesktopWidgetInstance(instanceId) + onDeleteRequested: { + SettingsData.removeDesktopWidgetInstance(instanceId); + ToastService.showInfo(I18n.tr("Widget removed")); + } + } + } + + SettingsRow { + iconName: "restart_alt" + title: I18n.tr("Reset to default") + visible: !SettingsData.greeterFollowLockScreen + clickable: true + onClicked: SettingsData.resetGreeterWidgets() + } + } + SettingsCard { title: I18n.tr("Appearance") settingKey: "greeterAppearance" @@ -297,6 +427,44 @@ Item { } } + SettingsCard { + width: parent.width + iconName: "tune" + title: I18n.tr("Advanced") + settingKey: "greeterAdvanced" + tags: ["greeter", "login", "sync", "status", "repair"] + collapsible: true + expanded: false + + SettingsRow { + iconName: "sync" + title: I18n.tr("Run full sync", "greeter advanced action") + subtitle: I18n.tr("Re-applies the greetd command, permissions and PAM authentication. Needs administrator access.", "greeter advanced action") + clickable: true + enabled: root.greeterBinaryExists && !root.busy + onClicked: GreeterService.sync() + } + + SettingsRow { + iconName: "fact_check" + title: I18n.tr("Check status", "greeter settings button, runs dms-greeter status") + clickable: true + enabled: root.greeterBinaryExists && !root.greeterStatusRunning + onClicked: root.runGreeterStatus() + } + + SettingsRow { + iconName: "link_off" + iconColor: Theme.error + title: I18n.tr("Unlink login screen", "greeter advanced action") + subtitle: I18n.tr("Stops syncing your DMS settings to the greeter. Set up links them again.", "greeter advanced action") + visible: GreeterService.slotState !== "" + clickable: true + enabled: root.greeterBinaryExists && !root.busy + onClicked: root.promptUnlink() + } + } + SettingsCard { width: parent.width iconName: "extension" @@ -336,8 +504,44 @@ Item { } } - GreeterSyncFabBar { - blocked: root.greeterInstallActionRunning + SettingsFabBar { + id: greeterFabs + + readonly property bool syncPending: SessionData.greeterSyncPending && GreeterService.binaryExists + + DankFab { + visible: greeterFabs.syncPending + text: I18n.tr("Revert") + iconName: "undo" + colorRole: "secondaryContainer" + enabled: !GreeterService.syncing + onClicked: SettingsData.revertGreeterSyncPending() + } + + DankFab { + visible: greeterFabs.syncPending + text: GreeterService.syncing ? I18n.tr("Syncing...", "greeter settings status while sync is running") : I18n.tr("Apply changes") + iconName: "check" + colorRole: "primary" + busy: GreeterService.syncing + enabled: !root.busy + onClicked: GreeterService.sync() + } + + DankFab { + visible: !greeterFabs.syncPending + text: I18n.tr("Edit widgets") + iconName: "edit" + colorRole: SettingsData.greeterFollowLockScreen ? "primary" : "secondaryContainer" + onClicked: SessionService.greeterEditorRequested() + } + + DankFab { + visible: !greeterFabs.syncPending && !SettingsData.greeterFollowLockScreen + text: I18n.tr("Add widget") + iconName: "add" + onClicked: root.showWidgetBrowser() + } } } } diff --git a/quickshell/Modules/Settings/LockWidgetSettings/LockStatusSettings.qml b/quickshell/Modules/Settings/LockWidgetSettings/LockStatusSettings.qml index 7852d8977..43fec5971 100644 --- a/quickshell/Modules/Settings/LockWidgetSettings/LockStatusSettings.qml +++ b/quickshell/Modules/Settings/LockWidgetSettings/LockStatusSettings.qml @@ -17,6 +17,7 @@ DesktopWidgetInstanceSettings { } SettingsToggleRow { + visible: !root.greeterInstance text: I18n.tr("Show media player") checked: root.cfg.showMediaPlayer ?? true onToggled: checked => root.updateConfig("showMediaPlayer", checked) diff --git a/quickshell/Services/DesktopWidgetRegistry.qml b/quickshell/Services/DesktopWidgetRegistry.qml index cc8336bf6..4db17022c 100644 --- a/quickshell/Services/DesktopWidgetRegistry.qml +++ b/quickshell/Services/DesktopWidgetRegistry.qml @@ -44,6 +44,7 @@ Singleton { type: "builtin", component: "qs.Modules.BuiltinDesktopPlugins.DesktopClockWidget", settingsComponent: "qs.Modules.Settings.DesktopWidgetSettings.ClockSettings", + greeter: true, defaultConfig: getDefaultClockConfig(), defaultSize: { width: 280, @@ -68,24 +69,40 @@ Singleton { } function registerLockBuiltins() { - const lockWidget = (id, name, icon) => registerWidget({ + const lockWidget = (id, name, icon, greeter) => registerWidget({ id: id, name: name, icon: icon, description: "", type: "builtin", lockOnly: true, + greeter: greeter, defaultConfig: Spec.SPEC.lockScreenWidgetInstances.def.find(inst => inst.widgetType === id)?.config ?? {}, defaultSize: { width: 200, height: 200 } }); - lockWidget("lockDate", I18n.tr("Date"), "event"); - lockWidget("lockAuth", I18n.tr("Password"), "lock"); - lockWidget("lockNotifications", I18n.tr("Notifications"), "notifications"); - lockWidget("lockStatus", I18n.tr("Status"), "wifi"); - lockWidget("lockPower", I18n.tr("Power"), "power_settings_new"); + lockWidget("lockDate", I18n.tr("Date"), "event", true); + lockWidget("lockAuth", I18n.tr("Password"), "lock", true); + lockWidget("lockNotifications", I18n.tr("Notifications"), "notifications", false); + lockWidget("lockStatus", I18n.tr("Status"), "wifi", true); + lockWidget("lockPower", I18n.tr("Power"), "power_settings_new", true); + registerWidget({ + id: "greeterSession", + name: I18n.tr("Session", "greeter session picker widget"), + icon: "login", + description: "", + type: "builtin", + lockOnly: true, + greeterOnly: true, + greeter: true, + defaultConfig: Spec.greeterSessionDefault().config, + defaultSize: { + width: 200, + height: 48 + } + }); } function getDefaultClockConfig() { @@ -255,6 +272,24 @@ Singleton { return registeredWidgetsList.filter(w => !w.lockOnly); } + function getLockWidgets() { + return registeredWidgetsList.filter(w => !w.greeterOnly); + } + + function getGreeterWidgets() { + return registeredWidgetsList.filter(w => w.greeter); + } + + function getListWidgets(listKey) { + switch (listKey) { + case "lockScreenWidgetInstances": + return getLockWidgets(); + case "greeterWidgetInstances": + return getGreeterWidgets(); + } + return getDesktopWidgets(); + } + function getPluginWidgets() { return registeredWidgetsList.filter(w => w.type === "plugin"); } diff --git a/quickshell/Services/GreeterService.qml b/quickshell/Services/GreeterService.qml index fcd26eabc..43dc7e19f 100644 --- a/quickshell/Services/GreeterService.qml +++ b/quickshell/Services/GreeterService.qml @@ -12,10 +12,18 @@ Singleton { property bool available: false property bool binaryExists: false + property bool configured: false + property bool enabled: false + property bool inGroup: false + // "live" when the greeter slot symlinks this user's DMS files, "snapshot" for a copied slot, "" when absent + property string slotState: "" property bool syncing: false property string syncStatus: "" property bool fallbackFromPrecheck: false + readonly property bool linked: slotState === "live" + readonly property bool profileSyncSufficient: configured && inGroup + signal syncFinished function refresh() { @@ -31,6 +39,19 @@ Singleton { sudoProbeProcess.running = true; } + // Links this user's slot without touching system config when the greeter is already installed. + function link() { + if (!profileSyncSufficient) { + sync(); + return; + } + if (!binaryExists || syncing) + return; + syncStatus = ""; + syncing = true; + profileSyncProcess.running = true; + } + function launchTerminalFallback(fromPrecheck, statusText) { fallbackFromPrecheck = fromPrecheck; if (statusText) @@ -38,6 +59,14 @@ Singleton { terminalFallbackProcess.running = true; } + function unlink() { + if (!binaryExists || syncing) + return; + syncStatus = ""; + syncing = true; + unlinkProcess.running = true; + } + function withOutput(message, out, err) { let text = message; if (out !== "") @@ -49,24 +78,65 @@ Singleton { function finishSync() { syncing = false; + refresh(); syncFinished(); } Process { id: detectProcess - command: ["sh", "-c", "command -v dms-greeter >/dev/null 2>&1 && echo binary; grep -qs dms-greeter /etc/greetd/config.toml && echo config"] + command: ["sh", "-c", `command -v dms-greeter >/dev/null 2>&1 && echo binary +grep -qs dms-greeter /etc/greetd/config.toml && echo config +[ "$(systemctl is-enabled greetd 2>/dev/null)" = enabled ] && echo enabled +id -nG | tr ' ' '\n' | grep -qxE 'greeter|greetd|_greeter' && echo group +own=$(readlink -f "$HOME/.config/DankMaterialShell/settings.json") +for f in "/var/cache/dms-greeter/users/$USER/settings.json" /var/cache/dms-greeter/settings.json; do + [ -L "$f" ] && [ "$(readlink -f "$f")" = "$own" ] && echo live && break +done +[ -f "/var/cache/dms-greeter/users/$USER/settings.json" ] && echo slot +exit 0`] running: true stdout: StdioCollector { onStreamFinished: { const found = text.split("\n"); root.binaryExists = found.includes("binary"); - root.available = root.binaryExists || found.includes("config"); + root.configured = found.includes("config"); + root.enabled = found.includes("enabled"); + root.inGroup = found.includes("group"); + root.slotState = found.includes("live") ? "live" : (found.includes("slot") ? "snapshot" : ""); + root.available = root.binaryExists || root.configured; } } } + Process { + id: profileSyncProcess + + command: ["dms-greeter", "sync", "--profile", "--yes"] + + stdout: StdioCollector { + id: profileOut + } + + stderr: StdioCollector { + id: profileErr + } + + onExited: exitCode => { + const out = profileOut.text.trim(); + const err = profileErr.text.trim(); + if (exitCode !== 0) { + root.syncStatus = root.withOutput(I18n.tr("Profile sync failed.") + " (exit " + exitCode + ")", out, err); + root.finishSync(); + return; + } + root.syncStatus = root.withOutput(I18n.tr("Sync completed successfully."), out, err); + ToastService.showInfo(I18n.tr("Greeter sync complete")); + root.finishSync(); + } + } + Process { id: sudoProbeProcess @@ -118,6 +188,44 @@ Singleton { } } + Process { + id: unlinkProcess + + command: ["dms-greeter", "unlink", "--yes"] + + stdout: StdioCollector { + id: unlinkOut + } + + stderr: StdioCollector { + id: unlinkErr + } + + onExited: exitCode => { + const out = unlinkOut.text.trim(); + const err = unlinkErr.text.trim(); + if (exitCode === 0) { + root.syncStatus = root.withOutput(I18n.tr("Login screen unlinked.", "greeter unlink status"), out, err); + root.finishSync(); + return; + } + root.syncStatus = root.withOutput(I18n.tr("Unlink needs sudo authentication. Opening terminal so you can use password or fingerprint.", "greeter unlink status"), out, err); + unlinkTerminalProcess.running = true; + } + } + + Process { + id: unlinkTerminalProcess + + command: ["dms-greeter", "unlink", "--terminal", "--yes"] + + onExited: exitCode => { + if (exitCode !== 0) + root.syncStatus += "\n\n" + I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms-greeter unlink' manually.", "greeter unlink status") + " (exit " + exitCode + ")"; + root.finishSync(); + } + } + Process { id: terminalFallbackProcess diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index d1ba5b8df..15d55d569 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -44,6 +44,7 @@ Singleton { signal sessionUnlocked signal sessionResumed signal lockEditorRequested + signal greeterEditorRequested signal lidOpened signal loginctlStateChanged diff --git a/quickshell/Widgets/DankClockWidget.qml b/quickshell/Widgets/DankClockWidget.qml new file mode 100644 index 000000000..7f65a63a4 --- /dev/null +++ b/quickshell/Widgets/DankClockWidget.qml @@ -0,0 +1,3 @@ +import qs.DankCommon.Widgets as DankCommon + +DankCommon.DankClockWidget {} diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 3612dc6f7..6051f605f 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -11502,6 +11502,26 @@ ], "parentLabel": "Date, time & region" }, + { + "section": "greeterAdvanced", + "label": "Advanced", + "tabIndex": 31, + "category": "Greeter", + "keywords": [ + "advanced", + "display manager", + "greetd", + "greeter", + "login", + "repair", + "security", + "status", + "sync" + ], + "parentLabel": "Security", + "icon": "tune", + "conditionKey": "greeterAvailable" + }, { "section": "greeterAppearance", "label": "Appearance", @@ -11611,6 +11631,37 @@ "icon": "extension", "conditionKey": "greeterAvailable" }, + { + "section": "greeterFollowLockScreen", + "label": "Follow lock screen", + "tabIndex": 31, + "category": "Greeter", + "keywords": [ + "arrange", + "components", + "display manager", + "follow", + "greetd", + "greeter", + "layout", + "lock", + "lockscreen", + "login", + "modules", + "picker", + "screen", + "security", + "separately", + "session", + "shows", + "supports", + "turn", + "widgets" + ], + "parentLabel": "Security", + "description": "The login screen shows the lock screen widgets it supports and its own session picker. Turn off to arrange it separately.", + "conditionKey": "greeterAvailable" + }, { "section": "_tab_31", "label": "Greeter", @@ -11677,14 +11728,48 @@ "display manager", "greetd", "greeter", + "link", "login", "security", - "status" + "status", + "sync" ], "parentLabel": "Security", "icon": "info", "conditionKey": "greeterAvailable" }, + { + "section": "greeterWidgets", + "label": "Widgets", + "tabIndex": 31, + "category": "Greeter", + "keywords": [ + "arrange", + "clock", + "components", + "display manager", + "greetd", + "greeter", + "layout", + "lock", + "lockscreen", + "login", + "modules", + "picker", + "screen", + "security", + "separately", + "session", + "shows", + "supports", + "turn", + "widgets" + ], + "parentLabel": "Security", + "icon": "widgets", + "description": "The login screen shows the lock screen widgets it supports and its own session picker. Turn off to arrange it separately.", + "conditionKey": "greeterAvailable" + }, { "section": "muxType", "label": "General",