diff --git a/.config/hypr/hypridle.conf b/.config/hypr/hypridle.conf index 2ec488c7..44daa189 100644 --- a/.config/hypr/hypridle.conf +++ b/.config/hypr/hypridle.conf @@ -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 { diff --git a/.config/quickshell/ii/GlobalStates.qml b/.config/quickshell/ii/GlobalStates.qml index 55f71fd6..6e57c923 100644 --- a/.config/quickshell/ii/GlobalStates.qml +++ b/.config/quickshell/ii/GlobalStates.qml @@ -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: { diff --git a/.config/quickshell/ii/modules/background/Background.qml b/.config/quickshell/ii/modules/background/Background.qml index 0ee66fb2..e72fe2c6 100644 --- a/.config/quickshell/ii/modules/background/Background.qml +++ b/.config/quickshell/ii/modules/background/Background.qml @@ -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 } } } diff --git a/.config/quickshell/ii/modules/bar/Bar.qml b/.config/quickshell/ii/modules/bar/Bar.qml index 45c62be9..fbe9c184 100644 --- a/.config/quickshell/ii/modules/bar/Bar.qml +++ b/.config/quickshell/ii/modules/bar/Bar.qml @@ -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 } diff --git a/.config/quickshell/ii/modules/dock/Dock.qml b/.config/quickshell/ii/modules/dock/Dock.qml index 2f4d5d63..0c5e12f1 100644 --- a/.config/quickshell/ii/modules/dock/Dock.qml +++ b/.config/quickshell/ii/modules/dock/Dock.qml @@ -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) diff --git a/.config/quickshell/ii/modules/lock/Lock.qml b/.config/quickshell/ii/modules/lock/Lock.qml new file mode 100644 index 00000000..89d39778 --- /dev/null +++ b/.config/quickshell/ii/modules/lock/Lock.qml @@ -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(); + } + } +} diff --git a/.config/quickshell/ii/modules/lock/LockContext.qml b/.config/quickshell/ii/modules/lock/LockContext.qml new file mode 100644 index 00000000..c1e8e0bc --- /dev/null +++ b/.config/quickshell/ii/modules/lock/LockContext.qml @@ -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; + } + } +} diff --git a/.config/quickshell/ii/modules/lock/LockSurface.qml b/.config/quickshell/ii/modules/lock/LockSurface.qml new file mode 100644 index 00000000..98d1f57b --- /dev/null +++ b/.config/quickshell/ii/modules/lock/LockSurface.qml @@ -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 + } + } +} diff --git a/.config/quickshell/ii/modules/lock/pam/password.conf b/.config/quickshell/ii/modules/lock/pam/password.conf new file mode 100644 index 00000000..7e5d75ae --- /dev/null +++ b/.config/quickshell/ii/modules/lock/pam/password.conf @@ -0,0 +1 @@ +auth required pam_unix.so diff --git a/.config/quickshell/ii/modules/notificationPopup/NotificationPopup.qml b/.config/quickshell/ii/modules/notificationPopup/NotificationPopup.qml index 5e18a173..d954cbfb 100644 --- a/.config/quickshell/ii/modules/notificationPopup/NotificationPopup.qml +++ b/.config/quickshell/ii/modules/notificationPopup/NotificationPopup.qml @@ -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" diff --git a/.config/quickshell/ii/modules/onScreenKeyboard/OnScreenKeyboard.qml b/.config/quickshell/ii/modules/onScreenKeyboard/OnScreenKeyboard.qml index 996546c5..0913d008 100644 --- a/.config/quickshell/ii/modules/onScreenKeyboard/OnScreenKeyboard.qml +++ b/.config/quickshell/ii/modules/onScreenKeyboard/OnScreenKeyboard.qml @@ -33,7 +33,7 @@ Scope { // Scope sourceComponent: PanelWindow { // Window id: oskRoot - visible: oskLoader.active + visible: oskLoader.active && !GlobalStates.screenLocked anchors { bottom: true diff --git a/.config/quickshell/ii/modules/session/Session.qml b/.config/quickshell/ii/modules/session/Session.qml index 76ca373f..51f84ca9 100644 --- a/.config/quickshell/ii/modules/session/Session.qml +++ b/.config/quickshell/ii/modules/session/Session.qml @@ -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 diff --git a/.config/quickshell/ii/shell.qml b/.config/quickshell/ii/shell.qml index a026342d..7cb86a03 100644 --- a/.config/quickshell/ii/shell.qml +++ b/.config/quickshell/ii/shell.qml @@ -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 {} }