import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Pam import Quickshell.Wayland import qs.Commons import "FingerprintModel.js" as FingerprintModel Item { id: root property var shell: null property string omarchyPath: "" readonly property string home: Quickshell.env("HOME") readonly property string stateHome: home + "/.local/state" readonly property string userName: Quickshell.env("USER") || Quickshell.env("LOGNAME") readonly property string currentBackgroundLink: stateHome + "/omarchy/current/background" property bool lockRequested: false property bool pendingSessionLock: false property bool authenticatingPassword: false property bool fingerprintAuthenticating: false property bool passwordPamConfigured: false property bool fingerprintConfigured: false property int fingerprintUnreachedStreak: 0 property bool fingerprintAttemptReachedDevice: false property bool fingerprintAttemptFastError: false property double fingerprintAttemptPromptedAtMs: 0 property double fingerprintLastNudgeMs: 0 property double fingerprintLastSettleMs: 0 property double fingerprintResumedAtMs: 0 property int fingerprintProbeStreak: 0 property bool previewVisible: false property string enteredPassword: "" property string pendingPassword: "" property string failureMessage: "" property int failedAttempts: 0 property string backgroundPath: "" property string videoPosterPath: "" property int backgroundVersion: 0 // The wallpaper file's mtime and size. The lock caches its wallpaper by // version, so a file overwritten in place must bump the version too. property string backgroundSignature: "" property string lastEvent: "init" property string lastEventAt: "" property bool displaysBlank: false // displaysBlank tracks what the lock asked for; Hyprland reports what each // panel actually did. While a video is on show the two are reconciled, so a // blank that failed keeps playing and a panel woken behind the lock's back // (a resume that kept the same outputs) resumes instead of freezing. property var monitorDpms: ({}) property bool monitorDpmsKnown: false readonly property bool videoBackground: Util.isVideoPath(backgroundPath) property bool strandedLock: false property bool strandedLockResolved: false readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure readonly property bool authenticating: authenticatingPassword || fingerprintAuthenticating readonly property var batteryService: shell && shell.services ? shell.firstPartyServiceFor("omarchy.battery") : null readonly property bool powerSaverActive: batteryService ? batteryService.powerSaverOnBattery : false // A prompt clears the unavailable notice before the attempt finishes. readonly property bool fingerprintUnavailable: FingerprintModel.isUnavailable(fingerprintProbeStreak) || (fingerprintConfigured && (!fingerprintAttemptReachedDevice || fingerprintAttemptFastError) && FingerprintModel.isUnavailable(fingerprintUnreachedStreak)) function realScreenCount() { var screens = Quickshell.screens || [] var count = 0 for (var i = 0; i < screens.length; i++) { var screen = screens[i] if (screen && screen.name && screen.width > 0 && screen.height > 0) count += 1 } return count } function hasRealScreen() { return realScreenCount() > 0 } function queueSessionLock() { pendingSessionLock = true if (!sessionLockStabilizeTimer.running) logEvent("lock-pending: screen-stabilizing") sessionLockStabilizeTimer.restart() if (!pendingSessionLockTimer.running) pendingSessionLockTimer.start() } function requestSessionLock() { if (!lockRequested || sessionLock.locked || sessionLock.secure) return if (sessionLockStabilizeTimer.running) return if (!hasRealScreen()) { if (!pendingSessionLock || lastEvent !== "lock-pending: no-real-screen") logEvent("lock-pending: no-real-screen") pendingSessionLock = true if (!pendingSessionLockTimer.running) pendingSessionLockTimer.start() return } pendingSessionLock = false pendingSessionLockTimer.stop() sessionLock.locked = true } // ext-session-lock outlives its client, and a restart carries no lock over, so // a session locked this early is an orphan behind Hyprland's failsafe. Outputs // are often still absent here, so ask until the answer means something. function checkStrandedLock() { if (strandedLockResolved || strandedLockCheckProc.running) return // A lock this shell took is nobody's orphan. if (locked || lockRequested) { strandedLockResolved = true return } strandedLockCheckProc.running = true } function recoverStrandedLock() { if (!strandedLock || locked || !passwordPamConfigured) return strandedLock = false logEvent("lock-stranded: recovering") beginLock() } function refreshBackground() { if (!readlinkProc.running) readlinkProc.running = true } function refreshPoster() { if (!root.videoBackground) { root.videoPosterPath = "" return } if (posterProc.running) return posterProc.sourcePath = root.backgroundPath posterProc.running = true } function refreshFingerprintStatus() { if (!fingerprintCheckProc.running) fingerprintCheckProc.running = true } // Only definitive enrollment results may disable authentication. function applyFingerprintProbe(text) { var status = FingerprintModel.classifyProbe(text) if (status === "unknown") { if (fingerprintConfigured) return fingerprintProbeStreak += 1 if (lockRequested) { fingerprintRecheckTimer.interval = FingerprintModel.retryDelayMs(fingerprintProbeStreak) fingerprintRecheckTimer.restart() } return } fingerprintProbeStreak = 0 fingerprintRecheckTimer.stop() fingerprintConfigured = status === "yes" if (lockRequested && fingerprintConfigured) { // A pending retry already owns the next attempt. if (!fingerprintRetryTimer.running) startFingerprint() } else if (!fingerprintConfigured) { // abort() delivers no completion signal, so close the attempt here // too; settle returns before arming a retry while unconfigured. if (fingerprintPam.active) fingerprintPam.abort() settleFingerprintAttempt() fingerprintRetryTimer.stop() } } function logEvent(event) { lastEvent = event lastEventAt = new Date().toISOString() console.log("omarchy lock " + lastEventAt + " " + event) } function resetAuthenticationState() { enteredPassword = "" pendingPassword = "" failureMessage = "" failedAttempts = 0 authenticatingPassword = false fingerprintAuthenticating = false fingerprintUnreachedStreak = 0 fingerprintAttemptFastError = false fingerprintAttemptPromptedAtMs = 0 fingerprintLastNudgeMs = 0 fingerprintLastSettleMs = 0 fingerprintResumedAtMs = 0 fingerprintProbeStreak = 0 fingerprintRecheckTimer.stop() fingerprintRetryTimer.stop() fingerprintReachTimer.stop() if (passwordPam.active) passwordPam.abort() if (fingerprintPam.active) fingerprintPam.abort() } function beginLock() { if (!passwordPamConfigured) { logEvent("lock-denied: missing-pam") return false } resetAuthenticationState() lockRequested = true armBlankTimer() logEvent("lock-requested") queueSessionLock() Qt.callLater(function() { root.refreshBackground() root.refreshFingerprintStatus() }) return true } function finishUnlock() { if (!root.locked && !lockRequested) return lockRequested = false pendingSessionLock = false sessionLockStabilizeTimer.stop() pendingSessionLockTimer.stop() resetAuthenticationState() idleBlankTimer.stop() sessionLock.locked = false logEvent("unlocked") runWake() } function armBlankTimer() { idleBlankTimer.armedAt = Date.now() idleBlankTimer.restart() } function runWake() { root.displaysBlank = false root.monitorDpmsKnown = false if (!wakeProcess.running) wakeProcess.running = true if (lockRequested) armBlankTimer() nudgeFingerprint() } // User activity advances retries without recreating a busy retry loop. function nudgeFingerprint() { if (!lockRequested) return if (!fingerprintConfigured && fingerprintRecheckTimer.running) { var now = Date.now() if (FingerprintModel.shouldNudge(now, fingerprintLastNudgeMs, now - FingerprintModel.IDLE_CLEAR_MS, fingerprintRecheckTimer.interval)) { fingerprintLastNudgeMs = now fingerprintRecheckTimer.interval = FingerprintModel.MATCH_RETRY_MS fingerprintRecheckTimer.restart() } return } if (!fingerprintConfigured) return if (fingerprintPam.active || fingerprintAuthenticating) return if (!fingerprintRetryTimer.running) return var now = Date.now() if (!FingerprintModel.shouldNudge(now, fingerprintLastNudgeMs, fingerprintLastSettleMs, fingerprintRetryTimer.interval)) return fingerprintLastNudgeMs = now armFingerprintRetry(FingerprintModel.MATCH_RETRY_MS) } function armFingerprintRetry(delayMs) { fingerprintRetryTimer.interval = delayMs fingerprintRetryTimer.restart() } // Reset pre-sleep failures while the resume hook restarts fprintd. function noteFingerprintResumed() { var now = Date.now() if (FingerprintModel.inResumeGrace(now, fingerprintResumedAtMs)) return logEvent("fingerprint-resume: streak=" + fingerprintUnreachedStreak) fingerprintResumedAtMs = now fingerprintUnreachedStreak = 0 } // A suspended PAM conversation may be orphaned by the daemon restart. function restartFingerprintAfterSleep() { noteFingerprintResumed() if (fingerprintAuthenticating || fingerprintPam.active) { if (fingerprintPam.active) fingerprintPam.abort() settleFingerprintAttempt() return } if (!fingerprintRetryTimer.running) return armFingerprintRetry(FingerprintModel.MATCH_RETRY_MS) } function runBlank() { root.displaysBlank = true root.monitorDpmsKnown = false if (!blankProcess.running) blankProcess.running = true } function screenBlank(screenName) { var name = String(screenName || "") if (!monitorDpmsKnown || !(name in monitorDpms)) return displaysBlank return !monitorDpms[name] } function applyMonitorDpms(text) { var monitors try { monitors = JSON.parse(String(text || "")) } catch (error) { return } if (!Array.isArray(monitors)) return var dpms = {} for (var i = 0; i < monitors.length; i++) { var monitor = monitors[i] if (monitor && monitor.name && !monitor.disabled) dpms[String(monitor.name)] = !!monitor.dpmsStatus } monitorDpms = dpms monitorDpmsKnown = true } function submitPassword(value) { var password = String(value || "") if (!lockRequested || authenticatingPassword || password.length === 0) return runWake() 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 enteredPassword = "" pendingPassword = "" failedAttempts += 1 failureMessage = "Authentication failed (" + failedAttempts + ")" runWake() } function startFingerprint() { if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return if (fingerprintPam.active || fingerprintAuthenticating) return fingerprintAuthenticating = true fingerprintAttemptReachedDevice = false fingerprintAttemptFastError = false fingerprintAttemptPromptedAtMs = 0 if (!fingerprintPam.start()) { // Pace a failed start while checking whether its PAM configuration was // removed; a definitive "no" stops retries and hides the indicator. settleFingerprintAttempt() refreshFingerprintStatus() return } // Bound claims that never prompt; a normal verify waits for a finger // under pam_fprintd's own timeout after reaching the reader. fingerprintReachTimer.restart() } // A prompt proves the claim landed, so stop waiting for reachability. function noteFingerprintReachedDevice() { if (fingerprintAttemptReachedDevice) return fingerprintAttemptReachedDevice = true fingerprintAttemptPromptedAtMs = Date.now() fingerprintReachTimer.stop() } // abort() gives no completion signal; settle the attempt here. function timeoutFingerprintReach() { logEvent("fingerprint-reach-timeout") if (fingerprintPam.active) fingerprintPam.abort() settleFingerprintAttempt() } // onError and onCompleted can both fire; settle each attempt once. function settleFingerprintAttempt(deviceError) { if (!fingerprintAuthenticating) return fingerprintAuthenticating = false fingerprintReachTimer.stop() if (!lockRequested || !fingerprintConfigured) return // An error can arrive before the sleep watcher notices the wall-clock gap. var now = Date.now() fingerprintAttemptFastError = !!deviceError && fingerprintAttemptReachedDevice && now - fingerprintAttemptPromptedAtMs < FingerprintModel.FAST_ERROR_MS var usableAttempt = fingerprintAttemptReachedDevice && !fingerprintAttemptFastError if (!usableAttempt && fingerprintSleepWatch.running && FingerprintModel.spannedSleep(now - fingerprintSleepWatch.lastTickMs, fingerprintSleepWatch.interval)) { noteFingerprintResumed() } // Reached attempts are the steady state (one per swipe window), so only // the misses and the recovery from them leave a trace. var previousStreak = fingerprintUnreachedStreak var inGrace = FingerprintModel.inResumeGrace(now, fingerprintResumedAtMs) fingerprintUnreachedStreak = FingerprintModel.nextStreak(previousStreak, usableAttempt, inGrace) if (!usableAttempt) { var crossed = !FingerprintModel.isUnavailable(previousStreak) && FingerprintModel.isUnavailable(fingerprintUnreachedStreak) logEvent((crossed ? "fingerprint-unavailable" : "fingerprint-unreached") + ": streak=" + fingerprintUnreachedStreak) } else if (previousStreak > 0) { logEvent("fingerprint-recovered: streak=" + previousStreak) } fingerprintLastSettleMs = now armFingerprintRetry(FingerprintModel.retryDelayMs(fingerprintUnreachedStreak)) } function handleFingerprintFinished(result) { if (result === PamResult.Success && lockRequested) { // A match after a run of misses is the recovery too; the unlock resets // the streak without settling, so log it here or it leaves no trace. if (fingerprintUnreachedStreak > 0) logEvent("fingerprint-recovered: streak=" + fingerprintUnreachedStreak) finishUnlock() } else { settleFingerprintAttempt(result === PamResult.Error) } } WlSessionLock { id: sessionLock locked: false onSecureStateChanged: { root.logEvent("secure=" + secure) if (secure) { root.pendingSessionLock = false sessionLockStabilizeTimer.stop() pendingSessionLockTimer.stop() root.startFingerprint() } } onLockStateChanged: { root.logEvent("session-locked=" + locked) if (locked) { root.pendingSessionLock = false sessionLockStabilizeTimer.stop() pendingSessionLockTimer.stop() } if (!locked && root.lockRequested) { root.lockRequested = false root.pendingSessionLock = false sessionLockStabilizeTimer.stop() pendingSessionLockTimer.stop() root.resetAuthenticationState() root.runWake() } } WlSessionLockSurface { id: lockSurface color: Color.background LockView { id: lockView anchors.fill: parent backgroundPath: root.backgroundPath videoPosterPath: root.videoPosterPath backgroundVersion: root.backgroundVersion fingerprintConfigured: root.fingerprintConfigured || root.fingerprintUnavailable fingerprintUnavailable: root.fingerprintUnavailable authenticatingPassword: root.authenticatingPassword failureMessage: root.failureMessage failedAttempts: root.failedAttempts inputEnabled: root.lockRequested loadBackground: root.locked displaysBlank: root.screenBlank(lockSurface.screen ? lockSurface.screen.name : "") powerSaverActive: root.powerSaverActive passwordText: root.enteredPassword onPasswordTextEdited: function(password) { root.enteredPassword = password } onSubmitPassword: function(password) { root.submitPassword(password) } onClearFailureRequested: root.failureMessage = "" onWakeRequested: root.runWake() } } } 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 videoPosterPath: root.videoPosterPath backgroundVersion: root.backgroundVersion fingerprintConfigured: root.fingerprintConfigured || root.fingerprintUnavailable fingerprintUnavailable: root.fingerprintUnavailable authenticatingPassword: false failureMessage: "" failedAttempts: 0 inputEnabled: false loadBackground: root.previewVisible powerSaverActive: root.powerSaverActive passwordText: "" } 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 onPamMessage: { if (!messageIsError) root.noteFingerprintReachedDevice() } onCompleted: function(result) { root.handleFingerprintFinished(result) } onError: function(error) { root.settleFingerprintAttempt(true) } } // The lock only starts decoding its wallpaper once locked, and a machine // suspending right after locking froze that decode partway: waking showed // the password field on a bare background, then the wallpaper popped in. // Keep each screen's lock wallpaper decoded in the image cache ahead of // time, as the lock view requests it (same URL, the screen's logical size, // PreserveAspectCrop), so the lock draws it on its first frame. readonly property string lockWallpaperPath: videoBackground ? videoPosterPath : backgroundPath readonly property string lockWallpaperUrl: lockWallpaperPath && !Util.isVideoPath(lockWallpaperPath) ? Util.fileUrl(lockWallpaperPath) + (backgroundVersion ? "?v=" + backgroundVersion : "") : "" Variants { model: Quickshell.screens Image { required property var modelData visible: false source: root.lockWallpaperUrl sourceSize.width: modelData.width sourceSize.height: modelData.height fillMode: Image.PreserveAspectCrop asynchronous: true cache: true } } Timer { id: fingerprintRetryTimer interval: FingerprintModel.MATCH_RETRY_MS repeat: false onTriggered: root.startFingerprint() } // Detect resume both during an active attempt and during backoff. Timer { id: fingerprintSleepWatch interval: 1000 repeat: true running: root.lockRequested && root.fingerprintConfigured property double lastTickMs: 0 onRunningChanged: lastTickMs = Date.now() onTriggered: { var now = Date.now() var slept = FingerprintModel.spannedSleep(now - lastTickMs, interval) lastTickMs = now if (slept) root.restartFingerprintAfterSleep() } } Timer { id: fingerprintReachTimer interval: FingerprintModel.REACH_TIMEOUT_MS repeat: false onTriggered: root.timeoutFingerprintReach() } Process { id: readlinkProc command: ["bash", "-c", "path=$(readlink -f -- \"$1\") && printf '%s\\n%s\\n' \"$path\" \"$(stat -Lc %Y:%s -- \"$path\" 2>/dev/null)\"", "_", root.currentBackgroundLink] stdout: StdioCollector { waitForEnd: true onStreamFinished: { var lines = String(text || "").split("\n") var next = String(lines[0] || "").trim() var signature = String(lines[1] || "").trim() if (next !== root.backgroundPath) { root.videoPosterPath = "" root.backgroundPath = next root.backgroundSignature = signature root.backgroundVersion += 1 } else if (signature !== root.backgroundSignature) { root.backgroundSignature = signature root.backgroundVersion += 1 } root.refreshPoster() } } } Process { id: posterProc property string sourcePath: "" command: ["bash", Quickshell.env("OMARCHY_PATH") + "/shell/plugins/lock/poster.sh", sourcePath] stdout: StdioCollector { id: posterOutput; waitForEnd: true } onExited: function(exitCode) { if (sourcePath !== root.backgroundPath) { root.refreshPoster() } else { root.videoPosterPath = exitCode === 0 ? String(posterOutput.text || "").trim() : "" } } } // Keep fprintd errors distinguishable from an explicit empty enrollment. Process { id: fingerprintCheckProc command: ["bash", "-c", "if [[ -f /etc/pam.d/omarchy-lock-fingerprint ]] && command -v fprintd-list >/dev/null 2>&1; then LC_ALL=C fprintd-list \"$USER\" 2>&1; else echo no; fi"] stdout: StdioCollector { id: fingerprintCheckStdout; waitForEnd: true } onExited: root.applyFingerprintProbe(fingerprintCheckStdout.text) } // Retries a probe that could not reach fprintd, paced like the attempt // retries so a daemon that stays unreachable is asked about ever less often. Timer { id: fingerprintRecheckTimer interval: FingerprintModel.ERROR_RETRY_BASE_MS repeat: false onTriggered: root.refreshFingerprintStatus() } Process { id: strandedLockCheckProc command: ["bash", "-c", "omarchy-hyprland-session-locked"] onExited: function(exitCode) { // No output to read the lock off yet. if (exitCode === 2) return root.strandedLockResolved = true // A lock taken while this was in flight is this shell's own. root.strandedLock = exitCode === 0 && !root.locked && !root.lockRequested root.recoverStrandedLock() } } Process { id: wakeProcess command: ["bash", "-c", "omarchy-system-wake"] } Process { id: blankProcess command: ["bash", "-c", "omarchy-brightness-keyboard off; omarchy-brightness-display off"] } // Quickshell exposes no DPMS signal, so the panel state is polled while a // video is the locked wallpaper. A wake or blank request drops the last // answer, so its optimistic state applies until the next poll confirms it. Process { id: monitorDpmsProcess command: ["hyprctl", "monitors", "-j"] stdout: StdioCollector { onStreamFinished: root.applyMonitorDpms(text) } } Timer { id: monitorDpmsTimer interval: 3000 repeat: true triggeredOnStart: true running: root.locked && root.videoBackground onTriggered: { if (!monitorDpmsProcess.running) monitorDpmsProcess.running = true } onRunningChanged: { if (!running) root.monitorDpmsKnown = false } } Timer { id: idleBlankTimer interval: 5000 repeat: false property double armedAt: 0 onTriggered: { // A countdown frozen by suspend fires right after resume, which would // blank the freshly woken unlock screen under the user. Wall-clock time // exposes the gap: take a fresh run-up instead of blanking. if (Date.now() - armedAt > interval + 2000) { root.armBlankTimer() return } // Only a password check in flight should hold the display up. The // fingerprint PAM stays armed for the whole lock, so gating on // `authenticating` here would keep the panel lit until unlock. if (root.lockRequested && !root.authenticatingPassword) root.runBlank() } } Timer { id: sessionLockStabilizeTimer interval: 500 repeat: false onTriggered: root.requestSessionLock() } Timer { id: pendingSessionLockTimer interval: 100 repeat: true onTriggered: root.requestSessionLock() } Timer { id: strandedLockRetryTimer interval: 500 repeat: true // Covers the compositor settling; screens coming back re-arm it. readonly property int budget: 20 property int remaining: 20 running: !root.strandedLockResolved && remaining > 0 function rearm() { if (!root.strandedLockResolved) remaining = budget } onTriggered: { remaining -= 1 root.checkStrandedLock() } } Connections { target: Quickshell function onScreensChanged() { // A panel coming back is a display turning on that runWake did not ask // for, so the blank state has to be given up here or a visible lock // wallpaper stays frozen until the next keypress. root.displaysBlank = false root.requestSessionLock() // A monitor still coming up has no workspace, so cannot answer yet. strandedLockRetryTimer.rearm() root.checkStrandedLock() } } onAuthenticatingPasswordChanged: { if (!lockRequested) return if (authenticatingPassword) idleBlankTimer.stop() else armBlankTimer() } FileView { path: "/etc/pam.d/omarchy-lock-password" watchChanges: true printErrors: false onLoaded: root.passwordPamConfigured = true onLoadFailed: root.passwordPamConfigured = false onFileChanged: reload() } // No lock before PAM is known good. An answer from before then may be stale -- // the failsafe can be cleared from a TTY -- so re-ask rather than act on it. onPasswordPamConfiguredChanged: { if (!passwordPamConfigured) return strandedLock = false strandedLockResolved = false strandedLockRetryTimer.rearm() checkStrandedLock() } Component.onCompleted: { refreshBackground() refreshFingerprintStatus() checkStrandedLock() } ShellIpc { 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, requested: root.lockRequested, pending: root.pendingSessionLock, sessionLocked: sessionLock.locked, secure: sessionLock.secure, realScreens: root.realScreenCount(), passwordPam: root.passwordPamConfigured, fingerprint: root.fingerprintConfigured, fingerprintUnavailable: root.fingerprintUnavailable, authenticating: root.authenticating, lastEvent: root.lastEvent, lastEventAt: root.lastEventAt }) } function preview(): string { root.refreshBackground() root.refreshFingerprintStatus() root.previewVisible = true return "ok" } function hidePreview(): string { root.previewVisible = false return "ok" } } }