diff --git a/ShellSettings.qml b/ShellSettings.qml index 8ad1dfe..75882cc 100644 --- a/ShellSettings.qml +++ b/ShellSettings.qml @@ -11,7 +11,7 @@ Singleton { FileView { path: `${Quickshell.env("XDG_DATA_HOME")}/quickshell/settings.json` watchChanges: true - onFileChanged: reload() + // onFileChanged: reload() onAdapterUpdated: writeAdapter() blockLoading: true diff --git a/bar/battery/BatteryIndicator.qml b/bar/battery/BatteryIndicator.qml index fed696f..d2e97f5 100644 --- a/bar/battery/BatteryIndicator.qml +++ b/bar/battery/BatteryIndicator.qml @@ -49,6 +49,11 @@ Item { anchors.fill: parent // placeholder for now + + // Text { + // text + // } + ComboBox { model: ["Power Save", "Balanced", "Performance"] currentIndex: PowerProfiles.profile diff --git a/bar/systray/TrayMenuEntry.qml b/bar/systray/TrayMenuEntry.qml index 1854fb7..8c5c9d4 100644 --- a/bar/systray/TrayMenuEntry.qml +++ b/bar/systray/TrayMenuEntry.qml @@ -105,8 +105,6 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - Layout.leftMargin: 6 - Layout.rightMargin: 6 } Item { diff --git a/lockscreen/Controller.qml b/lockscreen/Controller.qml new file mode 100644 index 0000000..d3ba238 --- /dev/null +++ b/lockscreen/Controller.qml @@ -0,0 +1,41 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import Quickshell.Wayland + +Scope { + id: root + + PersistentProperties { + id: persist + property bool locked: false + } + + IpcHandler { + target: "lockscreen" + + function lock(): void { + persist.locked = true; + } + } + + LockContext { + id: passwordContext + + onUnlocked: persist.locked = false + } + + WlSessionLock { + id: lock + + locked: persist.locked + + WlSessionLockSurface { + LockSurface { + anchors.fill: parent + context: passwordContext + } + } + } +} diff --git a/lockscreen/LockContext.qml b/lockscreen/LockContext.qml new file mode 100644 index 0000000..fa7b85f --- /dev/null +++ b/lockscreen/LockContext.qml @@ -0,0 +1,53 @@ +import QtQuick +import Quickshell +import Quickshell.Services.Pam + +Scope { + id: root + + property string currentText: "" + property bool unlockInProgress: false + property bool showFailure: false + signal unlocked + signal failed + + // Clear the failure text once the user starts typing. + onCurrentTextChanged: showFailure = false + + 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(); + root.currentText = ""; + } else { + root.showFailure = true; + } + + root.unlockInProgress = false; + } + } +} diff --git a/lockscreen/LockSurface.qml b/lockscreen/LockSurface.qml new file mode 100644 index 0000000..9b7d854 --- /dev/null +++ b/lockscreen/LockSurface.qml @@ -0,0 +1,198 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Qt5Compat.GraphicalEffects +import ".." + +Rectangle { + id: root + color: Window.active ? ShellSettings.colors["surface"] : ShellSettings.colors["surface_dim"] + required property LockContext context + + Item { + anchors.fill: parent + + Image { + id: bgImage + source: ShellSettings.settings.wallpaperUrl + fillMode: Image.PreserveAspectCrop + anchors.fill: parent + visible: false + } + + FastBlur { + anchors.fill: bgImage + source: bgImage + radius: 80 + transparentBorder: true + } + + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.3 + } + + Rectangle { + anchors.fill: parent + color: "transparent" + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0.2) + } + GradientStop { + position: 0.5 + color: Qt.rgba(0, 0, 0, 0.1) + } + GradientStop { + position: 1.0 + color: Qt.rgba(0, 0, 0, 0.4) + } + } + } + } + + // Date and time display + ColumnLayout { + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: 120 + } + spacing: 10 + + Text { + id: clock + horizontalAlignment: Text.AlignHCenter + renderType: Text.NativeRendering + font.pointSize: 72 + font.weight: Font.Light + color: "white" + text: { + const now = this.date; + let hours = now.getHours(); + const minutes = now.getMinutes().toString().padStart(2, '0'); + const ampm = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; // 0 should be 12 + return `${hours}:${minutes}`; + } + + property var date: new Date() + Layout.alignment: Qt.AlignHCenter + + Timer { + running: true + repeat: true + interval: 1000 + onTriggered: clock.date = new Date() + } + + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 0 + radius: 20 + samples: 41 + color: Qt.rgba(1, 1, 1, 0.3) + } + } + } + + // login section + ColumnLayout { + visible: Window.active + anchors.centerIn: parent + spacing: 30 + + Rectangle { + id: profileImage + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 120 + Layout.preferredHeight: 120 + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: profileImage.width + height: profileImage.height + radius: width / 2 + color: "black" + } + } + + Image { + source: "root:resources/general/pfp.png" + anchors.fill: parent + } + } + + // password input, should probably split this out into a seperate comp + LoginField { + id: passwordBox + enabled: !root.context.unlockInProgress + + Layout.preferredWidth: 250 + Layout.preferredHeight: 30 + Layout.maximumHeight: 30 + Layout.alignment: Qt.AlignHCenter + + onTextChanged: root.context.currentText = this.text + onAccepted: root.context.tryUnlock() + + Connections { + target: root.context + + function onCurrentTextChanged() { + if (!passwordBox.shaking) { + passwordBox.text = root.context.currentText; + } + } + + function onShowFailureChanged() { + if (root.context.showFailure && !passwordBox.shaking) { + passwordBox.shaking = true; + } + } + } + } + } + + // hint text + Text { + text: "Press Enter to unlock" + color: Qt.rgba(1, 1, 1, 0.5) + font.pointSize: 12 + horizontalAlignment: Text.AlignHCenter + opacity: passwordBox.text.length > 0 ? 1.0 : 0.0 + + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 60 + } + + Behavior on opacity { + NumberAnimation { + duration: 300 + } + } + } + + // testing button + Button { + visible: false + text: "Emergency Unlock" + onClicked: root.context.unlocked() + + anchors { + right: parent.right + bottom: parent.bottom + margins: 20 + } + } +} diff --git a/lockscreen/LoginField.qml b/lockscreen/LoginField.qml new file mode 100644 index 0000000..f387a17 --- /dev/null +++ b/lockscreen/LoginField.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects + +TextField { + id: root + color: "white" + scale: activeFocus ? 1.05 : 1.0 + padding: 8 + focus: true + echoMode: TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + font.pointSize: 11 + horizontalAlignment: Text.AlignHCenter + + background: Rectangle { + color: Qt.rgba(1, 1, 1, 0.1) + border.color: root.activeFocus ? Qt.rgba(1, 1, 1, 0.5) : Qt.rgba(1, 1, 1, 0.2) + border.width: 1 + radius: 8 + + layer.enabled: true + layer.effect: FastBlur { + radius: 10 + transparentBorder: true + } + } + + transform: Translate { + id: shakeTransform + x: 0 + } + + property bool shaking: false + + onShakingChanged: { + if (shaking) + shakeAnimation.start(); + } + + Behavior on scale { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + SequentialAnimation { + id: shakeAnimation + + NumberAnimation { + target: shakeTransform + property: "x" + to: -8 + duration: 50 + easing.type: Easing.OutQuad + } + NumberAnimation { + target: shakeTransform + property: "x" + to: 8 + duration: 100 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: shakeTransform + property: "x" + to: -6 + duration: 80 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: shakeTransform + property: "x" + to: 6 + duration: 80 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: shakeTransform + property: "x" + to: -3 + duration: 60 + easing.type: Easing.InOutQuad + } + + onFinished: { + root.shaking = false; + root.text = ""; + } + } +} diff --git a/lockscreen/README.md b/lockscreen/README.md new file mode 100644 index 0000000..1926b9b --- /dev/null +++ b/lockscreen/README.md @@ -0,0 +1,10 @@ +# Lockscreen + +This is a simple but functional lockscreen that follows the system color scheme. +The only authentication method it supports is a password. + +You can run the lockscreen with `quickshell -p shell.qml`. + +You can run the lockscreen in test mode (as a window) with `quickshell -p test.qml`. + +![](./image.png) diff --git a/lockscreen/image.png b/lockscreen/image.png new file mode 100644 index 0000000..f9d25d3 Binary files /dev/null and b/lockscreen/image.png differ diff --git a/lockscreen/pam/fingerprint.conf b/lockscreen/pam/fingerprint.conf new file mode 100644 index 0000000..14e89bd --- /dev/null +++ b/lockscreen/pam/fingerprint.conf @@ -0,0 +1 @@ +auth sufficient pam_fprintd.so diff --git a/lockscreen/pam/password.conf b/lockscreen/pam/password.conf new file mode 100644 index 0000000..7e5d75a --- /dev/null +++ b/lockscreen/pam/password.conf @@ -0,0 +1 @@ +auth required pam_unix.so diff --git a/lockscreen/test.qml b/lockscreen/test.qml new file mode 100644 index 0000000..22dddb9 --- /dev/null +++ b/lockscreen/test.qml @@ -0,0 +1,25 @@ +import QtQuick +import Quickshell + +ShellRoot { + LockContext { + id: lockContext + onUnlocked: Qt.quit(); + } + + FloatingWindow { + LockSurface { + anchors.fill: parent + context: lockContext + } + } + + // exit the example if the window closes + Connections { + target: Quickshell + + function onLastWindowClosed() { + Qt.quit(); + } + } +} diff --git a/notifications/NotificationCenter.qml b/notifications/NotificationCenter.qml index b682f68..90c3cba 100644 --- a/notifications/NotificationCenter.qml +++ b/notifications/NotificationCenter.qml @@ -42,7 +42,7 @@ Singleton { id: notificationPanel color: "red" implicitWidth: 500 - exclusionMode: ExclusionMode.Normal + exclusionMode: ExclusionMode.Ignore // WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive anchors { top: true diff --git a/shell.qml b/shell.qml index 3c4981c..b5d2452 100644 --- a/shell.qml +++ b/shell.qml @@ -8,6 +8,7 @@ import "notifications" as Notifications import "volume-osd" as VolumeOSD import "settings" as Settings import "launcher" as Launcher +import "lockscreen" as LockScreen import "wallpaper" as Wallpaper import "screencapture" as ScreenCapture @@ -29,6 +30,10 @@ ShellRoot { Bar.Bar { screen: scope.modelData } + + LockScreen.Controller { + + } } }