Files
DankMaterialShell/quickshell/Modules/Settings/LockScreenTab.qml
T
Hatta 3081941592 lock: lock before sleep when the command skips logind (#3587)
* lock: lock before sleep when the command skips logind

Custom hibernate never emits PrepareForSleep, so the session stayed unlocked. Wait for the compositor lock on every sleep path, and release the delay inhibitor if that lock finished first.

* lock: release the sleep inhibitor only after lockerReady

* lock: read the sleep inhibitor under its mutex in the race test
2026-09-29 16:50:13 -04:00

927 lines
41 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
import "../../Common/KeyUtils.js" as KeyUtils
Item {
id: root
property var parentModal: null
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
readonly property bool primaryPamHasFprint: SettingsData.lockPamPath !== "" && SettingsData.lockPamInlineFprint
readonly property bool primaryPamHasU2f: SettingsData.lockPamPath !== "" && SettingsData.lockPamInlineU2f
readonly property bool lockFprintControlledByPrimary: SettingsData.lockPamExternallyManaged || primaryPamHasFprint
readonly property bool lockU2fControlledByPrimary: SettingsData.lockPamExternallyManaged || primaryPamHasU2f
property var authServices: []
property bool authValidateRunning: false
property bool authValidateOk: false
property bool authValidateWarn: false
property string authValidateMessage: ""
property string authPendingApplyPath: ""
property bool authShowCustom: false
property bool u2fValidateRunning: false
property bool u2fValidateOk: false
property bool u2fValidateWarn: false
property string u2fValidateMessage: ""
property string u2fPendingApplyPath: ""
property bool u2fShowCustom: false
readonly property string authAutoLabel: I18n.tr("Auto", "automatic PAM authentication source option")
readonly property string authCustomLabel: I18n.tr("Custom", "custom PAM authentication source option") + "…"
function authServiceLabel(service) {
const label = service.name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("-");
return service.dir === "/etc/pam.d" ? label : label + " (" + service.dir + ")";
}
readonly property var authOptions: [authAutoLabel, ...authServices.map(s => authServiceLabel(s)), authCustomLabel]
readonly property string authCurrentValue: {
if (SettingsData.lockPamPath === "")
return authAutoLabel;
const svc = authServices.find(s => s.path === SettingsData.lockPamPath);
return svc ? authServiceLabel(svc) : authCustomLabel;
}
readonly property string u2fAuthCurrentValue: SettingsData.lockU2fPamPath === "" ? authAutoLabel : authCustomLabel
function refreshAuthServices() {
authListServicesProcess.running = true;
}
function applyAutoAuthSource() {
SettingsData.set("lockPamPath", "");
SettingsData.set("lockPamInlineFprint", false);
SettingsData.set("lockPamInlineU2f", false);
root.authValidateOk = false;
root.authValidateWarn = false;
root.authValidateMessage = "";
}
function validateAndApplyAuthSource(path) {
if (!path)
return;
root.authPendingApplyPath = path;
root.authValidateMessage = "";
root.authValidateOk = false;
root.authValidateWarn = false;
root.authValidateRunning = true;
authValidateProcess.command = ["dms", "auth", "validate", "--path", path, "--json"];
authValidateProcess.running = true;
}
function applyAutoU2fSource() {
SettingsData.set("lockU2fPamPath", "");
root.u2fValidateOk = false;
root.u2fValidateWarn = false;
root.u2fValidateMessage = "";
}
function validateAndApplyU2fSource(path) {
if (!path)
return;
root.u2fPendingApplyPath = path;
root.u2fValidateMessage = "";
root.u2fValidateOk = false;
root.u2fValidateWarn = false;
root.u2fValidateRunning = true;
u2fValidateProcess.command = ["dms", "auth", "validate", "--purpose", "u2f", "--path", path, "--json"];
u2fValidateProcess.running = true;
}
function lockFingerprintDescription() {
switch (SettingsData.lockFingerprintReason) {
case "ready":
return I18n.tr("Use fingerprint authentication for the lock screen", "lock screen fingerprint setting");
case "missing_enrollment":
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.", "lock screen fingerprint setting");
case "missing_reader":
return I18n.tr("No fingerprint reader detected", "fingerprint setting status");
case "missing_pam_support":
return I18n.tr("Not available - install fprintd and pam_fprintd", "lock screen fingerprint setting");
default:
return I18n.tr("Fingerprint availability could not be confirmed", "fingerprint setting status");
}
}
function lockU2fDescription() {
switch (SettingsData.lockU2fReason) {
case "ready":
return I18n.tr("Use a security key for lock screen authentication", "lock screen U2F security key setting");
case "missing_key_registration":
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status");
case "missing_pam_support":
return I18n.tr("Not available - install or configure pam_u2f", "lock screen security key setting");
default:
return I18n.tr("Security-key availability could not be confirmed", "security key setting status");
}
}
function refreshAuthDetection() {
SettingsData.refreshAuthAvailability();
}
Component.onCompleted: {
MultimediaService.ensureProbed();
refreshAuthDetection();
refreshAuthServices();
}
onVisibleChanged: {
if (visible) {
refreshAuthDetection();
refreshAuthServices();
}
}
LazyLoader {
id: wallpaperBrowserLoader
active: false
FileBrowserModal {
parentModal: root.parentModal
browserTitle: I18n.tr("Select lock screen background image")
bucket: "wallpaper"
showHiddenFiles: true
startPath: SettingsData.lockScreenWallpaperPath || Theme.wallpaperPath
filters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.svg"]
onAccepted: paths => SettingsData.set("lockScreenWallpaperPath", paths[0])
}
}
FileBrowserModal {
id: videoBrowserModal
browserTitle: I18n.tr("Select Video or Folder")
bucket: "video"
filters: ["*.mp4", "*.mkv", "*.webm", "*.mov", "*.avi", "*.m4v"]
onAccepted: paths => SettingsData.set("lockScreenVideoPath", paths[0])
}
Process {
id: authListServicesProcess
command: ["dms", "auth", "list-services", "--json"]
running: false
property string collected: ""
stdout: StdioCollector {
onStreamFinished: authListServicesProcess.collected = text || ""
}
onExited: exitCode => {
if (exitCode !== 0) {
root.authServices = [];
return;
}
try {
const data = JSON.parse(authListServicesProcess.collected);
root.authServices = (data && Array.isArray(data.services)) ? data.services.filter(s => s.hasAuth) : [];
} catch (e) {
root.authServices = [];
}
}
}
Process {
id: authValidateProcess
running: false
property string collected: ""
stdout: StdioCollector {
onStreamFinished: authValidateProcess.collected = text || ""
}
onExited: exitCode => {
root.authValidateRunning = false;
root.authValidateOk = false;
root.authValidateWarn = false;
let data = null;
try {
data = JSON.parse(authValidateProcess.collected);
} catch (e) {}
if (!data) {
root.authValidateMessage = I18n.tr("Config validation failed");
return;
}
if (!data.valid) {
const errs = Array.isArray(data.errors) ? data.errors : [];
root.authValidateMessage = [I18n.tr("Config validation failed"), ...errs].join("\n");
return;
}
SettingsData.set("lockPamPath", root.authPendingApplyPath);
SettingsData.set("lockPamInlineFprint", data.inlineFingerprint === true);
SettingsData.set("lockPamInlineU2f", data.inlineU2f === true);
const warns = Array.isArray(data.warnings) ? data.warnings : [];
root.authValidateOk = true;
root.authValidateWarn = warns.length > 0;
root.authValidateMessage = [I18n.tr("Authentication changes applied"), ...warns].join("\n");
}
}
Process {
id: u2fValidateProcess
running: false
property string collected: ""
stdout: StdioCollector {
onStreamFinished: u2fValidateProcess.collected = text || ""
}
onExited: exitCode => {
root.u2fValidateRunning = false;
root.u2fValidateOk = false;
root.u2fValidateWarn = false;
let data = null;
try {
data = JSON.parse(u2fValidateProcess.collected);
} catch (e) {}
if (!data) {
root.u2fValidateMessage = I18n.tr("Config validation failed");
return;
}
if (!data.valid) {
const errs = Array.isArray(data.errors) ? data.errors : [];
root.u2fValidateMessage = [I18n.tr("Config validation failed"), ...errs].join("\n");
return;
}
SettingsData.set("lockU2fPamPath", root.u2fPendingApplyPath);
const warns = Array.isArray(data.warnings) ? data.warnings : [];
root.u2fValidateOk = true;
root.u2fValidateWarn = warns.length > 0;
root.u2fValidateMessage = [I18n.tr("Authentication changes applied"), ...warns].join("\n");
root.refreshAuthDetection();
}
}
SettingsPage {
id: mainColumn
SettingsCard {
width: parent.width
iconName: "lock"
title: I18n.tr("Layout")
settingKey: "lockLayout"
SettingsToggleRow {
settingKey: "lockScreenShowPowerActions"
tags: ["lock", "screen", "power", "actions", "shutdown", "reboot"]
text: I18n.tr("Show power actions")
checked: SettingsData.lockScreenShowPowerActions
onToggled: checked => SettingsData.set("lockScreenShowPowerActions", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowSystemIcons"
tags: ["lock", "screen", "system", "icons", "status"]
text: I18n.tr("Show system icons")
checked: SettingsData.lockScreenShowSystemIcons
onToggled: checked => SettingsData.set("lockScreenShowSystemIcons", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowTime"
tags: ["lock", "screen", "time", "clock", "display"]
text: I18n.tr("Show time")
checked: SettingsData.lockScreenShowTime
onToggled: checked => SettingsData.set("lockScreenShowTime", checked)
}
SettingsButtonGroupRow {
settingKey: "lockScreenClockStyle"
tags: ["lock", "screen", "time", "clock", "style", "vertical"]
text: I18n.tr("Clock style")
visible: SettingsData.lockScreenShowTime
model: [I18n.tr("Horizontal", "lock screen clock style option"), I18n.tr("Vertical", "lock screen clock style option")]
currentIndex: SettingsData.lockScreenClockStyle === "vertical" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("lockScreenClockStyle", index === 1 ? "vertical" : "horizontal");
}
}
SettingsToggleRow {
settingKey: "lockScreenShowDate"
tags: ["lock", "screen", "date", "calendar", "display"]
text: I18n.tr("Show date")
checked: SettingsData.lockScreenShowDate
onToggled: checked => SettingsData.set("lockScreenShowDate", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowProfileImage"
tags: ["lock", "screen", "profile", "image", "avatar", "picture"]
text: I18n.tr("Show profile image")
checked: SettingsData.lockScreenShowProfileImage
onToggled: checked => SettingsData.set("lockScreenShowProfileImage", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowPasswordField"
tags: ["lock", "screen", "password", "field", "input", "visible"]
text: I18n.tr("Show password field")
description: I18n.tr("A hidden field appears as soon as a key is pressed")
checked: SettingsData.lockScreenShowPasswordField
onToggled: checked => SettingsData.set("lockScreenShowPasswordField", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowMediaPlayer"
tags: ["lock", "screen", "media", "player", "music", "mpris"]
text: I18n.tr("Show media player")
checked: SettingsData.lockScreenShowMediaPlayer
onToggled: checked => SettingsData.set("lockScreenShowMediaPlayer", checked)
}
SettingsSplitRow {
settingKey: "lockScreenShowWeather"
tab: "lock_screen"
tags: ["weather", "temperature"]
title: I18n.tr("Weather")
checked: SettingsData.lockScreenShowWeather
onToggled: checked => SettingsData.set("lockScreenShowWeather", checked)
onNavigated: root.parentModal?.navigateTo("weather")
}
SettingsDropdownRow {
settingKey: "lockScreenNotificationMode"
tags: ["lock", "screen", "notification", "notifications", "privacy"]
text: I18n.tr("Notifications")
options: [I18n.tr("Disabled", "lock screen notification mode option"), I18n.tr("Count only", "lock screen notification mode option"), I18n.tr("App names", "lock screen notification mode option"), I18n.tr("Full content", "lock screen notification mode option")]
currentValue: options[SettingsData.lockScreenNotificationMode] || options[0]
onValueChanged: value => {
const idx = options.indexOf(value);
if (idx >= 0) {
SettingsData.set("lockScreenNotificationMode", idx);
}
}
}
}
SettingsCard {
width: parent.width
iconName: "palette"
title: I18n.tr("Appearance")
settingKey: "lockAppearance"
SettingsFontDropdownRow {
settingKey: "lockScreenFontFamily"
tags: ["lock", "screen", "font", "typography"]
text: I18n.tr("Font")
currentFont: SettingsData.lockScreenFontFamily || ""
onFontSelected: family => SettingsData.set("lockScreenFontFamily", family)
}
SettingsRow {
settingKey: "lockScreenWallpaperPath"
tags: ["lock", "screen", "wallpaper", "background", "image"]
title: I18n.tr("Background")
resetKeys: ["lockScreenWallpaperPath"]
body: SettingsWallpaperThumb {
width: (parent.width - Theme.spacingL) / 2
anchors.horizontalCenter: parent.horizontalCenter
placeholderIcon: "lock"
emptyText: I18n.tr("Use desktop wallpaper")
allowColor: false
path: SettingsData.lockScreenWallpaperPath
onBrowse: {
wallpaperBrowserLoader.active = true;
if (wallpaperBrowserLoader.item)
wallpaperBrowserLoader.item.open();
}
onClear: SettingsData.set("lockScreenWallpaperPath", "")
}
}
SettingsDropdownRow {
readonly property var fillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"]
readonly property var fillModeLabels: fillModes.map(mode => I18n.tr(mode, "wallpaper fill mode"))
settingKey: "lockScreenWallpaperFillMode"
tags: ["lock", "screen", "wallpaper", "background", "fill"]
text: I18n.tr("Wallpaper fill mode")
options: fillModeLabels
currentValue: {
const idx = fillModes.indexOf(SettingsData.lockScreenWallpaperFillMode || "Fill");
return fillModeLabels[idx >= 0 ? idx : fillModes.indexOf("Fill")];
}
onValueChanged: value => {
const idx = fillModeLabels.indexOf(value);
if (idx >= 0)
SettingsData.set("lockScreenWallpaperFillMode", fillModes[idx]);
}
}
}
SettingsCard {
width: parent.width
iconName: "lock"
title: I18n.tr("Behavior")
settingKey: "lockBehavior"
SettingsToggleRow {
settingKey: "lockScreenPowerOffMonitorsOnLock"
tags: ["lock", "screen", "monitor", "display", "dpms", "power"]
text: I18n.tr("Power off displays on lock")
checked: SettingsData.lockScreenPowerOffMonitorsOnLock
onToggled: checked => SettingsData.set("lockScreenPowerOffMonitorsOnLock", checked)
}
SettingsToggleRow {
settingKey: "lockAtStartup"
tags: ["lock", "screen", "startup", "start", "boot", "login", "automatic"]
text: I18n.tr("Lock at startup")
checked: SettingsData.lockAtStartup
onToggled: checked => SettingsData.set("lockAtStartup", checked)
}
SettingsToggleRow {
settingKey: "lockBeforeSuspend"
tags: ["lock", "screen", "suspend", "hibernate", "sleep", "automatic"]
text: I18n.tr("Lock before suspend")
checked: SettingsData.lockBeforeSuspend
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
onToggled: checked => SettingsData.set("lockBeforeSuspend", checked)
}
}
SettingsCard {
width: parent.width
iconName: "movie"
title: I18n.tr("Video screensaver")
settingKey: "videoScreensaver"
SettingsRow {
visible: !MultimediaService.available
body: StyledText {
text: I18n.tr("QtMultimedia is not available - video screensaver requires qt multimedia services")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
width: parent.width
wrapMode: Text.WordWrap
}
}
SettingsToggleRow {
settingKey: "lockScreenVideoEnabled"
tags: ["lock", "screen", "video", "screensaver", "animation", "movie"]
text: I18n.tr("Play video on lock")
enabled: MultimediaService.available
checked: SettingsData.lockScreenVideoEnabled
onToggled: checked => SettingsData.set("lockScreenVideoEnabled", checked)
}
SettingsTextFieldRow {
leftIconName: "folder"
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
text: I18n.tr("Path")
resetKeys: ["lockScreenVideoPath"]
value: SettingsData.lockScreenVideoPath
placeholderText: "/path/to/videos"
onValueEdited: value => SettingsData.set("lockScreenVideoPath", value)
actions: DankButton {
text: I18n.tr("Browse")
onClicked: videoBrowserModal.open()
}
}
SettingsToggleRow {
settingKey: "lockScreenVideoCycling"
tags: ["lock", "screen", "video", "screensaver", "cycling", "random", "shuffle"]
text: I18n.tr("Automatic cycling")
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
enabled: MultimediaService.available
checked: SettingsData.lockScreenVideoCycling
onToggled: checked => SettingsData.set("lockScreenVideoCycling", checked)
}
}
SettingsCard {
width: parent.width
iconName: "key"
title: I18n.tr("Authentication")
settingKey: "lockAuthSource"
SettingsToggleRow {
settingKey: "lockPamExternallyManaged"
tags: ["lock", "screen", "pam", "managed", "external", "authentication", "policy"]
text: I18n.tr("Use system PAM authentication", "system PAM policy toggle")
checked: SettingsData.lockPamExternallyManaged
onToggled: checked => SettingsData.set("lockPamExternallyManaged", checked)
}
SettingsToggleRow {
settingKey: "enableFprint"
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
text: I18n.tr("Fingerprint", "lock screen fingerprint authentication toggle")
description: root.lockFprintControlledByPrimary ? I18n.tr("Managed by the primary PAM source", "factor managed by PAM source status") : root.lockFingerprintDescription()
descriptionColor: root.lockFprintControlledByPrimary || SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableFprint || root.primaryPamHasFprint
enabled: root.lockFprintToggleAvailable && !root.lockFprintControlledByPrimary
onToggled: checked => SettingsData.set("enableFprint", checked)
}
SettingsToggleRow {
settingKey: "enableU2f"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
text: I18n.tr("Security key")
description: root.lockU2fControlledByPrimary ? I18n.tr("Managed by the primary PAM source", "factor managed by PAM source status") : root.lockU2fDescription()
descriptionColor: root.lockU2fControlledByPrimary || SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableU2f || root.primaryPamHasU2f
enabled: root.lockU2fToggleAvailable && !root.lockU2fControlledByPrimary
onToggled: checked => SettingsData.set("enableU2f", checked)
}
SettingsDropdownRow {
settingKey: "u2fMode"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
visible: SettingsData.enableU2f && !root.lockU2fControlledByPrimary
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second factor (AND)", "U2F mode option: key required after password or fingerprint")]
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
onValueChanged: value => {
if (value === I18n.tr("Second factor (AND)", "U2F mode option: key required after password or fingerprint"))
SettingsData.set("u2fMode", "and");
else
SettingsData.set("u2fMode", "or");
}
}
SettingsToggleRow {
settingKey: "lockScreenSecurityKeyShortcutEnabled"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "shortcut", "keybind", "authentication"]
text: I18n.tr("Security key shortcut", "lock screen security key shortcut toggle")
checked: SettingsData.lockScreenSecurityKeyShortcutEnabled
visible: SettingsData.enableU2f && SettingsData.u2fMode === "or" && !root.lockU2fControlledByPrimary
onToggled: checked => SettingsData.set("lockScreenSecurityKeyShortcutEnabled", checked)
}
SettingsRow {
visible: SettingsData.lockScreenSecurityKeyShortcutEnabled && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !root.lockU2fControlledByPrimary
body: Row {
width: parent.width
spacing: Theme.spacingM
Column {
width: parent.width - securityKeyCapture.width - parent.spacing
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Key combination", "lock screen security key shortcut key combination setting")
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: securityKeyCapture.captureError !== "" ? securityKeyCapture.captureError : I18n.tr("Press Ctrl+key to set. Esc cancels.", "lock screen security key shortcut key combination capture hint")
font.pixelSize: Theme.fontSizeSmall
color: securityKeyCapture.captureError !== "" ? Theme.warning : Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
DankButton {
id: securityKeyCapture
width: 200
anchors.verticalCenter: parent.verticalCenter
focus: capturing
text: capturing ? I18n.tr("Press key...", "lock screen security key shortcut key combination capture prompt") : SettingsData.lockScreenSecurityKeyShortcut
backgroundColor: capturing ? Theme.selectedContainer : Theme.chipSurface
textColor: Theme.surfaceText
property bool capturing: false
property string captureError: ""
readonly property var reservedKeys: ["A", "E", "B", "F", "U", "K", "W", "H", "D"]
function startCapture() {
captureError = "";
capturing = true;
securityKeyCapture.forceActiveFocus();
}
function stopCapture() {
capturing = false;
}
onClicked: {
if (capturing)
stopCapture();
else
startCapture();
}
Keys.onPressed: event => {
if (!securityKeyCapture.capturing)
return;
if (KeyUtils.isModifierKey(event.key))
return;
if (event.key === Qt.Key_Escape) {
securityKeyCapture.stopCapture();
event.accepted = true;
return;
}
const mods = KeyUtils.modsFromEvent(event.modifiers);
const hasCtrl = mods.includes("Ctrl");
const hasAlt = mods.includes("Alt") || mods.includes("Super");
if (!hasCtrl || hasAlt)
return;
const hasShift = mods.includes("Shift");
const key = KeyUtils.xkbKeyFromQtKey(event.key, !!(event.modifiers & Qt.KeypadModifier), hasShift);
if (!key)
return;
if (!KeyUtils.qtKeyFromName(key))
return;
if (!hasShift && securityKeyCapture.reservedKeys.indexOf(key.toUpperCase()) !== -1) {
securityKeyCapture.captureError = I18n.tr("Ctrl+%1 is used for password editing", "lock screen security key shortcut reserved key warning").arg(key.toUpperCase());
event.accepted = true;
return;
}
SettingsData.set("lockScreenSecurityKeyShortcut", KeyUtils.formatToken(mods, key));
securityKeyCapture.captureError = "";
securityKeyCapture.stopCapture();
event.accepted = true;
}
}
}
}
}
SettingsCard {
width: parent.width
iconName: "monitor"
title: I18n.tr("Displays")
settingKey: "lockDisplay"
SettingsRow {
body: StyledText {
text: I18n.tr("Other displays show a solid color for OLED burn-in protection")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
}
SettingsDisplayPicker {
width: parent.width
displayPreferences: SettingsData.screenPreferences?.lockScreen || ["all"]
onPreferencesChanged: prefs => {
var p = SettingsData.screenPreferences || {};
var updated = Object.assign({}, p);
updated["lockScreen"] = prefs;
SettingsData.set("screenPreferences", updated);
}
}
SettingsRow {
title: I18n.tr("Inactive display color")
tags: ["lock", "screen", "inactive", "display", "color", "oled"]
resetKeys: ["lockScreenInactiveColor"]
visible: {
const prefs = SettingsData.screenPreferences?.lockScreen;
return Array.isArray(prefs) && !prefs.includes("all") && prefs.length > 0;
}
Rectangle {
width: Theme.iconButtonSize
height: Theme.iconButtonSize
radius: Theme.cornerRadius
color: SettingsData.lockScreenInactiveColor
border.color: Theme.outline
border.width: Theme.outlineWidth
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!PopoutService.colorPickerModal)
return;
PopoutService.colorPickerModal.selectedColor = SettingsData.lockScreenInactiveColor;
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Inactive display color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
SettingsData.set("lockScreenInactiveColor", selectedColor);
};
PopoutService.colorPickerModal.show();
}
}
}
}
}
SettingsCard {
width: parent.width
iconName: "tune"
title: I18n.tr("Advanced")
settingKey: "lockScreenAdvanced"
tags: ["pam", "authentication", "source", "loginctl", "u2f"]
collapsible: true
expanded: false
SettingsRow {
body: StyledText {
text: I18n.tr("Authentication changes apply automatically")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
}
SettingsDropdownRow {
settingKey: "lockPamPath"
resetKeys: []
tags: ["lock", "screen", "pam", "authentication", "source", "service"]
text: I18n.tr("Authentication source", "lock screen PAM source setting")
description: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : I18n.tr("Which PAM service the lock screen uses to authenticate", "lock screen PAM source setting")
options: root.authOptions
currentValue: root.authCurrentValue
onValueChanged: value => {
if (value === root.authAutoLabel) {
root.authShowCustom = false;
root.applyAutoAuthSource();
return;
}
if (value === root.authCustomLabel) {
root.authShowCustom = true;
return;
}
root.authShowCustom = false;
const svc = root.authServices.find(s => root.authServiceLabel(s) === value);
if (svc)
root.validateAndApplyAuthSource(svc.path);
}
}
SettingsRow {
visible: root.authShowCustom || root.authCurrentValue === root.authCustomLabel
body: Row {
width: parent.width
spacing: Theme.spacingS
DankTextField {
id: customPamField
outlined: true
leftIconName: "lock"
labelText: I18n.tr("Path")
width: parent.width - validatePamButton.width - Theme.spacingS
placeholderText: "/etc/pam.d/my-service"
text: SettingsData.lockPamPath
}
DankButton {
id: validatePamButton
text: I18n.tr("Apply changes")
enabled: !root.authValidateRunning && customPamField.text.trim() !== ""
onClicked: root.validateAndApplyAuthSource(customPamField.text.trim())
}
}
}
SettingsRow {
visible: root.authValidateMessage !== ""
body: Rectangle {
width: parent.width
height: Math.min(160, authStatusText.implicitHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.floatingWindowFieldColor
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
StyledText {
id: authStatusText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: root.authValidateMessage
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText)
wrapMode: Text.Wrap
verticalAlignment: Text.AlignTop
}
}
}
SettingsDropdownRow {
settingKey: "lockU2fPamPath"
resetKeys: []
tags: ["lock", "screen", "pam", "u2f", "security", "key", "source", "service"]
text: I18n.tr("Security key PAM source", "lock screen dedicated U2F PAM source setting")
description: SettingsData.lockU2fPamPath !== "" ? SettingsData.lockU2fPamPath : I18n.tr("Auto uses an installed or bundled key-only service.", "lock screen dedicated U2F PAM source setting")
visible: !root.lockU2fControlledByPrimary
options: [root.authAutoLabel, root.authCustomLabel]
currentValue: root.u2fAuthCurrentValue
onValueChanged: value => {
if (value === root.authAutoLabel) {
root.u2fShowCustom = false;
root.applyAutoU2fSource();
return;
}
root.u2fShowCustom = true;
}
}
SettingsRow {
visible: !root.lockU2fControlledByPrimary && (root.u2fShowCustom || root.u2fAuthCurrentValue === root.authCustomLabel)
body: Row {
width: parent.width
spacing: Theme.spacingS
DankTextField {
id: customU2fPamField
outlined: true
leftIconName: "key"
labelText: I18n.tr("Path")
width: parent.width - validateU2fPamButton.width - Theme.spacingS
placeholderText: "/etc/pam.d/dankshell-u2f"
text: SettingsData.lockU2fPamPath
}
DankButton {
id: validateU2fPamButton
text: I18n.tr("Apply changes")
enabled: !root.u2fValidateRunning && customU2fPamField.text.trim() !== ""
onClicked: root.validateAndApplyU2fSource(customU2fPamField.text.trim())
}
}
}
SettingsRow {
visible: !root.lockU2fControlledByPrimary && root.u2fValidateMessage !== ""
body: Rectangle {
width: parent.width
height: Math.min(160, u2fStatusText.implicitHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.floatingWindowFieldColor
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
StyledText {
id: u2fStatusText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: root.u2fValidateMessage
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: !root.u2fValidateOk ? Theme.error : (root.u2fValidateWarn ? Theme.warning : Theme.surfaceVariantText)
wrapMode: Text.Wrap
verticalAlignment: Text.AlignTop
}
}
}
SettingsRow {
visible: !SessionService.loginctlAvailable
body: StyledText {
text: I18n.tr("loginctl not available - lock integration requires DMS socket connection")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
width: parent.width
wrapMode: Text.Wrap
}
}
SettingsToggleRow {
settingKey: "loginctlLockIntegration"
tags: ["lock", "screen", "loginctl", "dbus", "integration", "external"]
text: I18n.tr("loginctl integration")
description: I18n.tr("Disable when using an external lock screen")
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
enabled: SessionService.loginctlAvailable
onToggled: checked => {
if (!SessionService.loginctlAvailable)
return;
SettingsData.set("loginctlLockIntegration", checked);
}
}
}
}
}