forked from Shinonome/dots-hyprland
lock: add option to require password for poweroff/reboot (#2085)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user