pragma Singleton pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io import qs.Common import qs.Services Singleton { id: root property var settingsRoot: null property string greetdPamText: "" property string systemAuthPamText: "" property string commonAuthPamText: "" property string passwordAuthPamText: "" property string systemLoginPamText: "" property string systemLocalLoginPamText: "" property string commonAuthPcPamText: "" property string loginPamText: "" property string dankshellU2fPamText: "" property string u2fKeysText: "" property string fingerprintProbeOutput: "" property int fingerprintProbeExitCode: 0 property bool fingerprintProbeStreamFinished: false property bool fingerprintProbeExited: false property string fingerprintProbeState: "probe_failed" property string pamSupportProbeOutput: "" property bool pamSupportProbeStreamFinished: false property bool pamSupportProbeExited: false property int pamSupportProbeExitCode: 0 property bool pamFprintSupportDetected: false property bool pamU2fSupportDetected: false readonly property string homeDir: Quickshell.env("HOME") || "" readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : "" readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== "" readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f") readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f") function envFlag(name) { const value = (Quickshell.env(name) || "").trim().toLowerCase(); if (value === "1" || value === "true" || value === "yes" || value === "on") return true; if (value === "0" || value === "false" || value === "no" || value === "off") return false; return null; } readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE") readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE") property bool authApplyRunning: false property bool authApplyQueued: false property bool authApplyRerunRequested: false property bool authApplyTerminalFallbackFromPrecheck: false property string authApplyStdout: "" property string authApplyStderr: "" property string authApplySudoProbeStderr: "" property string authApplyTerminalFallbackStderr: "" function detectQtTools() { qtToolsDetectionProcess.running = true; } function detectAuthCapabilities() { if (!settingsRoot) return; if (forcedFprintAvailable === null) { fingerprintProbeOutput = ""; fingerprintProbeStreamFinished = false; fingerprintProbeExited = false; fingerprintProbeProcess.running = true; } else { fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed"; } pamFprintSupportDetected = false; pamU2fSupportDetected = false; pamSupportProbeOutput = ""; pamSupportProbeStreamFinished = false; pamSupportProbeExited = false; pamSupportDetectionProcess.running = true; recomputeAuthCapabilities(); } function detectFprintd() { detectAuthCapabilities(); } function detectU2f() { detectAuthCapabilities(); } function checkPluginSettings() { pluginSettingsCheckProcess.running = true; } function scheduleAuthApply() { if (!settingsRoot || settingsRoot.isGreeterMode) return; authApplyQueued = true; if (authApplyRunning) { authApplyRerunRequested = true; return; } authApplyDebounce.restart(); } function beginAuthApply() { if (!authApplyQueued || authApplyRunning || !settingsRoot || settingsRoot.isGreeterMode) return; authApplyQueued = false; authApplyRerunRequested = false; authApplyStdout = ""; authApplyStderr = ""; authApplySudoProbeStderr = ""; authApplyTerminalFallbackStderr = ""; authApplyTerminalFallbackFromPrecheck = false; authApplyRunning = true; authApplySudoProbeProcess.running = true; } function launchAuthApplyTerminalFallback(fromPrecheck, details) { authApplyTerminalFallbackFromPrecheck = fromPrecheck; if (details && details !== "") ToastService.showInfo(I18n.tr("Authentication changes need sudo. Opening terminal so you can use password or fingerprint."), details, "", "auth-sync"); authApplyTerminalFallbackStderr = ""; authApplyTerminalFallbackProcess.running = true; } function finishAuthApply() { const shouldRerun = authApplyQueued || authApplyRerunRequested; authApplyRunning = false; authApplyRerunRequested = false; if (shouldRerun) authApplyDebounce.restart(); } function stripPamComment(line) { if (!line) return ""; const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) return ""; const hashIdx = trimmed.indexOf("#"); if (hashIdx >= 0) return trimmed.substring(0, hashIdx).trim(); return trimmed; } function pamModuleEnabled(pamText, moduleName) { if (!pamText || !moduleName) return false; const lines = pamText.split(/\r?\n/); for (let i = 0; i < lines.length; i++) { const line = stripPamComment(lines[i]); if (!line) continue; if (line.includes(moduleName)) return true; } return false; } function pamTextIncludesFile(pamText, filename) { if (!pamText || !filename) return false; const lines = pamText.split(/\r?\n/); for (let i = 0; i < lines.length; i++) { const line = stripPamComment(lines[i]); if (!line) continue; if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include"))) return true; } return false; } function greeterPamStackHasModule(moduleName) { if (pamModuleEnabled(greetdPamText, moduleName)) return true; const includedPamStacks = [ ["system-auth", systemAuthPamText], ["common-auth", commonAuthPamText], ["password-auth", passwordAuthPamText], ["system-login", systemLoginPamText], ["system-local-login", systemLocalLoginPamText], ["common-auth-pc", commonAuthPcPamText], ["login", loginPamText] ]; for (let i = 0; i < includedPamStacks.length; i++) { const stack = includedPamStacks[i]; if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName)) return true; } return false; } function hasEnrolledFingerprintOutput(output) { const lower = (output || "").toLowerCase(); if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled")) return true; const lines = lower.split(/\r?\n/); for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (trimmed.startsWith("finger:")) return true; if (trimmed.startsWith("- ") && trimmed.includes("finger")) return true; } return false; } function hasMissingFingerprintEnrollmentOutput(output) { const lower = (output || "").toLowerCase(); return lower.includes("no fingers enrolled") || lower.includes("no fingerprints enrolled") || lower.includes("no prints enrolled"); } function hasMissingFingerprintReaderOutput(output) { const lower = (output || "").toLowerCase(); return lower.includes("no devices available") || lower.includes("no device available") || lower.includes("no devices found") || lower.includes("list_devices failed") || lower.includes("no device"); } function parseFingerprintProbe(exitCode, output) { if (hasEnrolledFingerprintOutput(output)) return "ready"; if (hasMissingFingerprintEnrollmentOutput(output)) return "missing_enrollment"; if (hasMissingFingerprintReaderOutput(output)) return "missing_reader"; if (exitCode === 0) return "missing_enrollment"; if (exitCode === 127 || (output || "").includes("__missing_command__")) return "probe_failed"; return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support"; } function setLockFingerprintCapability(canEnable, ready, reason) { settingsRoot.lockFingerprintCanEnable = canEnable; settingsRoot.lockFingerprintReady = ready; settingsRoot.lockFingerprintReason = reason; } function setLockU2fCapability(canEnable, ready, reason) { settingsRoot.lockU2fCanEnable = canEnable; settingsRoot.lockU2fReady = ready; settingsRoot.lockU2fReason = reason; } function setGreeterFingerprintCapability(canEnable, ready, reason, source) { settingsRoot.greeterFingerprintCanEnable = canEnable; settingsRoot.greeterFingerprintReady = ready; settingsRoot.greeterFingerprintReason = reason; settingsRoot.greeterFingerprintSource = source; } function setGreeterU2fCapability(canEnable, ready, reason, source) { settingsRoot.greeterU2fCanEnable = canEnable; settingsRoot.greeterU2fReady = ready; settingsRoot.greeterU2fReason = reason; settingsRoot.greeterU2fSource = source; } function recomputeFingerprintCapabilities() { if (forcedFprintAvailable !== null) { const reason = forcedFprintAvailable ? "ready" : "probe_failed"; const source = forcedFprintAvailable ? "dms" : "none"; setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason); setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source); return; } const state = fingerprintProbeState; switch (state) { case "ready": setLockFingerprintCapability(true, true, "ready"); break; case "missing_enrollment": setLockFingerprintCapability(true, false, "missing_enrollment"); break; case "missing_reader": setLockFingerprintCapability(false, false, "missing_reader"); break; case "missing_pam_support": setLockFingerprintCapability(false, false, "missing_pam_support"); break; default: setLockFingerprintCapability(false, false, "probe_failed"); break; } if (greeterPamHasFprint) { switch (state) { case "ready": setGreeterFingerprintCapability(true, true, "configured_externally", "pam"); break; case "missing_enrollment": setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam"); break; case "missing_reader": setGreeterFingerprintCapability(false, false, "missing_reader", "pam"); break; default: setGreeterFingerprintCapability(true, false, "probe_failed", "pam"); break; } return; } switch (state) { case "ready": setGreeterFingerprintCapability(true, true, "ready", "dms"); break; case "missing_enrollment": setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms"); break; case "missing_reader": setGreeterFingerprintCapability(false, false, "missing_reader", "none"); break; case "missing_pam_support": setGreeterFingerprintCapability(false, false, "missing_pam_support", "none"); break; default: setGreeterFingerprintCapability(false, false, "probe_failed", "none"); break; } } function recomputeU2fCapabilities() { if (forcedU2fAvailable !== null) { const reason = forcedU2fAvailable ? "ready" : "probe_failed"; const source = forcedU2fAvailable ? "dms" : "none"; setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason); setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source); return; } const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected; const lockCanEnable = lockReady || pamU2fSupportDetected; const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support"); setLockU2fCapability(lockCanEnable, lockReady, lockReason); if (greeterPamHasU2f) { setGreeterU2fCapability(true, true, "configured_externally", "pam"); return; } const greeterReady = homeU2fKeysDetected; const greeterCanEnable = greeterReady || pamU2fSupportDetected; const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support"); setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none"); } function recomputeAuthCapabilities() { if (!settingsRoot) return; recomputeFingerprintCapabilities(); recomputeU2fCapabilities(); settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady; settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady; } function finalizeFingerprintProbe() { if (!fingerprintProbeStreamFinished || !fingerprintProbeExited) return; fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput); recomputeAuthCapabilities(); } function finalizePamSupportProbe() { if (!pamSupportProbeStreamFinished || !pamSupportProbeExited) return; pamFprintSupportDetected = false; pamU2fSupportDetected = false; const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/); for (let i = 0; i < lines.length; i++) { const parts = lines[i].split(":"); if (parts.length !== 2) continue; if (parts[0] === "pam_fprintd.so") pamFprintSupportDetected = parts[1] === "true"; else if (parts[0] === "pam_u2f.so") pamU2fSupportDetected = parts[1] === "true"; } if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support") fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput); recomputeAuthCapabilities(); } property var qtToolsDetectionProcess: Process { command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] running: false stdout: StdioCollector { onStreamFinished: { if (!settingsRoot) return; if (text && text.trim()) { const lines = text.trim().split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith("qt5ct:")) { settingsRoot.qt5ctAvailable = line.split(":")[1] === "true"; } else if (line.startsWith("qt6ct:")) { settingsRoot.qt6ctAvailable = line.split(":")[1] === "true"; } else if (line.startsWith("gtk:")) { settingsRoot.gtkAvailable = line.split(":")[1] === "true"; } } } } } } property var fingerprintProbeProcess: Process { command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"] running: false stdout: StdioCollector { onStreamFinished: { root.fingerprintProbeOutput = text || ""; root.fingerprintProbeStreamFinished = true; root.finalizeFingerprintProbe(); } } onExited: function (exitCode) { root.fingerprintProbeExitCode = exitCode; root.fingerprintProbeExited = true; root.finalizeFingerprintProbe(); } } property var pamSupportDetectionProcess: Process { command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"] running: false stdout: StdioCollector { onStreamFinished: { root.pamSupportProbeOutput = text || ""; root.pamSupportProbeStreamFinished = true; root.finalizePamSupportProbe(); } } onExited: function (exitCode) { root.pamSupportProbeExitCode = exitCode; root.pamSupportProbeExited = true; root.finalizePamSupportProbe(); } } Timer { id: authApplyDebounce interval: 300 repeat: false onTriggered: root.beginAuthApply() } property var authApplyProcess: Process { command: ["dms", "auth", "sync", "--yes"] running: false stdout: StdioCollector { onStreamFinished: root.authApplyStdout = text || "" } stderr: StdioCollector { onStreamFinished: root.authApplyStderr = text || "" } onExited: exitCode => { const out = (root.authApplyStdout || "").trim(); const err = (root.authApplyStderr || "").trim(); if (exitCode === 0) { let details = out; if (err !== "") details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err; ToastService.showInfo(I18n.tr("Authentication changes applied."), details, "", "auth-sync"); root.detectAuthCapabilities(); root.finishAuthApply(); return; } let details = ""; if (out !== "") details = out; if (err !== "") details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err; ToastService.showWarning(I18n.tr("Background authentication sync failed. Trying terminal mode."), details, "", "auth-sync"); root.launchAuthApplyTerminalFallback(false, ""); } } property var authApplySudoProbeProcess: Process { command: ["sudo", "-n", "true"] running: false stderr: StdioCollector { onStreamFinished: root.authApplySudoProbeStderr = text || "" } onExited: exitCode => { const err = (root.authApplySudoProbeStderr || "").trim(); if (exitCode === 0) { ToastService.showInfo(I18n.tr("Applying authentication changes…"), "", "", "auth-sync"); root.authApplyProcess.running = true; return; } root.launchAuthApplyTerminalFallback(true, err); } } property var authApplyTerminalFallbackProcess: Process { command: ["dms", "auth", "sync", "--terminal", "--yes"] running: false stderr: StdioCollector { onStreamFinished: root.authApplyTerminalFallbackStderr = text || "" } onExited: exitCode => { if (exitCode === 0) { const message = root.authApplyTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done."); ToastService.showInfo(message, "", "", "auth-sync"); } else { let details = (root.authApplyTerminalFallbackStderr || "").trim(); ToastService.showError(I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync"); } root.finishAuthApply(); } } FileView { id: greetdPamWatcher path: "/etc/pam.d/greetd" printErrors: false onLoaded: { root.greetdPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.greetdPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: systemAuthPamWatcher path: "/etc/pam.d/system-auth" printErrors: false onLoaded: { root.systemAuthPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.systemAuthPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: commonAuthPamWatcher path: "/etc/pam.d/common-auth" printErrors: false onLoaded: { root.commonAuthPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.commonAuthPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: passwordAuthPamWatcher path: "/etc/pam.d/password-auth" printErrors: false onLoaded: { root.passwordAuthPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.passwordAuthPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: systemLoginPamWatcher path: "/etc/pam.d/system-login" printErrors: false onLoaded: { root.systemLoginPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.systemLoginPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: systemLocalLoginPamWatcher path: "/etc/pam.d/system-local-login" printErrors: false onLoaded: { root.systemLocalLoginPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.systemLocalLoginPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: commonAuthPcPamWatcher path: "/etc/pam.d/common-auth-pc" printErrors: false onLoaded: { root.commonAuthPcPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.commonAuthPcPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: loginPamWatcher path: "/etc/pam.d/login" printErrors: false onLoaded: { root.loginPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.loginPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: dankshellU2fPamWatcher path: "/etc/pam.d/dankshell-u2f" printErrors: false onLoaded: { root.dankshellU2fPamText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.dankshellU2fPamText = ""; root.recomputeAuthCapabilities(); } } FileView { id: u2fKeysWatcher path: root.u2fKeysPath printErrors: false onLoaded: { root.u2fKeysText = text(); root.recomputeAuthCapabilities(); } onLoadFailed: { root.u2fKeysText = ""; root.recomputeAuthCapabilities(); } } property var pluginSettingsCheckProcess: Process { command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""] running: false onExited: function (exitCode) { if (!settingsRoot) return; settingsRoot.pluginSettingsFileExists = (exitCode === 0); } } }