lock screen?

This commit is contained in:
Zacharias-Brohn
2025-12-05 01:25:50 +01:00
parent 1155486858
commit ab54178747
19 changed files with 574 additions and 6 deletions
+24
View File
@@ -0,0 +1,24 @@
# Agent Guidelines for z-bar-qt
## Build & Test
- **Build**: `cmake -B build -G Ninja && ninja -C build` (uses CMake + Ninja)
- **Install**: `sudo ninja -C build install` (installs to /usr/lib/qt6/qml)
- **No test suite**: Project has no automated tests currently
- **Update script**: `scripts/update.sh` (runs `yay -Sy`)
## Code Style - C++
- **Standard**: C++20 with strict warnings enabled (see CMakeLists.txt line 14-20)
- **Headers**: `#pragma once` for header guards
- **Types**: Use [[nodiscard]] for getters, explicit constructors, const correctness
- **Qt Integration**: QML_ELEMENT/QML_UNCREATABLE macros, Q_PROPERTY for QML exposure
- **Naming**: camelCase for methods/variables, m_ prefix for member variables
- **Includes**: Qt headers with lowercase (qobject.h, qqmlintegration.h)
- **Namespaces**: Use `namespace ZShell` for plugin code
## Code Style - QML
- **Pragma**: Start with `pragma ComponentBehavior: Bound` for type safety
- **Imports**: Qt modules first, then Quickshell, then local (qs.Modules, qs.Config, qs.Helpers)
- **Aliases**: Use `qs` prefix for local module imports
- **Properties**: Use `required property` for mandatory bindings
- **Types**: Explicit type annotations in JavaScript (`: void`, `: string`)
- **Structure**: Components in Components/, Modules in Modules/, Config singletons in Config/
+3 -1
View File
@@ -23,6 +23,8 @@ Scope {
screen: modelData screen: modelData
color: "transparent" color: "transparent"
property var root: Quickshell.shellDir property var root: Quickshell.shellDir
WlrLayershell.namespace: "ZShell-Bar"
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
PanelWindow { PanelWindow {
@@ -69,7 +71,7 @@ Scope {
Variants { Variants {
id: popoutRegions id: popoutRegions
model: panels.children model: panels.children
Region { Region {
required property Item modelData required property Item modelData
+4
View File
@@ -22,6 +22,8 @@ Singleton {
property alias transparency: adapter.transparency property alias transparency: adapter.transparency
property alias baseFont: adapter.baseFont property alias baseFont: adapter.baseFont
property alias animScale: adapter.animScale property alias animScale: adapter.animScale
property alias lock: adapter.lock
property alias idle: adapter.idle
FileView { FileView {
id: root id: root
@@ -52,6 +54,8 @@ Singleton {
property Transparency transparency: Transparency {} property Transparency transparency: Transparency {}
property string baseFont: "Segoe UI Variable Text" property string baseFont: "Segoe UI Variable Text"
property real animScale: 1.0 property real animScale: 1.0
property LockConf lock: LockConf {}
property IdleTimeout idle: IdleTimeout {}
} }
} }
} }
+15
View File
@@ -0,0 +1,15 @@
import Quickshell.Io
JsonObject {
property list<var> timeouts: [
{
timeout: 180,
idleAction: "lock"
},
{
timeout: 300,
idleAction: "dpms off",
activeAction: "dpms on"
}
]
}
+14
View File
@@ -0,0 +1,14 @@
import Quickshell.Io
JsonObject {
property bool recolourLogo: false
property bool enableFprint: true
property int maxFprintTries: 3
property Sizes sizes: Sizes {}
component Sizes: JsonObject {
property real heightMult: 0.7
property real ratio: 16 / 9
property int centerWidth: 600
}
}
+10
View File
@@ -0,0 +1,10 @@
pragma Singleton
import Quickshell
import Quickshell.Services.Mpris
Singleton {
id: root
readonly property list<MprisPlayer> list: Mpris.players.values
}
+1
View File
@@ -19,6 +19,7 @@ Searcher {
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
actualCurrent = path; actualCurrent = path;
WallpaperPath.currentWallpaperPath = path; WallpaperPath.currentWallpaperPath = path;
Quickshell.execDetached(["python3", Quickshell.shellPath("scripts/LockScreenBg.py"), `--input_image=${root.actualCurrent}`, `--output_path=${Paths.state}/lockscreen_bg.png`]);
} }
function preview(path: string): void { function preview(path: string): void {
+3
View File
@@ -2,11 +2,13 @@ pragma Singleton
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Paths
Singleton { Singleton {
id: root id: root
property alias currentWallpaperPath: adapter.currentWallpaperPath property alias currentWallpaperPath: adapter.currentWallpaperPath
property alias lockscreenBg: adapter.lockscreenBg
FileView { FileView {
id: fileView id: fileView
@@ -18,6 +20,7 @@ Singleton {
JsonAdapter { JsonAdapter {
id: adapter id: adapter
property string currentWallpaperPath: "" property string currentWallpaperPath: ""
property string lockscreenBg: `${Paths.state}/lockscreen_bg.png`
} }
} }
} }
+1
View File
@@ -22,6 +22,7 @@ Scope {
color: "transparent" color: "transparent"
visible: false visible: false
WlrLayershell.namespace: "ZShell-Launcher"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
+39
View File
@@ -0,0 +1,39 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Wayland
import qs.Config
import qs.Helpers
Scope {
id: root
required property Lock lock
readonly property bool enabled: !Players.list.some( p => p.isPlaying )
function handleIdleAction( action: var ): void {
if ( !action )
return;
if ( action === "lock" )
lock.lock.locked = true;
else if ( action === "unlock" )
lock.lock.locked = false;
else if ( typeof action === "string" )
Hypr.dispatch( action );
else
Quickshell.execDetached( action );
}
Variants {
model: Config.idle.timeouts
IdleMonitor {
required property var modelData
enabled: root.enabled && modelData.timeout > 0 ? true : false
timeout: modelData.timeout
onIsIdleChanged: root.handleIdleAction( isIdle ? modelData.idleAction : modelData.activeAction )
}
}
}
+37
View File
@@ -0,0 +1,37 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick
Scope {
id: root
property alias lock: lock
WlSessionLock {
id: lock
signal unlock
LockSurface {
lock: lock
pam: pam
}
}
Pam {
id: pam
lock: lock
}
GlobalShortcut {
name: "lock"
description: "Lock the current session"
appid: "zshell-lock"
onPressed: {
lock.locked = true
}
}
}
+286
View File
@@ -0,0 +1,286 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import qs.Config
import qs.Helpers
import qs.Effects
import qs.Components
import qs.Modules
WlSessionLockSurface {
id: root
required property WlSessionLock lock
required property Pam pam
color: "transparent"
TextInput {
id: hiddenInput
focus: true
visible: false
Keys.onPressed: function(event: KeyEvent): void {
root.pam.handleKey(event);
event.accepted = true;
}
onTextChanged: text = ""
}
ScreencopyView {
id: background
anchors.fill: parent
captureSource: root.screen
opacity: 1
layer.enabled: true
layer.effect: MultiEffect {
autoPaddingEnabled: false
blurEnabled: true
blur: 2
blurMax: 32
blurMultiplier: 0
}
}
// Image {
// id: backgroundImage
// anchors.fill: parent
// asynchronous: true
// source: WallpaperPath.lockscreenBg
// }
Rectangle {
anchors.fill: parent
color: "transparent"
Rectangle {
id: contentBox
anchors.centerIn: parent
// Material Design 3: Use surfaceContainer for elevated surfaces
color: DynamicColors.tPalette.m3surfaceContainer
radius: 28
// M3 spacing: 24px horizontal, 32px vertical padding
implicitWidth: Math.floor(childrenRect.width + 48)
implicitHeight: Math.floor(childrenRect.height + 64)
// M3 Elevation 2 shadow effect
layer.enabled: true
layer.effect: MultiEffect {
source: contentBox
blurEnabled: false
blurMax: 12
shadowBlur: 1
shadowColor: DynamicColors.palette.m3shadow
shadowOpacity: 0.3
shadowEnabled: true
autoPaddingEnabled: true
}
ColumnLayout {
id: mainLayout
anchors.centerIn: parent
width: childrenRect.width
spacing: 0
// Title: M3 Display Small (32sp, 400 weight)
Text {
id: titleText
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 8
text: "Session Locked"
font.pixelSize: 32
font.weight: Font.Normal
font.letterSpacing: 0.5
color: DynamicColors.palette.m3onSurface
}
// Support text: M3 Body Medium (14sp, 500 weight)
Text {
id: supportText
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 24
text: "Please enter your password to unlock"
font.pixelSize: 14
font.weight: Font.Medium
color: DynamicColors.palette.m3onSurfaceVariant
}
// Input field container
Rectangle {
id: inputContainer
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 16
Layout.preferredWidth: 320
Layout.preferredHeight: 48
color: DynamicColors.tPalette.m3surfaceContainerHigh
radius: 1000
border.width: 1
border.color: {
if (root.pam.state === "error" || root.pam.state === "fail") {
return DynamicColors.palette.m3error;
}
return DynamicColors.palette.m3outline;
}
Behavior on border.color {
ColorAnimation { duration: 150 }
}
ListView {
id: charList
readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
function bindImWidth(): void {
imWidthBehavior.enabled = false;
implicitWidth = Qt.binding(() => fullWidth);
imWidthBehavior.enabled = true;
}
anchors.centerIn: parent
anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0
implicitWidth: fullWidth
implicitHeight: 16
orientation: Qt.Horizontal
spacing: 8
interactive: false
model: ScriptModel {
values: root.pam.buffer.split("")
}
delegate: CustomRect {
id: ch
implicitWidth: implicitHeight
implicitHeight: charList.implicitHeight
color: DynamicColors.palette.m3onSurface
radius: 1000
opacity: 0
scale: 0
Component.onCompleted: {
opacity = 1;
scale = 1;
}
ListView.onRemove: removeAnim.start()
SequentialAnimation {
id: removeAnim
PropertyAction {
target: ch
property: "ListView.delayRemove"
value: true
}
ParallelAnimation {
Anim {
target: ch
property: "opacity"
to: 0
}
Anim {
target: ch
property: "scale"
to: 0.5
}
}
PropertyAction {
target: ch
property: "ListView.delayRemove"
value: false
}
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {
duration: MaterialEasing.expressiveFastSpatialTime
easing.bezierCurve: MaterialEasing.expressiveFastSpatial
}
}
}
Behavior on implicitWidth {
id: imWidthBehavior
Anim {}
}
}
// Input focus indicator (M3 focused state)
Rectangle {
anchors.fill: parent
radius: 12
color: "transparent"
border.width: 2
border.color: DynamicColors.palette.m3primary
opacity: 0
visible: hiddenInput.activeFocus
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Component.onCompleted: {
if (hiddenInput.activeFocus) opacity = 1;
}
}
// Message display: M3 Body Small (12sp, 500 weight) for error messages
Text {
id: messageDisplay
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
text: {
if (root.pam.lockMessage) {
return root.pam.lockMessage;
}
if (root.pam.state === "error") {
return "Authentication error";
}
if (root.pam.state === "fail") {
return "Invalid password";
}
if (root.pam.state === "max") {
return "Maximum attempts reached";
}
return "";
}
visible: text.length > 0
font.pixelSize: 12
font.weight: Font.Medium
color: root.pam.state === "max" ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurfaceVariant
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: 320
}
}
}
Component.onCompleted: hiddenInput.forceActiveFocus()
}
}
+97
View File
@@ -0,0 +1,97 @@
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pam
import QtQuick
Scope {
id: root
required property WlSessionLock lock
readonly property alias passwd: passwd
property string lockMessage
property string state
property string fprintState
property string buffer
signal flashMsg
function handleKey(event: KeyEvent): void {
if (passwd.active || state === "max")
return;
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
passwd.start();
} else if (event.key === Qt.Key_Backspace) {
if ( event.modifiers & Qt.ControlModifier ) {
buffer = "";
} else {
buffer = buffer.slice(0, -1);
}
} else if ( event.key === Qt.Key_Escape ) {
buffer = "";
} else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
buffer += event.text;
}
}
PamContext {
id: passwd
config: "passwd"
configDirectory: Quickshell.shellDir + "/assets/pam.d"
onMessageChanged: {
if ( message.startsWith( "The account is locked" ))
root.lockMessage = message;
else if ( root.lockMessage && message.endsWith( " left to unlock)" ))
root.lockMessage += "\n" + message;
}
onResponseRequiredChanged: {
if ( !responseRequired )
return;
respond(root.buffer);
root.buffer = "";
}
onCompleted: res => {
if (res === PamResult.Success)
return root.lock.locked = false;
if (res === PamResult.Error)
root.state = "error";
else if (res === PamResult.MaxTries)
root.state = "max";
else if (res === PamResult.Failed)
root.state = "fail";
root.flashMsg();
stateReset.restart();
}
}
Timer {
id: stateReset
interval: 4000
onTriggered: {
if (root.state !== "max")
root.state = "";
}
}
Connections {
target: root.lock
function onSecureChanged(): void {
if (root.lock.secure) {
root.buffer = "";
root.state = "";
root.fprintState = "";
root.lockMessage = "";
}
}
}
}
+1
View File
@@ -21,6 +21,7 @@ PanelWindow {
bottom: true bottom: true
} }
WlrLayershell.namespace: "ZShell-Notifs"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
required property PanelWindow bar required property PanelWindow bar
+4 -5
View File
@@ -13,6 +13,7 @@ PanelWindow {
id: root id: root
color: "transparent" color: "transparent"
screen: root.bar.screen screen: root.bar.screen
anchors { anchors {
top: true top: true
right: true right: true
@@ -20,9 +21,11 @@ PanelWindow {
bottom: true bottom: true
} }
WlrLayershell.namespace: "ZShell-Notifs"
WlrLayershell.layer: WlrLayer.Overlay
mask: Region { regions: root.notifRegions } mask: Region { regions: root.notifRegions }
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
property list<Region> notifRegions: [] property list<Region> notifRegions: []
required property bool centerShown required property bool centerShown
required property PanelWindow bar required property PanelWindow bar
@@ -30,10 +33,6 @@ PanelWindow {
property color backgroundColor: Config.useDynamicColors ? DynamicColors.tPalette.m3surface : Config.baseBgColor property color backgroundColor: Config.useDynamicColors ? DynamicColors.tPalette.m3surface : Config.baseBgColor
visible: Hyprland.monitorFor(screen).focused visible: Hyprland.monitorFor(screen).focused
Component.onCompleted: {
console.log(NotifServer.list.filter( n => n.popup ).length + " notification popups loaded.");
}
ListView { ListView {
id: notifListView id: notifListView
model: ScriptModel { model: ScriptModel {
+3
View File
@@ -0,0 +1,3 @@
auth required pam_fprintd.so
auth include common-auth
account include common-account
+6
View File
@@ -0,0 +1,6 @@
#%PAM-1.0
auth required pam_faillock.so preauth
auth [success=1 default=bad] pam_unix.so nullok
auth [default=die] pam_faillock.so authfail
auth required pam_faillock.so authsucc
+18
View File
@@ -0,0 +1,18 @@
from PIL import Image, ImageFilter
import argparse
def gen_blurred_image(input_image, output_path):
img = Image.open(input_image)
img = img.filter(ImageFilter.GaussianBlur(40))
img.save(output_path, "PNG")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate a blurred lock screen background image.")
parser.add_argument("--input_image", type=str)
parser.add_argument("--output_path", type=str)
args = parser.parse_args()
gen_blurred_image(args.input_image, args.output_path)
+8
View File
@@ -2,6 +2,7 @@
//@ pragma Env QSG_RENDER_LOOP=threaded //@ pragma Env QSG_RENDER_LOOP=threaded
import Quickshell import Quickshell
import qs.Modules import qs.Modules
import qs.Modules.Lock
import qs.Helpers import qs.Helpers
Scope { Scope {
@@ -9,4 +10,11 @@ Scope {
Wallpaper {} Wallpaper {}
Launcher {} Launcher {}
AreaPicker {} AreaPicker {}
Lock {
id: lock
}
IdleInhibitor {
lock: lock
}
} }