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 centerClock: true
property bool showLockedText: 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 { property JsonObject media: JsonObject {
+33 -2
View File
@@ -10,18 +10,50 @@ import Quickshell.Hyprland
Scope { Scope {
id: root 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. // This stores all the information shared between the lock surfaces on each screen.
// https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen // https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen
LockContext { LockContext {
id: 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 // Unlock the screen before exiting, or the compositor will display a
// fallback lock you can't interact with. // fallback lock you can't interact with.
GlobalStates.screenLocked = false; GlobalStates.screenLocked = false;
// Refocus last focused window on unlock (hack) // Refocus last focused window on unlock (hack)
Quickshell.execDetached(["bash", "-c", `sleep 0.2; hyprctl --batch "dispatch togglespecialworkspace; dispatch togglespecialworkspace"`]) 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" + "decides to keyboard-unfocus the lock screen"
onPressed: { onPressed: {
// console.log("I BEG FOR PLEAS REFOCUZ")
lockContext.shouldReFocus(); lockContext.shouldReFocus();
} }
} }
@@ -6,8 +6,11 @@ import Quickshell.Services.Pam
Scope { Scope {
id: root id: root
enum ActionEnum { Unlock, Poweroff, Reboot }
signal shouldReFocus() signal shouldReFocus()
signal unlocked() signal unlocked(targetAction: var)
signal failed() signal failed()
// These properties are in the context and not individual lock surfaces // These properties are in the context and not individual lock surfaces
@@ -15,16 +18,31 @@ Scope {
property string currentText: "" property string currentText: ""
property bool unlockInProgress: false property bool unlockInProgress: false
property bool showFailure: false property bool showFailure: false
property var targetAction: LockContext.ActionEnum.Unlock
function resetTargetAction() {
root.targetAction = LockContext.ActionEnum.Unlock;
}
function clearText() {
root.currentText = "";
}
function resetClearTimer() { function resetClearTimer() {
passwordClearTimer.restart(); passwordClearTimer.restart();
} }
function reset() {
root.resetTargetAction();
root.clearText();
root.unlockInProgress = false;
}
Timer { Timer {
id: passwordClearTimer id: passwordClearTimer
interval: 10000 interval: 10000
onTriggered: { 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. // pam_unix won't send any important messages so all we need is the completion status.
onCompleted: result => { onCompleted: result => {
if (result == PamResult.Success) { if (result == PamResult.Success) {
root.unlocked(); root.unlocked(root.targetAction);
if (Config.options.lock.unlockKeyring) root.unlockKeyring();
} else { } else {
root.showFailure = true; root.clearText();
root.unlockInProgress = false;
GlobalStates.screenUnlockFailed = true; 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 required property LockContext context
property bool active: false property bool active: false
property bool showInputField: active || context.currentText.length > 0 property bool showInputField: active || context.currentText.length > 0
readonly property bool requirePasswordToPower: Config.options.lock.security.requirePasswordToPower
// Force focus on entry // Force focus on entry
function forceFieldFocus() { function forceFieldFocus() {
@@ -73,7 +74,10 @@ MouseArea {
// } // }
// implicitHeight: 40 // implicitHeight: 40
// colBackground: Appearance.colors.colLayer2 // colBackground: Appearance.colors.colLayer2
// onClicked: context.unlocked() // onClicked: {
// context.unlocked(LockContext.ActionEnum.Unlock);
// GlobalStates.screenLocked = false;
// }
// contentItem: StyledText { // contentItem: StyledText {
// text: "[[ DEBUG BYPASS ]]" // text: "[[ DEBUG BYPASS ]]"
// } // }
@@ -136,7 +140,15 @@ MouseArea {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
iconSize: 24 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 color: confirmButton.enabled ? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
} }
} }
@@ -155,29 +167,14 @@ MouseArea {
opacity: root.toolbarOpacity opacity: root.toolbarOpacity
// Username // Username
Row { IconAndTextPair {
spacing: 6
Layout.leftMargin: 8 Layout.leftMargin: 8
Layout.fillHeight: true icon: "account_circle"
text: SystemInfo.username
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
}
} }
// Keyboard layout (Xkb) // Keyboard layout (Xkb)
Loader { Loader {
Layout.leftMargin: 8
Layout.rightMargin: 8 Layout.rightMargin: 8
Layout.fillHeight: true Layout.fillHeight: true
@@ -230,77 +227,94 @@ MouseArea {
scale: root.toolbarScale scale: root.toolbarScale
opacity: root.toolbarOpacity opacity: root.toolbarOpacity
Row { IconAndTextPair {
visible: UPower.displayDevice.isLaptopBattery visible: UPower.displayDevice.isLaptopBattery
spacing: 4 icon: Battery.isCharging ? "bolt" : "battery_android_full"
Layout.fillHeight: true text: Math.round(Battery.percentage * 100)
Layout.leftMargin: 10 color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
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
}
} }
ToolbarButton { ActionToolbarIconButton {
id: sleepButton id: sleepButton
implicitWidth: height
onClicked: Session.suspend() onClicked: Session.suspend()
text: "dark_mode"
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 24
text: "dark_mode"
color: Appearance.colors.colOnSurfaceVariant
}
} }
ToolbarButton { PasswordGuardedActionToolbarIconButton {
id: powerButton id: powerButton
implicitWidth: height text: "power_settings_new"
targetAction: LockContext.ActionEnum.Poweroff
onClicked: Session.poweroff()
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 24
text: "power_settings_new"
color: Appearance.colors.colOnSurfaceVariant
}
} }
ToolbarButton { PasswordGuardedActionToolbarIconButton {
id: rebootButton 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 { toggled: root.context.targetAction === guardedBtn.targetAction
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter onClicked: {
verticalAlignment: Text.AlignVCenter if (!root.requirePasswordToPower) {
iconSize: 24 root.context.unlocked(guardedBtn.targetAction);
text: "restart_alt" return;
color: Appearance.colors.colOnSurfaceVariant }
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
}
}
} }