lock screen?
This commit is contained in:
@@ -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/
|
||||
@@ -23,6 +23,8 @@ Scope {
|
||||
screen: modelData
|
||||
color: "transparent"
|
||||
property var root: Quickshell.shellDir
|
||||
|
||||
WlrLayershell.namespace: "ZShell-Bar"
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
PanelWindow {
|
||||
@@ -69,7 +71,7 @@ Scope {
|
||||
Variants {
|
||||
id: popoutRegions
|
||||
model: panels.children
|
||||
|
||||
|
||||
Region {
|
||||
required property Item modelData
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ Singleton {
|
||||
property alias transparency: adapter.transparency
|
||||
property alias baseFont: adapter.baseFont
|
||||
property alias animScale: adapter.animScale
|
||||
property alias lock: adapter.lock
|
||||
property alias idle: adapter.idle
|
||||
|
||||
FileView {
|
||||
id: root
|
||||
@@ -52,6 +54,8 @@ Singleton {
|
||||
property Transparency transparency: Transparency {}
|
||||
property string baseFont: "Segoe UI Variable Text"
|
||||
property real animScale: 1.0
|
||||
property LockConf lock: LockConf {}
|
||||
property IdleTimeout idle: IdleTimeout {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import Quickshell.Io
|
||||
|
||||
JsonObject {
|
||||
property list<var> timeouts: [
|
||||
{
|
||||
timeout: 180,
|
||||
idleAction: "lock"
|
||||
},
|
||||
{
|
||||
timeout: 300,
|
||||
idleAction: "dpms off",
|
||||
activeAction: "dpms on"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||
}
|
||||
@@ -19,6 +19,7 @@ Searcher {
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = 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 {
|
||||
|
||||
@@ -2,11 +2,13 @@ pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Paths
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias currentWallpaperPath: adapter.currentWallpaperPath
|
||||
property alias lockscreenBg: adapter.lockscreenBg
|
||||
|
||||
FileView {
|
||||
id: fileView
|
||||
@@ -18,6 +20,7 @@ Singleton {
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
property string currentWallpaperPath: ""
|
||||
property string lockscreenBg: `${Paths.state}/lockscreen_bg.png`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ Scope {
|
||||
color: "transparent"
|
||||
visible: false
|
||||
|
||||
WlrLayershell.namespace: "ZShell-Launcher"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
|
||||
@@ -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 )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ PanelWindow {
|
||||
bottom: true
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "ZShell-Notifs"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
required property PanelWindow bar
|
||||
|
||||
@@ -13,6 +13,7 @@ PanelWindow {
|
||||
id: root
|
||||
color: "transparent"
|
||||
screen: root.bar.screen
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
@@ -20,9 +21,11 @@ PanelWindow {
|
||||
bottom: true
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "ZShell-Notifs"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
|
||||
mask: Region { regions: root.notifRegions }
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
property list<Region> notifRegions: []
|
||||
required property bool centerShown
|
||||
required property PanelWindow bar
|
||||
@@ -30,10 +33,6 @@ PanelWindow {
|
||||
property color backgroundColor: Config.useDynamicColors ? DynamicColors.tPalette.m3surface : Config.baseBgColor
|
||||
visible: Hyprland.monitorFor(screen).focused
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log(NotifServer.list.filter( n => n.popup ).length + " notification popups loaded.");
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: notifListView
|
||||
model: ScriptModel {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
auth required pam_fprintd.so
|
||||
auth include common-auth
|
||||
account include common-account
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user