move lock dir up to shell/lock as sibling of modules
This commit is contained in:
parent
d6cd2f173a
commit
b5b1f4f406
7 changed files with 5 additions and 5 deletions
|
|
@ -25,7 +25,7 @@ stdenvNoCC.mkDerivation {
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|
||||||
mkdir -p $out/share/nova-shell
|
mkdir -p $out/share/nova-shell
|
||||||
cp -r shell/shell.qml shell/modules shell/assets $out/share/nova-shell/
|
cp -r shell/shell.qml shell/modules shell/lock shell/assets $out/share/nova-shell/
|
||||||
|
|
||||||
# Compile fragment shader to Qt RHI format
|
# Compile fragment shader to Qt RHI format
|
||||||
qsb --qt6 \
|
qsb --qt6 \
|
||||||
|
|
|
||||||
78
shell/lock/Lock.qml
Normal file
78
shell/lock/Lock.qml
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "../modules" as M
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool _enabled: M.Modules.lock.enable
|
||||||
|
property string _sessionPath: ""
|
||||||
|
|
||||||
|
WlSessionLock {
|
||||||
|
id: _lock
|
||||||
|
|
||||||
|
LockSurface {
|
||||||
|
lock: _lock
|
||||||
|
auth: _auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LockAuth {
|
||||||
|
id: _auth
|
||||||
|
lock: _lock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the actual logind session object path at startup
|
||||||
|
Process {
|
||||||
|
id: _sessionResolver
|
||||||
|
command: ["busctl", "call", "--system", "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "GetSession", "s", "auto"]
|
||||||
|
running: root._enabled
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => {
|
||||||
|
// Output: o "/org/freedesktop/login1/session/_32"
|
||||||
|
const match = data.match(/"([^"]+)"/);
|
||||||
|
if (match)
|
||||||
|
root._sessionPath = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
if (root._sessionPath)
|
||||||
|
_logindMonitor.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for logind Lock/Unlock signals via gdbus monitor.
|
||||||
|
// TODO: replace with native D-Bus integration when nova-stats becomes a quickshell plugin
|
||||||
|
Process {
|
||||||
|
id: _logindMonitor
|
||||||
|
command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root._sessionPath]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => {
|
||||||
|
if (data.indexOf(".Lock ()") !== -1 && root._enabled)
|
||||||
|
_lock.locked = true;
|
||||||
|
// Unlock is PAM-driven, ignore logind Unlock signal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logind LockedHint when lock state changes
|
||||||
|
Process {
|
||||||
|
id: _lockedHint
|
||||||
|
command: ["busctl", "call", "--system", "org.freedesktop.login1", root._sessionPath || "/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session", "SetLockedHint", "b", _lock.locked ? "true" : "false"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: _lock
|
||||||
|
|
||||||
|
function onLockStateChanged() {
|
||||||
|
if (root._sessionPath)
|
||||||
|
_lockedHint.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
shell/lock/LockAuth.qml
Normal file
91
shell/lock/LockAuth.qml
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.Pam
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property WlSessionLock lock
|
||||||
|
|
||||||
|
// Auth state: "", "busy", "fail", "error", "max"
|
||||||
|
property string state: ""
|
||||||
|
property string message: ""
|
||||||
|
property string buffer: ""
|
||||||
|
|
||||||
|
signal unlockRequested
|
||||||
|
signal authFailed
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
if (_passwd.active || state === "max")
|
||||||
|
return;
|
||||||
|
if (buffer.length > 0)
|
||||||
|
_passwd.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
property PamContext _passwd: PamContext {
|
||||||
|
config: "nova-shell"
|
||||||
|
configDirectory: Quickshell.shellDir + "/assets/pam.d"
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active)
|
||||||
|
root.state = "busy";
|
||||||
|
}
|
||||||
|
|
||||||
|
onResponseRequiredChanged: {
|
||||||
|
if (!responseRequired)
|
||||||
|
return;
|
||||||
|
respond(root.buffer);
|
||||||
|
root.buffer = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompleted: res => {
|
||||||
|
if (res === PamResult.Success) {
|
||||||
|
root.state = "";
|
||||||
|
root.message = "";
|
||||||
|
root.unlockRequested();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res === PamResult.Error) {
|
||||||
|
root.state = "error";
|
||||||
|
root.message = "Authentication error";
|
||||||
|
} else if (res === PamResult.MaxTries) {
|
||||||
|
root.state = "max";
|
||||||
|
root.message = "Too many attempts";
|
||||||
|
} else if (res === PamResult.Failed) {
|
||||||
|
root.state = "fail";
|
||||||
|
root.message = "Wrong password";
|
||||||
|
}
|
||||||
|
|
||||||
|
root.authFailed();
|
||||||
|
_stateReset.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageChanged: {
|
||||||
|
if (message.startsWith("The account is locked"))
|
||||||
|
root.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property Timer _stateReset: Timer {
|
||||||
|
interval: 3000
|
||||||
|
onTriggered: {
|
||||||
|
if (root.state !== "max")
|
||||||
|
root.state = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state when lock becomes secure (freshly locked)
|
||||||
|
property Connections _lockConn: Connections {
|
||||||
|
target: root.lock
|
||||||
|
|
||||||
|
function onSecureChanged() {
|
||||||
|
if (root.lock.secure) {
|
||||||
|
root.buffer = "";
|
||||||
|
root.state = "";
|
||||||
|
root.message = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
104
shell/lock/LockInput.qml
Normal file
104
shell/lock/LockInput.qml
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import QtQuick
|
||||||
|
import "../modules" as M
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property string buffer
|
||||||
|
required property string state
|
||||||
|
|
||||||
|
implicitHeight: 48
|
||||||
|
implicitWidth: 280
|
||||||
|
|
||||||
|
// Background pill
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: {
|
||||||
|
if (root.state === "fail" || root.state === "error")
|
||||||
|
return Qt.rgba(M.Theme.base08.r, M.Theme.base08.g, M.Theme.base08.b, 0.15);
|
||||||
|
if (root.state === "busy")
|
||||||
|
return Qt.rgba(M.Theme.base0D.r, M.Theme.base0D.g, M.Theme.base0D.b, 0.1);
|
||||||
|
return M.Theme.base02;
|
||||||
|
}
|
||||||
|
radius: height / 2
|
||||||
|
border.color: {
|
||||||
|
if (root.state === "fail" || root.state === "error")
|
||||||
|
return M.Theme.base08;
|
||||||
|
if (root.state === "busy")
|
||||||
|
return M.Theme.base0D;
|
||||||
|
return M.Theme.base03;
|
||||||
|
}
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder text
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: {
|
||||||
|
if (root.state === "busy")
|
||||||
|
return "Authenticating...";
|
||||||
|
if (root.state === "max")
|
||||||
|
return "Too many attempts";
|
||||||
|
return "Enter password";
|
||||||
|
}
|
||||||
|
color: M.Theme.base04
|
||||||
|
font.pixelSize: M.Theme.fontSize
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
opacity: root.buffer.length === 0 ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 150
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password dots
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.buffer.length
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: 10
|
||||||
|
height: 10
|
||||||
|
radius: 5
|
||||||
|
color: M.Theme.base05
|
||||||
|
|
||||||
|
scale: 0
|
||||||
|
opacity: 0
|
||||||
|
Component.onCompleted: {
|
||||||
|
scale = 1;
|
||||||
|
opacity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 120
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
272
shell/lock/LockSurface.qml
Normal file
272
shell/lock/LockSurface.qml
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "../modules" as M
|
||||||
|
|
||||||
|
WlSessionLockSurface {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property WlSessionLock lock
|
||||||
|
required property LockAuth auth
|
||||||
|
|
||||||
|
color: M.Theme.base00
|
||||||
|
|
||||||
|
// Blur screenshot of desktop as background
|
||||||
|
ScreencopyView {
|
||||||
|
id: background
|
||||||
|
anchors.fill: parent
|
||||||
|
captureSource: root.screen
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
autoPaddingEnabled: false
|
||||||
|
blurEnabled: true
|
||||||
|
blur: 1
|
||||||
|
blurMax: 64
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation on opacity {
|
||||||
|
to: 1
|
||||||
|
duration: 400
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dim overlay
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(M.Theme.base00.r, M.Theme.base00.g, M.Theme.base00.b, 0.4)
|
||||||
|
opacity: background.opacity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center content
|
||||||
|
Item {
|
||||||
|
id: content
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 320
|
||||||
|
height: _col.height
|
||||||
|
|
||||||
|
opacity: 0
|
||||||
|
scale: 0.9
|
||||||
|
NumberAnimation on opacity {
|
||||||
|
to: 1
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation on scale {
|
||||||
|
to: 1
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: _col
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: 24
|
||||||
|
|
||||||
|
// Clock
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: Qt.formatTime(new Date(), "HH:mm")
|
||||||
|
color: M.Theme.base05
|
||||||
|
font.pixelSize: 72
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
font.bold: true
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: parent.text = Qt.formatTime(new Date(), "HH:mm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: Qt.formatDate(new Date(), "dddd, d MMMM")
|
||||||
|
color: M.Theme.base04
|
||||||
|
font.pixelSize: M.Theme.fontSize + 2
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 60000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: parent.text = Qt.formatDate(new Date(), "dddd, d MMMM")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: 24
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password input
|
||||||
|
LockInput {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: 280
|
||||||
|
buffer: root.auth.buffer
|
||||||
|
state: root.auth.state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error message
|
||||||
|
Text {
|
||||||
|
id: _errorText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: root.auth.message
|
||||||
|
color: M.Theme.base08
|
||||||
|
font.pixelSize: M.Theme.fontSize - 1
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
opacity: root.auth.message ? 1 : 0
|
||||||
|
height: root.auth.message ? implicitHeight : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard input via TextInput - engages Qt's full input pipeline including
|
||||||
|
// text-input protocol, which is more reliable than Keys on a plain Item in
|
||||||
|
// layer-shell/lock surfaces where raw wl_keyboard delivery can be flaky.
|
||||||
|
TextInput {
|
||||||
|
id: _keyInput
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
color: "transparent"
|
||||||
|
selectionColor: "transparent"
|
||||||
|
selectedTextColor: "transparent"
|
||||||
|
cursorVisible: false
|
||||||
|
enabled: !root._unlocking && root.auth.state !== "max" && root.auth.state !== "busy"
|
||||||
|
|
||||||
|
onTextChanged: if (root.auth)
|
||||||
|
root.auth.buffer = text
|
||||||
|
|
||||||
|
Keys.onReturnPressed: root.auth.submit()
|
||||||
|
Keys.onEnterPressed: root.auth.submit()
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
onActiveFocusChanged: {
|
||||||
|
if (!activeFocus)
|
||||||
|
forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible)
|
||||||
|
_keyInput.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync TextInput when auth clears buffer externally (PAM submit, lock reset)
|
||||||
|
Connections {
|
||||||
|
target: root.auth
|
||||||
|
|
||||||
|
function onBufferChanged() {
|
||||||
|
if (_keyInput.text !== root.auth.buffer)
|
||||||
|
_keyInput.text = root.auth.buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock animation
|
||||||
|
property bool _unlocking: false
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.auth
|
||||||
|
|
||||||
|
function onUnlockRequested() {
|
||||||
|
root._unlocking = true;
|
||||||
|
_unlockAnim.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: _unlockAnim
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "scale"
|
||||||
|
to: 0.9
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: background
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyAction {
|
||||||
|
target: root.lock
|
||||||
|
property: "locked"
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shake animation on auth failure
|
||||||
|
SequentialAnimation {
|
||||||
|
id: _shakeAnim
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "anchors.horizontalCenterOffset"
|
||||||
|
to: 12
|
||||||
|
duration: 50
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "anchors.horizontalCenterOffset"
|
||||||
|
to: -12
|
||||||
|
duration: 50
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "anchors.horizontalCenterOffset"
|
||||||
|
to: 8
|
||||||
|
duration: 50
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "anchors.horizontalCenterOffset"
|
||||||
|
to: -8
|
||||||
|
duration: 50
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: content
|
||||||
|
property: "anchors.horizontalCenterOffset"
|
||||||
|
to: 0
|
||||||
|
duration: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.auth
|
||||||
|
function onAuthFailed() {
|
||||||
|
_shakeAnim.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
shell/lock/qmldir
Normal file
4
shell/lock/qmldir
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Lock 1.0 Lock.qml
|
||||||
|
LockAuth 1.0 LockAuth.qml
|
||||||
|
LockInput 1.0 LockInput.qml
|
||||||
|
LockSurface 1.0 LockSurface.qml
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||||
|
|
||||||
import "modules"
|
import "modules"
|
||||||
import "modules/lock" as Lock
|
import "lock" as Lock
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
|
|
|
||||||
Reference in a new issue