Switch hyprlock to QS
This commit is contained in:
@@ -19,6 +19,7 @@ User-installed plugins live alongside these conceptually but on disk under
|
||||
| Omarchy menu | `omarchy.menu` | `menu` | `menu/Menu.qml` |
|
||||
| Notifications | `omarchy.notifications` | `service` | `notifications/Service.qml` |
|
||||
| Idle monitor | `omarchy.idle` | `service` | `idle/Service.qml` |
|
||||
| Lock screen | `omarchy.lock` | `service` | `lock/Service.qml` |
|
||||
| OSD | `omarchy.osd` | `panel` | `osd/Osd.qml` |
|
||||
| Polkit agent | `omarchy.polkit` | `service` | `polkit/PolkitAgent.qml` |
|
||||
|
||||
@@ -69,6 +70,14 @@ clears it without writing a selection.
|
||||
The plugin has `keepLoaded: true` so the layer-shell window survives
|
||||
between summons within a single shell session.
|
||||
|
||||
## Lock screen
|
||||
|
||||
Session-lock surface using Quickshell's native `WlSessionLock` and two
|
||||
separate PAM services: `omarchy-lock-password` for password auth and,
|
||||
only when fingerprints are enrolled, `omarchy-lock-fingerprint` for
|
||||
fingerprint auth. It mirrors the previous lock screen field dimensions,
|
||||
colors, blurred wallpaper, placeholder, and corner toggle.
|
||||
|
||||
## Polkit agent
|
||||
|
||||
Theme-aware authentication dialog for privileged actions. It uses
|
||||
|
||||
+131
-32
@@ -21,6 +21,11 @@ Item {
|
||||
|
||||
property bool idledThisCycle: false
|
||||
property bool screensaverStartedThisCycle: false
|
||||
property string lastEvent: "starting"
|
||||
property string lastEventAt: ""
|
||||
property string lastScreensaverState: "unknown"
|
||||
property string lastScreensaverCheckAt: ""
|
||||
property int screensaverCheckCount: 0
|
||||
|
||||
PersistentProperties {
|
||||
id: persisted
|
||||
@@ -34,8 +39,23 @@ Item {
|
||||
return Math.floor(n)
|
||||
}
|
||||
|
||||
function runProcess(process, command) {
|
||||
if (process.running) return false
|
||||
function nowIso() {
|
||||
return new Date().toISOString()
|
||||
}
|
||||
|
||||
function logEvent(event, details) {
|
||||
var suffix = details === undefined || details === null || details === "" ? "" : ": " + String(details)
|
||||
root.lastEventAt = nowIso()
|
||||
root.lastEvent = event + suffix
|
||||
console.log("omarchy idle " + root.lastEventAt + " " + root.lastEvent)
|
||||
}
|
||||
|
||||
function runProcess(process, label, command) {
|
||||
if (process.running) {
|
||||
logEvent("process-skip", label + " already running")
|
||||
return false
|
||||
}
|
||||
logEvent("process-start", label + " " + command)
|
||||
process.command = ["bash", "-lc", command]
|
||||
process.running = true
|
||||
return true
|
||||
@@ -43,21 +63,27 @@ Item {
|
||||
|
||||
function launchScreensaver() {
|
||||
root.screensaverStartedThisCycle = true
|
||||
runProcess(screensaverProcess, "pidof hyprlock >/dev/null || omarchy-launch-screensaver")
|
||||
screensaverResumePollTimer.restart()
|
||||
runProcess(screensaverProcess, "screensaver", "[[ $(omarchy-shell lock isLocked 2>/dev/null) == \"true\" ]] || omarchy-launch-screensaver")
|
||||
}
|
||||
|
||||
function lockSystem() {
|
||||
function lockSystem(reason) {
|
||||
logEvent("lock-system", reason || "requested")
|
||||
screensaverTimer.stop()
|
||||
lockTimer.stop()
|
||||
screensaverResumePollTimer.stop()
|
||||
root.idledThisCycle = false
|
||||
root.screensaverStartedThisCycle = false
|
||||
runProcess(lockProcess, "omarchy-system-lock")
|
||||
runProcess(lockProcess, "lock", "omarchy-system-lock")
|
||||
}
|
||||
|
||||
function startIdleCycle() {
|
||||
if (root.idledThisCycle) return
|
||||
if (root.idledThisCycle) {
|
||||
logEvent("idle-cycle-already-running")
|
||||
return
|
||||
}
|
||||
|
||||
logEvent("idle-cycle-start", "screensaver=" + root.screensaverTimeoutSeconds + " lock=" + root.lockTimeoutSeconds)
|
||||
root.idledThisCycle = true
|
||||
root.screensaverStartedThisCycle = false
|
||||
screensaverResumePollTimer.stop()
|
||||
@@ -65,16 +91,17 @@ Item {
|
||||
if (root.screensaverDelaySeconds === 0) launchScreensaver()
|
||||
else screensaverTimer.restart()
|
||||
|
||||
if (root.lockDelaySeconds === 0) lockSystem()
|
||||
if (root.lockDelaySeconds === 0) lockSystem("lock-timeout-immediate")
|
||||
else lockTimer.restart()
|
||||
}
|
||||
|
||||
function cancelIdleCycle() {
|
||||
function cancelIdleCycle(reason) {
|
||||
logEvent("idle-cycle-cancel", reason || "requested")
|
||||
screensaverTimer.stop()
|
||||
lockTimer.stop()
|
||||
screensaverResumePollTimer.stop()
|
||||
|
||||
if (root.idledThisCycle) runProcess(wakeProcess, "omarchy-system-wake")
|
||||
if (root.idledThisCycle) runProcess(wakeProcess, "wake", "omarchy-system-wake")
|
||||
|
||||
root.idledThisCycle = false
|
||||
root.screensaverStartedThisCycle = false
|
||||
@@ -83,7 +110,9 @@ Item {
|
||||
function checkScreensaverAfterActiveSignal() {
|
||||
if (!root.idledThisCycle || !root.screensaverStartedThisCycle) return
|
||||
if (screensaverCheckProcess.running) return
|
||||
screensaverCheckProcess.command = ["bash", "-lc", "pgrep -f org.omarchy.screensaver >/dev/null && echo running || echo stopped"]
|
||||
root.lastScreensaverState = "checking"
|
||||
root.lastScreensaverCheckAt = nowIso()
|
||||
screensaverCheckProcess.command = ["bash", "-lc", "if hyprctl clients -j 2>/dev/null | jq -e '.[] | select(.class == \"org.omarchy.screensaver\" or .initialClass == \"org.omarchy.screensaver\")' >/dev/null; then echo running-window; elif pgrep -f '[o]rg.omarchy.screensaver' >/dev/null; then echo running-process; elif omarchy-toggle-enabled screensaver-off; then echo disabled; else echo stopped; fi"]
|
||||
screensaverCheckProcess.running = true
|
||||
}
|
||||
|
||||
@@ -94,14 +123,16 @@ Item {
|
||||
// the lock timer running while the screensaver is still alive so the lock
|
||||
// deadline remains `idle.lock` seconds from the original user idle time.
|
||||
if (root.screensaverStartedThisCycle) {
|
||||
logEvent("idle-monitor-active", "screensaver cycle remains armed")
|
||||
screensaverResumePollTimer.restart()
|
||||
return
|
||||
}
|
||||
|
||||
cancelIdleCycle()
|
||||
cancelIdleCycle("activity")
|
||||
}
|
||||
|
||||
function handleIdleChanged() {
|
||||
logEvent("idle-monitor", idleMonitor.isIdle ? "idle" : "active")
|
||||
if (!root.idleEnabled) return
|
||||
|
||||
if (idleMonitor.isIdle) startIdleCycle()
|
||||
@@ -113,9 +144,31 @@ Item {
|
||||
enabled: root.idleEnabled,
|
||||
idle: idleMonitor.isIdle,
|
||||
inIdleCycle: root.idledThisCycle,
|
||||
screensaverStarted: root.screensaverStartedThisCycle,
|
||||
sleepMonitor: sleepMonitorProcess.running,
|
||||
screensaver: root.screensaverTimeoutSeconds,
|
||||
lock: root.lockTimeoutSeconds
|
||||
lock: root.lockTimeoutSeconds,
|
||||
screensaverDelay: root.screensaverDelaySeconds,
|
||||
lockDelay: root.lockDelaySeconds,
|
||||
timers: {
|
||||
screensaver: screensaverTimer.running,
|
||||
lock: lockTimer.running,
|
||||
poll: screensaverResumePollTimer.running,
|
||||
sleepMonitorRestart: sleepMonitorRestartTimer.running
|
||||
},
|
||||
processes: {
|
||||
screensaver: screensaverProcess.running,
|
||||
lock: lockProcess.running,
|
||||
wake: wakeProcess.running,
|
||||
check: screensaverCheckProcess.running,
|
||||
sleepLock: sleepLockProcess.running,
|
||||
sleepWake: sleepWakeProcess.running
|
||||
},
|
||||
lastEvent: root.lastEvent,
|
||||
lastEventAt: root.lastEventAt,
|
||||
lastScreensaverState: root.lastScreensaverState,
|
||||
lastScreensaverCheckAt: root.lastScreensaverCheckAt,
|
||||
screensaverCheckCount: root.screensaverCheckCount
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,10 +176,10 @@ Item {
|
||||
if (!root.idleEnabled) return
|
||||
|
||||
if (preparing) {
|
||||
cancelIdleCycle()
|
||||
runProcess(sleepLockProcess, "OMARCHY_LOCK_ONLY=true omarchy-system-lock")
|
||||
cancelIdleCycle("sleep-preparing")
|
||||
runProcess(sleepLockProcess, "sleep-lock", "OMARCHY_LOCK_ONLY=true omarchy-system-lock")
|
||||
} else {
|
||||
runProcess(sleepWakeProcess, "sleep 1 && omarchy-system-wake")
|
||||
runProcess(sleepWakeProcess, "sleep-wake", "sleep 1 && omarchy-system-wake")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +188,8 @@ Item {
|
||||
if (persisted.idleEnabled === enabled) return enabled ? "enabled" : "disabled"
|
||||
|
||||
persisted.idleEnabled = enabled
|
||||
if (!enabled) cancelIdleCycle()
|
||||
logEvent("idle-enabled", enabled ? "enabled" : "disabled")
|
||||
if (!enabled) cancelIdleCycle("disabled")
|
||||
else Qt.callLater(root.handleIdleChanged)
|
||||
|
||||
return enabled ? "enabled" : "disabled"
|
||||
@@ -160,7 +214,7 @@ Item {
|
||||
id: lockTimer
|
||||
interval: root.lockDelaySeconds * 1000
|
||||
repeat: false
|
||||
onTriggered: if (root.idleEnabled && root.idledThisCycle) root.lockSystem()
|
||||
onTriggered: if (root.idleEnabled && root.idledThisCycle) root.lockSystem("lock-timeout")
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -170,22 +224,52 @@ Item {
|
||||
onTriggered: root.checkScreensaverAfterActiveSignal()
|
||||
}
|
||||
|
||||
Process { id: screensaverProcess }
|
||||
Process { id: lockProcess }
|
||||
Process { id: wakeProcess }
|
||||
Process { id: sleepLockProcess }
|
||||
Process { id: sleepWakeProcess }
|
||||
Process {
|
||||
id: screensaverProcess
|
||||
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "screensaver exitCode=" + exitCode + " status=" + exitStatus) }
|
||||
}
|
||||
Process {
|
||||
id: lockProcess
|
||||
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "lock exitCode=" + exitCode + " status=" + exitStatus) }
|
||||
}
|
||||
Process {
|
||||
id: wakeProcess
|
||||
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "wake exitCode=" + exitCode + " status=" + exitStatus) }
|
||||
}
|
||||
Process {
|
||||
id: sleepLockProcess
|
||||
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "sleep-lock exitCode=" + exitCode + " status=" + exitStatus) }
|
||||
}
|
||||
Process {
|
||||
id: sleepWakeProcess
|
||||
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "sleep-wake exitCode=" + exitCode + " status=" + exitStatus) }
|
||||
}
|
||||
|
||||
Process {
|
||||
id: screensaverCheckProcess
|
||||
stdout: StdioCollector {
|
||||
id: screensaverCheckStdout
|
||||
waitForEnd: true
|
||||
stdout: SplitParser {
|
||||
onRead: function(line) {
|
||||
root.lastScreensaverState = String(line || "").trim()
|
||||
root.lastScreensaverCheckAt = root.nowIso()
|
||||
root.screensaverCheckCount++
|
||||
root.logEvent("screensaver-check", root.lastScreensaverState)
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (!root.idledThisCycle || !root.screensaverStartedThisCycle) return
|
||||
if (String(screensaverCheckStdout.text || "").trim() === "running") return
|
||||
root.cancelIdleCycle()
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
root.logEvent("process-exit", "screensaver-check exitCode=" + exitCode + " status=" + exitStatus + " state=" + root.lastScreensaverState)
|
||||
if (!root.idleEnabled || !root.idledThisCycle || !root.screensaverStartedThisCycle) return
|
||||
|
||||
var state = String(root.lastScreensaverState || "").trim()
|
||||
if (state.indexOf("running") === 0) return
|
||||
if (state === "disabled") {
|
||||
screensaverResumePollTimer.stop()
|
||||
return
|
||||
}
|
||||
|
||||
// If the screensaver disappears while we're still in the idle cycle,
|
||||
// treat that as the user returning and require authentication before
|
||||
// showing the desktop again.
|
||||
root.lockSystem("screensaver-stopped state=" + state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,17 +284,28 @@ Item {
|
||||
else if (text === "boolean false") root.handleSleepPreparing(false)
|
||||
}
|
||||
}
|
||||
onExited: sleepMonitorRestartTimer.restart()
|
||||
onExited: {
|
||||
root.logEvent("sleep-monitor-exit", "restarting")
|
||||
sleepMonitorRestartTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: sleepMonitorRestartTimer
|
||||
interval: 5000
|
||||
repeat: false
|
||||
onTriggered: if (!sleepMonitorProcess.running) sleepMonitorProcess.running = true
|
||||
onTriggered: {
|
||||
if (!sleepMonitorProcess.running) {
|
||||
root.logEvent("sleep-monitor-restart")
|
||||
sleepMonitorProcess.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(root.handleIdleChanged)
|
||||
Component.onCompleted: {
|
||||
logEvent("service-ready")
|
||||
Qt.callLater(root.handleIdleChanged)
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "idle"
|
||||
@@ -219,6 +314,10 @@ Item {
|
||||
return root.statusJson()
|
||||
}
|
||||
|
||||
function debug(): string {
|
||||
return root.statusJson()
|
||||
}
|
||||
|
||||
function enable(): string {
|
||||
return root.setIdleEnabled(true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string backgroundPath: ""
|
||||
property int backgroundVersion: 0
|
||||
property bool fingerprintConfigured: false
|
||||
property bool authenticatingPassword: false
|
||||
property string failureMessage: ""
|
||||
property int failedAttempts: 0
|
||||
property bool inputEnabled: true
|
||||
property real scaleFactor: 1
|
||||
property bool hasTyped: false
|
||||
|
||||
readonly property string fingerprintGlyph: "\uDB80\uDE37"
|
||||
readonly property string placeholderText: fingerprintConfigured ? "Enter Password " + fingerprintGlyph : "Enter Password"
|
||||
readonly property real effectiveScale: Math.max(1, scaleFactor)
|
||||
readonly property int fieldWidth: Math.round(650 / effectiveScale)
|
||||
readonly property int fieldHeight: Math.round(100 / effectiveScale)
|
||||
readonly property int outlineThickness: Math.max(1, Math.round(4 / effectiveScale))
|
||||
readonly property int fieldFontSize: Style.font.heading
|
||||
|
||||
signal submitPassword(string password)
|
||||
|
||||
function withAlpha(color, alpha) {
|
||||
return Qt.rgba(color.r, color.g, color.b, alpha)
|
||||
}
|
||||
|
||||
function fileUrl(path) {
|
||||
if (!path) return ""
|
||||
var encoded = String(path).split("/").map(encodeURIComponent).join("/")
|
||||
return "file://" + encoded + "?v=" + backgroundVersion
|
||||
}
|
||||
|
||||
function forcePasswordFocus() {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
|
||||
function clearPassword() {
|
||||
passwordInput.text = ""
|
||||
hasTyped = false
|
||||
}
|
||||
|
||||
onInputEnabledChanged: {
|
||||
hasTyped = false
|
||||
if (inputEnabled) Qt.callLater(forcePasswordFocus)
|
||||
}
|
||||
Component.onCompleted: {
|
||||
hasTyped = false
|
||||
if (inputEnabled) Qt.callLater(forcePasswordFocus)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Color.background
|
||||
|
||||
Image {
|
||||
id: wallpaper
|
||||
anchors.fill: parent
|
||||
source: root.fileUrl(root.backgroundPath)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: false
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: wallpaper
|
||||
source: wallpaper
|
||||
blurEnabled: wallpaper.status === Image.Ready
|
||||
blur: 1.0
|
||||
blurMax: 64
|
||||
blurMultiplier: 1.0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.forcePasswordFocus()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: inputField
|
||||
width: root.fieldWidth
|
||||
height: root.fieldHeight
|
||||
anchors.centerIn: parent
|
||||
color: root.withAlpha(Color.background, 0.8)
|
||||
border.color: root.failureMessage.length > 0 ? Color.urgent : (root.authenticatingPassword ? Color.accent : Color.foreground)
|
||||
border.width: root.outlineThickness
|
||||
radius: Style.cornerRadius
|
||||
clip: true
|
||||
|
||||
TextInput {
|
||||
id: passwordInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.outlineThickness + 18
|
||||
anchors.rightMargin: root.outlineThickness + 18
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
activeFocusOnPress: true
|
||||
clip: true
|
||||
enabled: root.inputEnabled && !root.authenticatingPassword
|
||||
readOnly: root.authenticatingPassword
|
||||
echoMode: TextInput.Password
|
||||
passwordCharacter: "\u2022"
|
||||
passwordMaskDelay: 0
|
||||
color: Color.foreground
|
||||
selectionColor: root.withAlpha(Color.accent, 0.45)
|
||||
selectedTextColor: Color.foreground
|
||||
font.family: "monospace"
|
||||
font.pixelSize: root.fieldFontSize
|
||||
cursorVisible: activeFocus && !root.authenticatingPassword && root.hasTyped
|
||||
|
||||
onTextChanged: {
|
||||
if (text.length > 0) root.hasTyped = true
|
||||
if (text.length > 0 && root.failureMessage.length > 0) root.failureMessage = ""
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
var submitted = text
|
||||
text = ""
|
||||
root.hasTyped = false
|
||||
if (submitted.length > 0) root.submitPassword(submitted)
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape || (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_U)) {
|
||||
text = ""
|
||||
root.hasTyped = false
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: passwordInput
|
||||
text: root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText
|
||||
visible: passwordInput.text.length === 0
|
||||
color: root.failureMessage.length > 0 ? Color.urgent : Color.foreground
|
||||
font.family: "monospace"
|
||||
font.pixelSize: root.fieldFontSize
|
||||
font.italic: root.failureMessage.length > 0
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pam
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var shell: null
|
||||
property string omarchyPath: ""
|
||||
|
||||
readonly property string home: Quickshell.env("HOME")
|
||||
readonly property string userName: Quickshell.env("USER") || Quickshell.env("LOGNAME")
|
||||
readonly property string currentBackgroundLink: home + "/.config/omarchy/current/background"
|
||||
|
||||
property bool lockRequested: false
|
||||
property bool authenticatingPassword: false
|
||||
property bool fingerprintAuthenticating: false
|
||||
property bool passwordPamConfigured: false
|
||||
property bool fingerprintConfigured: false
|
||||
property bool previewVisible: false
|
||||
property string pendingPassword: ""
|
||||
property string failureMessage: ""
|
||||
property int failedAttempts: 0
|
||||
property string backgroundPath: ""
|
||||
property int backgroundVersion: 0
|
||||
|
||||
readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure
|
||||
readonly property bool authenticating: authenticatingPassword || fingerprintAuthenticating
|
||||
|
||||
function refreshBackground() {
|
||||
if (!readlinkProc.running) readlinkProc.running = true
|
||||
}
|
||||
|
||||
function refreshFingerprintStatus() {
|
||||
if (!fingerprintCheckProc.running) fingerprintCheckProc.running = true
|
||||
}
|
||||
|
||||
function resetAuthenticationState() {
|
||||
pendingPassword = ""
|
||||
failureMessage = ""
|
||||
failedAttempts = 0
|
||||
authenticatingPassword = false
|
||||
fingerprintAuthenticating = false
|
||||
fingerprintRetryTimer.stop()
|
||||
if (passwordPam.active) passwordPam.abort()
|
||||
if (fingerprintPam.active) fingerprintPam.abort()
|
||||
}
|
||||
|
||||
function beginLock() {
|
||||
if (!passwordPamConfigured) return false
|
||||
|
||||
resetAuthenticationState()
|
||||
refreshBackground()
|
||||
refreshFingerprintStatus()
|
||||
lockRequested = true
|
||||
sessionLock.locked = true
|
||||
return true
|
||||
}
|
||||
|
||||
function finishUnlock() {
|
||||
if (!root.locked && !lockRequested) return
|
||||
|
||||
lockRequested = false
|
||||
resetAuthenticationState()
|
||||
sessionLock.locked = false
|
||||
runWake()
|
||||
}
|
||||
|
||||
function runWake() {
|
||||
if (!wakeProcess.running) wakeProcess.running = true
|
||||
}
|
||||
|
||||
function submitPassword(value) {
|
||||
var password = String(value || "")
|
||||
if (!lockRequested || authenticatingPassword || password.length === 0) return
|
||||
|
||||
pendingPassword = password
|
||||
failureMessage = ""
|
||||
authenticatingPassword = true
|
||||
|
||||
if (!passwordPam.start()) {
|
||||
handlePasswordFailure()
|
||||
return
|
||||
}
|
||||
|
||||
Qt.callLater(respondToPasswordPrompt)
|
||||
}
|
||||
|
||||
function respondToPasswordPrompt() {
|
||||
if (!authenticatingPassword || !passwordPam.active || !passwordPam.responseRequired) return
|
||||
passwordPam.respond(pendingPassword)
|
||||
}
|
||||
|
||||
function handlePasswordFailure() {
|
||||
if (!lockRequested) return
|
||||
|
||||
authenticatingPassword = false
|
||||
pendingPassword = ""
|
||||
failedAttempts += 1
|
||||
failureMessage = "Authentication failed (" + failedAttempts + ")"
|
||||
}
|
||||
|
||||
function startFingerprint() {
|
||||
if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return
|
||||
if (fingerprintPam.active || fingerprintAuthenticating) return
|
||||
|
||||
fingerprintAuthenticating = true
|
||||
if (!fingerprintPam.start()) {
|
||||
fingerprintAuthenticating = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleFingerprintFinished(result) {
|
||||
fingerprintAuthenticating = false
|
||||
|
||||
if (!lockRequested) return
|
||||
if (result === PamResult.Success) {
|
||||
finishUnlock()
|
||||
} else if (fingerprintConfigured) {
|
||||
fingerprintRetryTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: sessionLock
|
||||
|
||||
locked: false
|
||||
|
||||
onSecureStateChanged: {
|
||||
if (secure) root.startFingerprint()
|
||||
}
|
||||
|
||||
onLockStateChanged: {
|
||||
if (!locked && root.lockRequested) {
|
||||
root.lockRequested = false
|
||||
root.resetAuthenticationState()
|
||||
root.runWake()
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: lockSurface
|
||||
color: Color.background
|
||||
|
||||
LockView {
|
||||
id: lockView
|
||||
anchors.fill: parent
|
||||
backgroundPath: root.backgroundPath
|
||||
backgroundVersion: root.backgroundVersion
|
||||
fingerprintConfigured: root.fingerprintConfigured
|
||||
authenticatingPassword: root.authenticatingPassword
|
||||
failureMessage: root.failureMessage
|
||||
failedAttempts: root.failedAttempts
|
||||
inputEnabled: root.lockRequested
|
||||
scaleFactor: lockSurface.screen && lockSurface.screen.devicePixelRatio > 0 ? lockSurface.screen.devicePixelRatio : 1
|
||||
onSubmitPassword: function(password) { root.submitPassword(password) }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: previewWindow
|
||||
visible: root.previewVisible
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "omarchy-lock-preview"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
LockView {
|
||||
anchors.fill: parent
|
||||
backgroundPath: root.backgroundPath
|
||||
backgroundVersion: root.backgroundVersion
|
||||
fingerprintConfigured: root.fingerprintConfigured
|
||||
authenticatingPassword: false
|
||||
failureMessage: ""
|
||||
failedAttempts: 0
|
||||
inputEnabled: false
|
||||
scaleFactor: previewWindow.screen && previewWindow.screen.devicePixelRatio > 0 ? previewWindow.screen.devicePixelRatio : 1
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: root.previewVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: passwordPam
|
||||
config: "omarchy-lock-password"
|
||||
user: root.userName
|
||||
|
||||
onResponseRequiredChanged: root.respondToPasswordPrompt()
|
||||
onPamMessage: root.respondToPasswordPrompt()
|
||||
|
||||
onCompleted: function(result) {
|
||||
root.authenticatingPassword = false
|
||||
root.pendingPassword = ""
|
||||
|
||||
if (!root.lockRequested) return
|
||||
if (result === PamResult.Success) root.finishUnlock()
|
||||
else root.handlePasswordFailure()
|
||||
}
|
||||
|
||||
onError: function(error) {
|
||||
root.handlePasswordFailure()
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: fingerprintPam
|
||||
config: "omarchy-lock-fingerprint"
|
||||
user: root.userName
|
||||
|
||||
onCompleted: function(result) {
|
||||
root.handleFingerprintFinished(result)
|
||||
}
|
||||
|
||||
onError: function(error) {
|
||||
root.fingerprintAuthenticating = false
|
||||
if (root.lockRequested && root.fingerprintConfigured) fingerprintRetryTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fingerprintRetryTimer
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: root.startFingerprint()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: readlinkProc
|
||||
command: ["readlink", "-f", root.currentBackgroundLink]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var next = String(text || "").trim()
|
||||
if (next !== root.backgroundPath) {
|
||||
root.backgroundPath = next
|
||||
root.backgroundVersion += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fingerprintCheckProc
|
||||
command: ["bash", "-lc", "if [[ -f /etc/pam.d/omarchy-lock-fingerprint ]] && command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"$USER\" 2>/dev/null | grep -qi finger; then echo yes; else echo no; fi"]
|
||||
stdout: StdioCollector { id: fingerprintCheckStdout; waitForEnd: true }
|
||||
onExited: {
|
||||
root.fingerprintConfigured = String(fingerprintCheckStdout.text || "").trim() === "yes"
|
||||
if (root.lockRequested && root.fingerprintConfigured) root.startFingerprint()
|
||||
else if (!root.fingerprintConfigured && fingerprintPam.active) fingerprintPam.abort()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wakeProcess
|
||||
command: ["bash", "-lc", "omarchy-system-wake"]
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/omarchy-lock-password"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onLoaded: root.passwordPamConfigured = true
|
||||
onLoadFailed: root.passwordPamConfigured = false
|
||||
onFileChanged: reload()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
refreshBackground()
|
||||
refreshFingerprintStatus()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "lock"
|
||||
|
||||
function lock(): string {
|
||||
if (!root.passwordPamConfigured) return "missing-pam"
|
||||
if (!root.locked && !root.beginLock()) return "failed"
|
||||
return "ok"
|
||||
}
|
||||
|
||||
function isLocked(): string {
|
||||
return root.locked ? "true" : "false"
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
return JSON.stringify({
|
||||
locked: root.locked,
|
||||
secure: sessionLock.secure,
|
||||
passwordPam: root.passwordPamConfigured,
|
||||
fingerprint: root.fingerprintConfigured,
|
||||
authenticating: root.authenticating
|
||||
})
|
||||
}
|
||||
|
||||
function preview(): string {
|
||||
root.refreshBackground()
|
||||
root.refreshFingerprintStatus()
|
||||
root.previewVisible = true
|
||||
return "ok"
|
||||
}
|
||||
|
||||
function hidePreview(): string {
|
||||
root.previewVisible = false
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.lock",
|
||||
"name": "Lock Screen",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Quickshell session lock with separate password and fingerprint PAM flows.",
|
||||
"kinds": [
|
||||
"service"
|
||||
],
|
||||
"keepLoaded": true,
|
||||
"entryPoints": {
|
||||
"service": "Service.qml"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user