use quickshell for lock

This commit is contained in:
end-4 2025-07-20 08:58:40 +07:00
parent a000abc908
commit c1c291a9e3
13 changed files with 381 additions and 9 deletions

View file

@ -1,9 +1,11 @@
$lock_cmd = pidof hyprlock || hyprlock
$lock_cmd = hyprctl dispatch global quickshell:lock
$suspend_cmd = systemctl suspend || loginctl suspend
general {
lock_cmd = $lock_cmd
before_sleep_cmd = loginctl lock-session
after_sleep_cmd = hyprctl dispatch global quickshell:lockFocus
inhibit_sleep = 3
}
listener {

View file

@ -15,6 +15,8 @@ Singleton {
property bool overviewOpen: false
property bool workspaceShowNumbers: false
property bool superReleaseMightTrigger: true
property bool screenLocked: false
property bool screenLockContainsCharacters: false
property real screenZoom: 1
onScreenZoomChanged: {

View file

@ -19,7 +19,6 @@ Scope {
readonly property real fixedClockY: Config.options.background.clockY
Variants {
// For each monitor
model: Quickshell.screens
PanelWindow {
@ -44,6 +43,8 @@ Scope {
property real clockY: (modelData.height / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.height)
property var textHorizontalAlignment: clockX < screen.width / 3 ? Text.AlignLeft :
(clockX > screen.width * 2 / 3 ? Text.AlignRight : Text.AlignHCenter)
property var layoutHorizontalAlignment: clockX < screen.width / 3 ? Qt.AlignLeft :
(clockX > screen.width * 2 / 3 ? Qt.AlignRight : Qt.AlignHCenter)
// Colors
property color dominantColor: Appearance.colors.colPrimary
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
@ -52,6 +53,7 @@ Scope {
// Layer props
screen: modelData
exclusionMode: ExclusionMode.Ignore
// WlrLayershell.layer: GlobalStates.screenLocked ? WlrLayer.Top : WlrLayer.Bottom
WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "quickshell:background"
anchors {
@ -186,7 +188,7 @@ Scope {
ColumnLayout {
id: clockColumn
anchors.centerIn: parent
spacing: -5
spacing: 0
StyledText {
Layout.fillWidth: true
@ -203,6 +205,7 @@ Scope {
}
StyledText {
Layout.fillWidth: true
Layout.topMargin: -5
horizontalAlignment: bgRoot.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
@ -215,6 +218,51 @@ Scope {
text: DateTime.date
}
}
RowLayout {
anchors {
top: clockColumn.bottom
right: clockColumn.right
topMargin: 5
}
opacity: GlobalStates.screenLocked ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
MaterialSymbol {
text: "lock"
iconSize: Appearance.font.pixelSize.huge
color: bgRoot.colText
}
StyledText {
text: "Locked"
color: bgRoot.colText
font {
pixelSize: Appearance.font.pixelSize.larger
}
}
}
}
}
// Password prompt
StyledText {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 30
}
opacity: (GlobalStates.screenLocked && !GlobalStates.screenLockContainsCharacters) ? 1 : 0
scale: opacity
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
text: "Enter password"
color: CF.ColorUtils.transparentize(bgRoot.colText, 0.5)
font {
pixelSize: Appearance.font.pixelSize.normal
}
}
}

View file

@ -35,11 +35,11 @@ Scope {
return screens;
return screens.filter(screen => list.includes(screen.name));
}
Loader {
LazyLoader {
id: barLoader
active: GlobalStates.barOpen
active: GlobalStates.barOpen && !GlobalStates.screenLocked
required property ShellScreen modelData
sourceComponent: PanelWindow { // Bar window
component: PanelWindow { // Bar window
id: barRoot
screen: barLoader.modelData
@ -47,9 +47,10 @@ Scope {
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0
readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth
exclusionMode: ExclusionMode.Ignore
exclusiveZone: Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0)
WlrLayershell.namespace: "quickshell:bar"
implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding
exclusiveZone: Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0)
mask: Region {
item: barContent
}

View file

@ -23,6 +23,7 @@ Scope { // Scope
required property var modelData
id: dockRoot
screen: modelData
visible: !GlobalStates.screenLocked
property bool reveal: root.pinned
|| (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse)

View file

@ -0,0 +1,99 @@
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.lock
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
// 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: {
// Unlock the screen before exiting, or the compositor will display a
// fallback lock you can't interact with.
GlobalStates.screenLocked = false;
}
}
WlSessionLock {
id: lock
locked: GlobalStates.screenLocked
WlSessionLockSurface {
color: "transparent"
Loader {
active: GlobalStates.screenLocked
anchors.fill: parent
opacity: active ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
sourceComponent: LockSurface {
context: lockContext
}
}
}
}
// Blur layer hack
Variants {
model: Quickshell.screens
LazyLoader {
id: blurLayerLoader
required property var modelData
active: GlobalStates.screenLocked
component: PanelWindow {
screen: blurLayerLoader.modelData
WlrLayershell.namespace: "quickshell:lockWindowPusher"
color: "transparent"
anchors {
top: true
left: true
right: true
}
// implicitHeight: lockContext.currentText == "" ? 1 : screen.height
implicitHeight: 1
exclusiveZone: screen.height * 3 // For some reason if we don't multiply by some number it would look really weird
}
}
}
IpcHandler {
target: "lock"
function activate(): void {
GlobalStates.screenLocked = true;
}
function focus(): void {
lockContext.shouldReFocus();
}
}
GlobalShortcut {
name: "lock"
description: "Locks the screen"
onPressed: {
GlobalStates.screenLocked = true;
}
}
GlobalShortcut {
name: "lockFocus"
description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason"
+ "decides to keyboard-unfocus the lock screen"
onPressed: {
// console.log("I BEG FOR PLEAS REFOCUZ")
lockContext.shouldReFocus();
}
}
}

View file

@ -0,0 +1,59 @@
import qs
import QtQuick
import Quickshell
import Quickshell.Services.Pam
Scope {
id: root
signal shouldReFocus()
signal unlocked()
signal failed()
// These properties are in the context and not individual lock surfaces
// so all surfaces can share the same state.
property string currentText: ""
property bool unlockInProgress: false
property bool showFailure: false
// Clear the failure text once the user starts typing.
onCurrentTextChanged: {
showFailure = false;
GlobalStates.screenLockContainsCharacters = currentText.length > 0;
}
function tryUnlock() {
if (currentText === "") return;
root.unlockInProgress = true;
pam.start();
}
PamContext {
id: pam
// Its best to have a custom pam config for quickshell, as the system one
// might not be what your interface expects, and break in some way.
// This particular example only supports passwords.
configDirectory: "pam"
config: "password.conf"
// pam_unix will ask for a response for the password prompt
onPamMessage: {
if (this.responseRequired) {
this.respond(root.currentText);
}
}
// pam_unix won't send any important messages so all we need is the completion status.
onCompleted: result => {
if (result == PamResult.Success) {
root.unlocked();
} else {
root.showFailure = true;
}
root.currentText = "";
root.unlockInProgress = false;
}
}
}

View file

@ -0,0 +1,147 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
MouseArea {
id: root
required property LockContext context
property bool active: false
property bool showInputField: active || context.currentText.length > 0
function forceFieldFocus() {
passwordBox.forceActiveFocus();
}
Component.onCompleted: {
forceFieldFocus();
}
Connections {
target: context
function onShouldReFocus() {
forceFieldFocus();
}
}
Keys.onPressed: (event) => { // Esc to clear
// console.log("KEY!!")
if (event.key === Qt.Key_Escape) {
root.context.currentText = ""
}
forceFieldFocus();
}
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onPressed: (mouse) => {
forceFieldFocus();
// console.log("Pressed")
}
onPositionChanged: (mouse) => {
forceFieldFocus();
// console.log(JSON.stringify(mouse))
}
anchors.fill: parent
// RippleButton {
// anchors {
// top: parent.top
// left: parent.left
// leftMargin: 10
// topMargin: 10
// }
// implicitHeight: 40
// colBackground: Appearance.colors.colLayer2
// onClicked: context.unlocked()
// contentItem: StyledText {
// text: "[[ DEBUG BYPASS ]]"
// }
// }
// Password entry
Rectangle {
id: passwordBoxContainer
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: root.showInputField ? 20 : -height
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
radius: Appearance.rounding.full
color: Appearance.colors.colLayer2
implicitWidth: 160
implicitHeight: 44
StyledText {
visible: root.context.showFailure && passwordBox.text.length == 0
anchors.centerIn: parent
text: "Incorrect"
color: Appearance.m3colors.m3error
}
StyledTextInput {
id: passwordBox
anchors {
fill: parent
margins: 10
}
clip: true
horizontalAlignment: TextInput.AlignHCenter
verticalAlignment: TextInput.AlignVCenter
focus: true
onFocusChanged: root.forceFieldFocus();
color: Appearance.colors.colOnLayer2
font {
pixelSize: 10
}
// Password
enabled: !root.context.unlockInProgress
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
// Synchronizing (across monitors) and unlocking
onTextChanged: root.context.currentText = this.text
onAccepted: root.context.tryUnlock()
Connections {
target: root.context
function onCurrentTextChanged() {
passwordBox.text = root.context.currentText;
}
}
}
}
RippleButton {
anchors {
verticalCenter: passwordBoxContainer.verticalCenter
left: passwordBoxContainer.right
leftMargin: 5
}
visible: opacity > 0
implicitHeight: passwordBoxContainer.implicitHeight - 12
implicitWidth: implicitHeight
toggled: true
buttonRadius: passwordBoxContainer.radius
colBackground: Appearance.colors.colLayer2
onClicked: root.context.tryUnlock()
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 24
text: "arrow_right_alt"
color: Appearance.colors.colOnPrimary
}
}
}

View file

@ -0,0 +1 @@
auth required pam_unix.so

View file

@ -14,7 +14,7 @@ Scope {
PanelWindow {
id: root
visible: (Notifications.popupList.length > 0)
visible: (Notifications.popupList.length > 0) && !GlobalStates.screenLocked
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? null
WlrLayershell.namespace: "quickshell:notificationPopup"

View file

@ -33,7 +33,7 @@ Scope { // Scope
sourceComponent: PanelWindow { // Window
id: oskRoot
visible: oskLoader.active
visible: oskLoader.active && !GlobalStates.screenLocked
anchors {
bottom: true

View file

@ -25,6 +25,15 @@ Scope {
id: sessionLoader
active: false
Connections {
target: GlobalStates
function onScreenLockedChanged() {
if (GlobalStates.screenLocked) {
sessionLoader.active = false;
}
}
}
sourceComponent: PanelWindow { // Session menu
id: sessionRoot
visible: sessionLoader.active

View file

@ -10,6 +10,7 @@ import "./modules/background/"
import "./modules/bar/"
import "./modules/cheatsheet/"
import "./modules/dock/"
import "./modules/lock/"
import "./modules/mediaControls/"
import "./modules/notificationPopup/"
import "./modules/onScreenDisplay/"
@ -33,6 +34,7 @@ ShellRoot {
property bool enableBackground: true
property bool enableCheatsheet: true
property bool enableDock: true
property bool enableLock: true
property bool enableMediaControls: true
property bool enableNotificationPopup: true
property bool enableOnScreenDisplayBrightness: true
@ -56,6 +58,7 @@ ShellRoot {
LazyLoader { active: enableBackground; component: Background {} }
LazyLoader { active: enableCheatsheet; component: Cheatsheet {} }
LazyLoader { active: enableDock && Config.options.dock.enable; component: Dock {} }
LazyLoader { active: enableLock; component: Lock {} }
LazyLoader { active: enableMediaControls; component: MediaControls {} }
LazyLoader { active: enableNotificationPopup; component: NotificationPopup {} }
LazyLoader { active: enableOnScreenDisplayBrightness; component: OnScreenDisplayBrightness {} }