diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 00e0afd2a..ea99949d5 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -268,7 +268,10 @@ Singleton { } property bool centerClock: true property bool showLockedText: true - property bool unlockKeyring: true + property JsonObject security: JsonObject { + property bool unlockKeyring: true + property bool requirePasswordToPower: false + } } property JsonObject media: JsonObject { diff --git a/.config/quickshell/ii/modules/lock/Lock.qml b/.config/quickshell/ii/modules/lock/Lock.qml index 1acaa19e4..4b086970f 100644 --- a/.config/quickshell/ii/modules/lock/Lock.qml +++ b/.config/quickshell/ii/modules/lock/Lock.qml @@ -10,18 +10,50 @@ import Quickshell.Hyprland Scope { id: root + + function unlockKeyring() { + Quickshell.execDetached({ + environment: ({ + UNLOCK_PASSWORD: root.currentText + }), + command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")] + }) + } + // This stores all the information shared between the lock surfaces on each screen. // https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen LockContext { id: lockContext - onUnlocked: { + Connections { + target: GlobalStates + function onScreenLockedChanged() { + if (GlobalStates.screenLocked) lockContext.reset(); + } + } + + onUnlocked: (targetAction) => { + // Perform the target action if it's not just unlocking + if (targetAction == LockContext.ActionEnum.Poweroff) { + Session.poweroff(); + return; + } else if (targetAction == LockContext.ActionEnum.Reboot) { + Session.reboot(); + return; + } + + // Unlock the keyring if configured to do so + if (Config.options.lock.security.unlockKeyring) root.unlockKeyring(); + // Unlock the screen before exiting, or the compositor will display a // fallback lock you can't interact with. GlobalStates.screenLocked = false; // Refocus last focused window on unlock (hack) Quickshell.execDetached(["bash", "-c", `sleep 0.2; hyprctl --batch "dispatch togglespecialworkspace; dispatch togglespecialworkspace"`]) + + // Reset + lockContext.reset(); } } @@ -90,7 +122,6 @@ Scope { + "decides to keyboard-unfocus the lock screen" onPressed: { - // console.log("I BEG FOR PLEAS REFOCUZ") lockContext.shouldReFocus(); } } diff --git a/.config/quickshell/ii/modules/lock/LockContext.qml b/.config/quickshell/ii/modules/lock/LockContext.qml index f48b84057..7d030fc42 100644 --- a/.config/quickshell/ii/modules/lock/LockContext.qml +++ b/.config/quickshell/ii/modules/lock/LockContext.qml @@ -6,8 +6,11 @@ import Quickshell.Services.Pam Scope { id: root + + enum ActionEnum { Unlock, Poweroff, Reboot } + signal shouldReFocus() - signal unlocked() + signal unlocked(targetAction: var) signal failed() // These properties are in the context and not individual lock surfaces @@ -15,16 +18,31 @@ Scope { property string currentText: "" property bool unlockInProgress: false property bool showFailure: false + property var targetAction: LockContext.ActionEnum.Unlock + + function resetTargetAction() { + root.targetAction = LockContext.ActionEnum.Unlock; + } + + function clearText() { + root.currentText = ""; + } function resetClearTimer() { passwordClearTimer.restart(); } + function reset() { + root.resetTargetAction(); + root.clearText(); + root.unlockInProgress = false; + } + Timer { id: passwordClearTimer interval: 10000 onTriggered: { - root.currentText = ""; + root.reset(); } } @@ -55,24 +73,14 @@ Scope { // pam_unix won't send any important messages so all we need is the completion status. onCompleted: result => { if (result == PamResult.Success) { - root.unlocked(); - if (Config.options.lock.unlockKeyring) root.unlockKeyring(); + root.unlocked(root.targetAction); } else { - root.showFailure = true; + root.clearText(); + root.unlockInProgress = false; GlobalStates.screenUnlockFailed = true; + root.showFailure = true; } - - root.currentText = ""; - root.unlockInProgress = false; } } - function unlockKeyring() { - Quickshell.execDetached({ - environment: ({ - UNLOCK_PASSWORD: root.currentText - }), - command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")] - }) - } } diff --git a/.config/quickshell/ii/modules/lock/LockSurface.qml b/.config/quickshell/ii/modules/lock/LockSurface.qml index 14fcf4a6a..7a4340705 100644 --- a/.config/quickshell/ii/modules/lock/LockSurface.qml +++ b/.config/quickshell/ii/modules/lock/LockSurface.qml @@ -14,6 +14,7 @@ MouseArea { required property LockContext context property bool active: false property bool showInputField: active || context.currentText.length > 0 + readonly property bool requirePasswordToPower: Config.options.lock.security.requirePasswordToPower // Force focus on entry function forceFieldFocus() { @@ -73,7 +74,10 @@ MouseArea { // } // implicitHeight: 40 // colBackground: Appearance.colors.colLayer2 - // onClicked: context.unlocked() + // onClicked: { + // context.unlocked(LockContext.ActionEnum.Unlock); + // GlobalStates.screenLocked = false; + // } // contentItem: StyledText { // text: "[[ DEBUG BYPASS ]]" // } @@ -136,7 +140,15 @@ MouseArea { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter iconSize: 24 - text: "arrow_right_alt" + text: { + if (root.context.targetAction === LockContext.ActionEnum.Unlock) { + return "arrow_right_alt"; + } else if (root.context.targetAction === LockContext.ActionEnum.Poweroff) { + return "power_settings_new"; + } else if (root.context.targetAction === LockContext.ActionEnum.Reboot) { + return "restart_alt"; + } + } color: confirmButton.enabled ? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext } } @@ -155,29 +167,14 @@ MouseArea { opacity: root.toolbarOpacity // Username - Row { - spacing: 6 + IconAndTextPair { Layout.leftMargin: 8 - Layout.fillHeight: true - - MaterialSymbol { - id: userIcon - anchors.verticalCenter: parent.verticalCenter - fill: 1 - text: "account_circle" - iconSize: Appearance.font.pixelSize.huge - color: Appearance.colors.colOnSurfaceVariant - } - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: SystemInfo.username - color: Appearance.colors.colOnSurfaceVariant - } + icon: "account_circle" + text: SystemInfo.username } // Keyboard layout (Xkb) Loader { - Layout.leftMargin: 8 Layout.rightMargin: 8 Layout.fillHeight: true @@ -230,77 +227,94 @@ MouseArea { scale: root.toolbarScale opacity: root.toolbarOpacity - Row { + IconAndTextPair { visible: UPower.displayDevice.isLaptopBattery - spacing: 4 - Layout.fillHeight: true - Layout.leftMargin: 10 - Layout.rightMargin: 10 - - MaterialSymbol { - id: boltIcon - anchors { - verticalCenter: parent.verticalCenter - } - fill: 1 - text: Battery.isCharging ? "bolt" : "battery_android_full" - iconSize: Appearance.font.pixelSize.huge - animateChange: true - color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant - } - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: Math.round(Battery.percentage * 100) - color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant - } + icon: Battery.isCharging ? "bolt" : "battery_android_full" + text: Math.round(Battery.percentage * 100) + color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant } - ToolbarButton { + ActionToolbarIconButton { id: sleepButton - implicitWidth: height - onClicked: Session.suspend() - - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - iconSize: 24 - text: "dark_mode" - color: Appearance.colors.colOnSurfaceVariant - } + text: "dark_mode" } - ToolbarButton { + PasswordGuardedActionToolbarIconButton { id: powerButton - implicitWidth: height - - onClicked: Session.poweroff() - - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - iconSize: 24 - text: "power_settings_new" - color: Appearance.colors.colOnSurfaceVariant - } + text: "power_settings_new" + targetAction: LockContext.ActionEnum.Poweroff } - ToolbarButton { + PasswordGuardedActionToolbarIconButton { id: rebootButton - implicitWidth: height + text: "restart_alt" + targetAction: LockContext.ActionEnum.Reboot + } + } - onClicked: Session.reboot() + component PasswordGuardedActionToolbarIconButton: ActionToolbarIconButton { + id: guardedBtn + required property var targetAction - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - iconSize: 24 - text: "restart_alt" - color: Appearance.colors.colOnSurfaceVariant + toggled: root.context.targetAction === guardedBtn.targetAction + + onClicked: { + if (!root.requirePasswordToPower) { + root.context.unlocked(guardedBtn.targetAction); + return; + } + if (root.context.targetAction === guardedBtn.targetAction) { + root.context.resetTargetAction(); + } else { + root.context.targetAction = guardedBtn.targetAction; + root.context.shouldReFocus(); } } } + + component ActionToolbarIconButton: ToolbarButton { + id: iconBtn + implicitWidth: height + + colBackgroundToggled: Appearance.colors.colSecondaryContainer + colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover + colRippleToggled: Appearance.colors.colSecondaryContainerActive + + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + iconSize: 24 + text: iconBtn.text + color: iconBtn.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant + } + } + + component IconAndTextPair: Row { + id: pair + required property string icon + required property string text + property color color: Appearance.colors.colOnSurfaceVariant + + spacing: 4 + Layout.fillHeight: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + + + MaterialSymbol { + anchors.verticalCenter: parent.verticalCenter + fill: 1 + text: pair.icon + iconSize: Appearance.font.pixelSize.huge + animateChange: true + color: pair.color + } + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: pair.text + color: pair.color + } + } }