Fork the desktop off Omarchy as a self-contained system
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Polkit
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
import "PolkitModel.js" as PolkitModel
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string fontFamily: Style.font.menuFamily
|
||||
// Bound to the central [polkit] section in shell.toml via Color.qml.
|
||||
property color accent: Color.polkit.accent
|
||||
property color background: Color.polkit.background
|
||||
property color foreground: Color.polkit.text
|
||||
property color border: Color.polkit.border
|
||||
property color borderError: Color.polkit.borderError
|
||||
property var borderSpec: Border.surfaceSpec("polkit", errorFlash ? "border-error" : "border", errorFlash ? borderError : border, Math.max(1, Style.space(2)), "border-alpha")
|
||||
property color scrim: Color.polkit.scrim
|
||||
readonly property int cornerRadius: Style.cornerRadius
|
||||
property int contentMargin: Style.spacing.panelPadding
|
||||
property int fieldHeight: Math.max(Style.space(42), Style.spacing.controlHeight)
|
||||
|
||||
property bool closing: false
|
||||
property bool submitted: false
|
||||
property string currentMessage: ""
|
||||
property string currentPrompt: ""
|
||||
property string currentSupplementary: ""
|
||||
property bool responseRequired: false
|
||||
property bool responseVisible: false
|
||||
property bool failed: false
|
||||
property bool errorFlash: false
|
||||
// pam_fprintd appears in the polkit PAM stack (a sensor is enrolled).
|
||||
property bool fingerprintConfigured: false
|
||||
// Lid shut right now — the reader is physically unreachable, so we fall back
|
||||
// to the password even when a sensor is enrolled. Refreshed per request.
|
||||
property bool laptopClosed: false
|
||||
property int shakeOffset: 0
|
||||
|
||||
readonly property bool dialogVisible: polkitAgent.isActive || closing
|
||||
// We show one method at a time. Fingerprint owns the dialog while PAM is
|
||||
// waiting on the reader (lid open, sensor enrolled); the moment PAM asks for
|
||||
// a password — including immediately when the lid is shut and the clamshell
|
||||
// gate skips pam_fprintd — we switch to the password field instead.
|
||||
readonly property bool fingerprintMode: fingerprintConfigured && !laptopClosed && dialogVisible && !responseRequired && !submitted && !errorFlash
|
||||
readonly property int cardHeight: panel.height > 0 ? Math.min(fieldHeight + contentMargin * 2, panel.height - Style.gapsOut * 2) : fieldHeight + contentMargin * 2
|
||||
// Password mode is a wide field; fingerprint mode collapses to a square that
|
||||
// just frames the centered sensor icon.
|
||||
readonly property int cardWidth: fingerprintMode ? cardHeight : Math.min(Style.space(312), Math.max(Style.space(260), panel.width - Style.gapsOut * 2))
|
||||
|
||||
function authorizationLabel(message) {
|
||||
return PolkitModel.authorizationLabel(message)
|
||||
}
|
||||
|
||||
function loadPamConfig(raw) {
|
||||
fingerprintConfigured = PolkitModel.fingerprintConfiguredFromPamConfig(raw)
|
||||
}
|
||||
|
||||
function refreshLidState() {
|
||||
if (!laptopClosedProc.running) laptopClosedProc.running = true
|
||||
}
|
||||
|
||||
function resetSnapshot() {
|
||||
currentMessage = ""
|
||||
currentPrompt = ""
|
||||
currentSupplementary = ""
|
||||
responseRequired = false
|
||||
responseVisible = false
|
||||
failed = false
|
||||
errorFlash = false
|
||||
submitted = false
|
||||
passwordInput.text = ""
|
||||
}
|
||||
|
||||
function syncFromFlow() {
|
||||
var flow = polkitAgent.flow
|
||||
if (!flow) return
|
||||
|
||||
currentMessage = String(flow.message || "Authentication is needed...")
|
||||
currentPrompt = String(flow.inputPrompt || "")
|
||||
currentSupplementary = String(flow.supplementaryMessage || "")
|
||||
responseRequired = !!flow.isResponseRequired
|
||||
responseVisible = !!flow.responseVisible
|
||||
failed = !!flow.failed
|
||||
|
||||
if (responseRequired) submitted = false
|
||||
}
|
||||
|
||||
function beginFlow() {
|
||||
closeTimer.stop()
|
||||
closing = false
|
||||
submitted = false
|
||||
passwordInput.text = ""
|
||||
refreshLidState()
|
||||
syncFromFlow()
|
||||
Qt.callLater(refocus)
|
||||
}
|
||||
|
||||
function refocus() {
|
||||
if (!dialogVisible) return
|
||||
// In fingerprint mode there is no field to type into — park focus on the
|
||||
// key catcher so Escape still cancels; otherwise focus the password field.
|
||||
if (fingerprintMode) keyCatcher.forceActiveFocus()
|
||||
else passwordInput.forceActiveFocus()
|
||||
}
|
||||
|
||||
function submitResponse() {
|
||||
var flow = polkitAgent.flow
|
||||
if (!flow || !flow.isResponseRequired) return
|
||||
submitted = true
|
||||
errorFlash = false
|
||||
flow.submit(passwordInput.text)
|
||||
passwordInput.text = ""
|
||||
keyCatcher.forceActiveFocus()
|
||||
}
|
||||
|
||||
function cancelRequest() {
|
||||
var flow = polkitAgent.flow
|
||||
passwordInput.text = ""
|
||||
submitted = false
|
||||
closing = true
|
||||
closeTimer.restart()
|
||||
if (flow) flow.cancelAuthenticationRequest()
|
||||
}
|
||||
|
||||
function triggerFailureFeedback() {
|
||||
submitted = false
|
||||
errorFlash = true
|
||||
passwordInput.text = ""
|
||||
errorTimer.restart()
|
||||
shakeAnimation.restart()
|
||||
Qt.callLater(refocus)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
closing = false
|
||||
resetSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: errorTimer
|
||||
interval: 1200
|
||||
repeat: false
|
||||
onTriggered: root.errorFlash = false
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: shakeAnimation
|
||||
NumberAnimation { target: root; property: "shakeOffset"; to: -8; duration: 35; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: root; property: "shakeOffset"; to: 8; duration: 50; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { target: root; property: "shakeOffset"; to: 0; duration: 55; easing.type: Easing.OutQuad }
|
||||
}
|
||||
FileView {
|
||||
path: "/etc/pam.d/polkit-1"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onLoaded: root.loadPamConfig(text())
|
||||
onLoadFailed: root.fingerprintConfigured = false
|
||||
onFileChanged: reload()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopClosedProc
|
||||
command: ["bash", "-c", "blob-hw-laptop-closed && echo closed || echo open"]
|
||||
stdout: StdioCollector { id: laptopClosedOut; waitForEnd: true }
|
||||
onExited: root.laptopClosed = String(laptopClosedOut.text || "").trim() === "closed"
|
||||
}
|
||||
|
||||
PolkitAgent {
|
||||
id: polkitAgent
|
||||
path: "/org/blob/PolkitAgent"
|
||||
|
||||
onAuthenticationRequestStarted: root.beginFlow()
|
||||
onIsActiveChanged: {
|
||||
if (isActive) root.syncFromFlow()
|
||||
else if (!root.closing) root.resetSnapshot()
|
||||
}
|
||||
onIsRegisteredChanged: {
|
||||
if (isRegistered) console.log("blob polkit agent registered")
|
||||
else console.warn("blob polkit agent is not registered; another agent may be running")
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: polkitAgent.flow
|
||||
|
||||
function onIsResponseRequiredChanged() {
|
||||
root.syncFromFlow()
|
||||
if (!polkitAgent.flow || !polkitAgent.flow.isResponseRequired) passwordInput.text = ""
|
||||
Qt.callLater(root.refocus)
|
||||
}
|
||||
|
||||
function onInputPromptChanged() { root.syncFromFlow() }
|
||||
function onResponseVisibleChanged() { root.syncFromFlow() }
|
||||
function onSupplementaryMessageChanged() { root.syncFromFlow() }
|
||||
function onFailedChanged() { root.syncFromFlow() }
|
||||
|
||||
function onAuthenticationFailed() {
|
||||
root.syncFromFlow()
|
||||
root.triggerFailureFeedback()
|
||||
}
|
||||
|
||||
function onAuthenticationSucceeded() {
|
||||
root.closing = true
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
function onAuthenticationRequestCancelled() {
|
||||
root.closing = true
|
||||
closeTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: panel
|
||||
visible: root.dialogVisible
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "blob-polkit"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.scrim
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.refocus()
|
||||
}
|
||||
|
||||
BorderSurface {
|
||||
id: card
|
||||
width: root.cardWidth
|
||||
height: root.cardHeight
|
||||
radius: root.cornerRadius
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: root.shakeOffset
|
||||
color: root.background
|
||||
borderSpec: root.borderSpec
|
||||
padding: root.contentMargin
|
||||
|
||||
MouseArea { anchors.fill: parent; onClicked: root.refocus() }
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.priority: Keys.BeforeItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.cancelRequest()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (root.responseRequired) root.submitResponse()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fingerprint mode shows just the sensor icon, centered and alone \u2014 no
|
||||
// padlock, no field, no prompt text.
|
||||
OpticalGlyph {
|
||||
anchors.centerIn: parent
|
||||
width: Math.round(root.fieldHeight * 0.7)
|
||||
height: width
|
||||
visible: root.fingerprintMode
|
||||
text: "\udb80\ude37"
|
||||
fontFamily: root.fontFamily
|
||||
fontSize: Math.round(root.fieldHeight * 0.7)
|
||||
color: root.errorFlash ? Color.polkit.textError : root.accent
|
||||
}
|
||||
|
||||
Row {
|
||||
id: cardRow
|
||||
visible: !root.fingerprintMode
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: card.contentTopInset
|
||||
anchors.rightMargin: card.contentRightInset
|
||||
anchors.bottomMargin: card.contentBottomInset
|
||||
anchors.leftMargin: card.contentLeftInset
|
||||
spacing: Style.space(14)
|
||||
|
||||
Text {
|
||||
text: "\uf023"
|
||||
color: root.errorFlash ? Color.polkit.textError : root.accent
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.iconLarge
|
||||
width: Style.space(26)
|
||||
height: root.fieldHeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - Style.space(40)
|
||||
height: root.fieldHeight
|
||||
|
||||
TextInput {
|
||||
id: passwordInput
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
activeFocusOnPress: true
|
||||
clip: true
|
||||
selectionColor: Util.alpha(root.accent, 0.45)
|
||||
selectedTextColor: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.iconLarge
|
||||
echoMode: root.responseVisible ? TextInput.Normal : TextInput.Password
|
||||
passwordCharacter: "\u2022"
|
||||
color: root.errorFlash ? Color.polkit.textError : root.foreground
|
||||
cursorVisible: activeFocus && !root.submitted && !root.errorFlash
|
||||
readOnly: root.submitted || root.errorFlash
|
||||
enabled: root.dialogVisible
|
||||
onAccepted: root.submitResponse()
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.cancelRequest()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.errorFlash ? "Wrong" : (root.submitted ? "Checking..." : "Enter password")
|
||||
color: root.errorFlash ? Color.polkit.textError : root.foreground
|
||||
opacity: root.errorFlash ? 1 : 0.36
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.iconLarge
|
||||
elide: Text.ElideRight
|
||||
visible: passwordInput.text.length === 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(1, Style.space(2))
|
||||
height: Style.space(24)
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.errorFlash ? Color.polkit.textError : root.foreground
|
||||
visible: passwordInput.visible && passwordInput.activeFocus && passwordInput.text.length === 0 && !root.submitted && !root.errorFlash
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
enabled: passwordInput.visible
|
||||
onClicked: passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(justificationText.implicitWidth + Style.space(24), panel.width - Style.gapsOut * 2)
|
||||
height: Style.space(28)
|
||||
anchors.horizontalCenter: card.horizontalCenter
|
||||
anchors.bottom: card.top
|
||||
anchors.bottomMargin: Style.space(10)
|
||||
radius: root.cornerRadius
|
||||
color: root.background
|
||||
|
||||
Text {
|
||||
id: justificationText
|
||||
textFormat: Text.PlainText
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.space(12)
|
||||
anchors.rightMargin: Style.space(12)
|
||||
text: root.authorizationLabel(root.currentMessage)
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
function promptLooksFingerprint(text) {
|
||||
var s = String(text || "").toLowerCase()
|
||||
return s.indexOf("finger") !== -1 || s.indexOf("fprint") !== -1 || s.indexOf("swipe") !== -1
|
||||
}
|
||||
|
||||
function fingerprintConfiguredFromPamConfig(raw) {
|
||||
// Fingerprint is available whenever pam_fprintd appears anywhere in the auth
|
||||
// stack — it need not be the first module. A clamshell gate (pam_exec) may
|
||||
// legitimately precede it to skip fingerprint while the lid is closed.
|
||||
var lines = String(raw || "").split("\n")
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].replace(/^\s+|\s+$/g, "")
|
||||
if (!line || line.charAt(0) === "#") continue
|
||||
if (!line.match(/^auth\s+/)) continue
|
||||
if (line.indexOf("pam_fprintd.so") !== -1) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function authorizationLabel(message) {
|
||||
var text = String(message || "")
|
||||
var match = text.match(/^Authentication is (?:needed|required) to run [`']([^`']+)[`'] as /i)
|
||||
return match ? "Authorize running '" + match[1] + "'" : text
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
promptLooksFingerprint: promptLooksFingerprint,
|
||||
fingerprintConfiguredFromPamConfig: fingerprintConfiguredFromPamConfig,
|
||||
authorizationLabel: authorizationLabel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "blob.polkit",
|
||||
"name": "Polkit Agent",
|
||||
"version": "1.0.0",
|
||||
"author": "Blob",
|
||||
"description": "Theme-aware authentication dialog for privileged actions.",
|
||||
"blob": {
|
||||
"capabilities": [
|
||||
"authentication"
|
||||
]
|
||||
},
|
||||
"kinds": [
|
||||
"service"
|
||||
],
|
||||
"keepLoaded": true,
|
||||
"entryPoints": {
|
||||
"service": "PolkitAgent.qml"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user