|
|
|
@@ -0,0 +1,358 @@
|
|
|
|
|
import QtQuick
|
|
|
|
|
import Quickshell
|
|
|
|
|
import Quickshell.Io
|
|
|
|
|
import Quickshell.Services.Polkit
|
|
|
|
|
import Quickshell.Wayland
|
|
|
|
|
import qs.Commons
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
id: root
|
|
|
|
|
|
|
|
|
|
property string omarchyPath: ""
|
|
|
|
|
property var shell: null
|
|
|
|
|
property var manifest: null
|
|
|
|
|
|
|
|
|
|
property string fontFamily: Quickshell.env("OMARCHY_MENU_FONT") || "monospace"
|
|
|
|
|
property string styleFile: Quickshell.env("OMARCHY_MENU_STYLE_FILE") || (Quickshell.env("HOME") + "/.local/state/omarchy/toggles/quickshell-menu.json")
|
|
|
|
|
property color accent: Color.accent
|
|
|
|
|
property color background: Color.menu.background
|
|
|
|
|
property color foreground: Color.menu.text
|
|
|
|
|
property color border: foreground
|
|
|
|
|
property int cornerRadius: 0
|
|
|
|
|
property int contentMargin: 18
|
|
|
|
|
property int contentSpacing: 12
|
|
|
|
|
property int fieldHeight: 42
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
property bool fingerprintFirst: false
|
|
|
|
|
property int shakeOffset: 0
|
|
|
|
|
|
|
|
|
|
readonly property bool dialogVisible: polkitAgent.isActive || closing
|
|
|
|
|
readonly property bool fingerprintWaiting: dialogVisible && !responseRequired && !submitted && (fingerprintFirst || promptLooksFingerprint(currentPrompt + " " + currentSupplementary))
|
|
|
|
|
readonly property int cardWidth: Math.min(312, Math.max(260, panel.width - 48))
|
|
|
|
|
readonly property int cardHeight: panel.height > 0 ? Math.min(fieldHeight + contentMargin * 2, panel.height - 48) : fieldHeight + contentMargin * 2
|
|
|
|
|
|
|
|
|
|
function withAlpha(color, alpha) {
|
|
|
|
|
return Qt.rgba(color.r, color.g, color.b, alpha)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadStyle(raw) {
|
|
|
|
|
try {
|
|
|
|
|
var style = JSON.parse(raw || "{}")
|
|
|
|
|
root.cornerRadius = Number(style.radius || 0)
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function messageText() {
|
|
|
|
|
return "Authentication is needed..."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function promptLooksFingerprint(text) {
|
|
|
|
|
var s = String(text || "").toLowerCase()
|
|
|
|
|
return s.indexOf("finger") !== -1 || s.indexOf("fprint") !== -1 || s.indexOf("swipe") !== -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadPamConfig(raw) {
|
|
|
|
|
fingerprintFirst = false
|
|
|
|
|
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
|
|
|
|
|
fingerprintFirst = line.indexOf("pam_fprintd.so") !== -1
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 = ""
|
|
|
|
|
syncFromFlow()
|
|
|
|
|
Qt.callLater(refocus)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function refocus() {
|
|
|
|
|
if (!dialogVisible) return
|
|
|
|
|
if (fingerprintWaiting) 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: root.styleFile
|
|
|
|
|
watchChanges: true
|
|
|
|
|
onLoaded: root.loadStyle(text())
|
|
|
|
|
onFileChanged: { reload(); root.loadStyle(text()) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FileView {
|
|
|
|
|
path: "/etc/pam.d/polkit-1"
|
|
|
|
|
watchChanges: true
|
|
|
|
|
printErrors: false
|
|
|
|
|
onLoaded: root.loadPamConfig(text())
|
|
|
|
|
onLoadFailed: root.fingerprintFirst = false
|
|
|
|
|
onFileChanged: reload()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PolkitAgent {
|
|
|
|
|
id: polkitAgent
|
|
|
|
|
path: "/org/omarchy/PolkitAgent"
|
|
|
|
|
|
|
|
|
|
onAuthenticationRequestStarted: root.beginFlow()
|
|
|
|
|
onIsActiveChanged: {
|
|
|
|
|
if (isActive) root.syncFromFlow()
|
|
|
|
|
else if (!root.closing) root.resetSnapshot()
|
|
|
|
|
}
|
|
|
|
|
onIsRegisteredChanged: {
|
|
|
|
|
if (isRegistered) console.log("omarchy polkit agent registered")
|
|
|
|
|
else console.warn("omarchy 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: "omarchy-polkit"
|
|
|
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
|
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
|
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
color: root.withAlpha(root.background, 0.5)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
onClicked: root.refocus()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
id: card
|
|
|
|
|
width: root.cardWidth
|
|
|
|
|
height: root.cardHeight
|
|
|
|
|
radius: root.cornerRadius
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
anchors.horizontalCenterOffset: root.shakeOffset
|
|
|
|
|
color: root.background
|
|
|
|
|
border.color: root.accent
|
|
|
|
|
border.width: 2
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Row {
|
|
|
|
|
id: cardRow
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.margins: root.contentMargin
|
|
|
|
|
spacing: 14
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: "\uf023"
|
|
|
|
|
color: root.errorFlash ? Color.urgent : root.accent
|
|
|
|
|
font.family: root.fontFamily
|
|
|
|
|
font.pixelSize: 20
|
|
|
|
|
width: 26
|
|
|
|
|
height: root.fieldHeight
|
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
width: parent.width - 40
|
|
|
|
|
height: root.fieldHeight
|
|
|
|
|
|
|
|
|
|
TextInput {
|
|
|
|
|
id: passwordInput
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
|
|
|
activeFocusOnPress: true
|
|
|
|
|
clip: true
|
|
|
|
|
selectionColor: root.withAlpha(root.accent, 0.45)
|
|
|
|
|
selectedTextColor: root.foreground
|
|
|
|
|
font.family: root.fontFamily
|
|
|
|
|
font.pixelSize: 19
|
|
|
|
|
echoMode: root.responseVisible ? TextInput.Normal : TextInput.Password
|
|
|
|
|
passwordCharacter: "\u2022"
|
|
|
|
|
color: root.errorFlash ? Color.urgent : root.foreground
|
|
|
|
|
cursorVisible: activeFocus && !root.submitted && !root.errorFlash
|
|
|
|
|
readOnly: root.submitted || root.errorFlash
|
|
|
|
|
enabled: root.dialogVisible && !root.fingerprintWaiting
|
|
|
|
|
visible: !root.fingerprintWaiting
|
|
|
|
|
onAccepted: root.submitResponse()
|
|
|
|
|
Keys.onPressed: function(event) {
|
|
|
|
|
if (event.key === Qt.Key_Escape) {
|
|
|
|
|
root.cancelRequest()
|
|
|
|
|
event.accepted = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
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.urgent : root.foreground
|
|
|
|
|
opacity: root.errorFlash ? 1 : 0.36
|
|
|
|
|
font.family: root.fontFamily
|
|
|
|
|
font.pixelSize: 19
|
|
|
|
|
elide: Text.ElideRight
|
|
|
|
|
visible: passwordInput.visible && passwordInput.text.length === 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
width: 2
|
|
|
|
|
height: 24
|
|
|
|
|
anchors.left: parent.left
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
color: root.errorFlash ? Color.urgent : 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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|