lock: add option to require password for poweroff/reboot (#2085)

This commit is contained in:
end-4
2025-10-03 01:02:03 +02:00
parent 027f9a1793
commit 42913816ce
4 changed files with 152 additions and 96 deletions
@@ -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 {
+33 -2
View File
@@ -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();
}
}
@@ -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")]
})
}
}
@@ -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
}
}
}