diff --git a/quickshell/DMSGreeter.qml b/quickshell/DMSGreeter.qml index 0a1fc86c..f3068f5d 100644 --- a/quickshell/DMSGreeter.qml +++ b/quickshell/DMSGreeter.qml @@ -1,7 +1,5 @@ import QtQuick import Quickshell -import Quickshell.Services.Greetd -import qs.Common import qs.Modules.Greetd Scope { diff --git a/quickshell/Modals/Greeter/GreeterDoctorPage.qml b/quickshell/Modals/Greeter/GreeterDoctorPage.qml index ef503e04..288cd581 100644 --- a/quickshell/Modals/Greeter/GreeterDoctorPage.qml +++ b/quickshell/Modals/Greeter/GreeterDoctorPage.qml @@ -225,7 +225,13 @@ Item { } StyledText { - text: root.errorCount > 0 ? I18n.tr("%1 issue(s) found", "greeter doctor page error count").arg(root.errorCount) : I18n.tr("All checks passed", "greeter doctor page success") + text: { + if (root.errorCount === 0) + return I18n.tr("All checks passed", "greeter doctor page success"); + return root.errorCount === 1 + ? I18n.tr("%1 issue found", "greeter doctor page error count").arg(root.errorCount) + : I18n.tr("%1 issues found", "greeter doctor page error count").arg(root.errorCount); + } font.pixelSize: Theme.fontSizeMedium color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText } diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml index 0ea00d96..c20c9bb4 100644 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ b/quickshell/Modules/Greetd/GreeterContent.qml @@ -230,19 +230,19 @@ Item { function currentAuthMessage() { if (GreeterState.pamState === "error") - return "Authentication error - try again"; + return I18n.tr("Authentication error - try again"); if (GreeterState.pamState === "max") - return "Too many failed attempts - account may be locked"; + return I18n.tr("Too many failed attempts - account may be locked"); if (GreeterState.pamState === "fail") { if (passwordAttemptLimitHint > 0) { const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint)); const remaining = Math.max(passwordAttemptLimitHint - attempt, 0); if (remaining > 0) { - return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)"; + return I18n.tr("Incorrect password - attempt %1 of %2 (lockout may follow)").arg(attempt).arg(passwordAttemptLimitHint); } - return "Incorrect password - next failures may trigger account lockout"; + return I18n.tr("Incorrect password - next failures may trigger account lockout"); } - return "Incorrect password"; + return I18n.tr("Incorrect password"); } return ""; } @@ -767,7 +767,7 @@ Item { property string fullTimeStr: { const format = GreetdSettings.getEffectiveTimeFormat(); - return systemClock.date.toLocaleTimeString(Qt.locale(), format); + return systemClock.date.toLocaleTimeString(I18n.locale(), format); } property var timeParts: fullTimeStr.split(':') property string hours: timeParts[0] || "" @@ -876,7 +876,7 @@ Item { anchors.top: clockContainer.bottom anchors.topMargin: 4 text: { - return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.getEffectiveLockDateFormat()); + return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat()); } font.pixelSize: Theme.fontSizeXLarge color: "white" @@ -1012,15 +1012,15 @@ Item { anchors.verticalCenter: parent.verticalCenter text: { if (GreeterState.unlocking) { - return "Logging in..."; + return I18n.tr("Logging in..."); } if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) { - return "Authenticating..."; + return I18n.tr("Authenticating..."); } if (GreeterState.showPasswordInput) { - return "Password..."; + return I18n.tr("Password..."); } - return "Username..."; + return I18n.tr("Username..."); } color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline font.pixelSize: Theme.fontSizeMedium diff --git a/quickshell/Modules/Greetd/README.md b/quickshell/Modules/Greetd/README.md index 1975609f..4ac5a777 100644 --- a/quickshell/Modules/Greetd/README.md +++ b/quickshell/Modules/Greetd/README.md @@ -6,7 +6,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the - **Multi user**: Login with any system user - **dms sync**: Sync settings with dms for consistent styling between shell and greeter -- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc. +- **Multiple compositors**: The `dms-greeter` wrapper supports niri, Hyprland, sway, scroll, miracle-wm, labwc, and mangowc. - **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd` - **Session Memory**: Remembers last selected session and user - Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser` @@ -152,8 +152,8 @@ sudo chmod +x /usr/local/bin/dms-greeter 5. Create greeter cache directory with proper permissions: ```bash sudo mkdir -p /var/cache/dms-greeter -sudo chown greeter:greeter /var/cache/dms-greeter -sudo chmod 750 /var/cache/dms-greeter +sudo chown : /var/cache/dms-greeter +sudo chmod 2770 /var/cache/dms-greeter ``` 6. Edit or create `/etc/greetd/config.toml`: @@ -163,7 +163,7 @@ vt = 1 [default_session] user = "greeter" -# Change compositor to sway, hyprland, or mangowc if preferred +# Change compositor to another wrapper-supported compositor if preferred command = "/usr/local/bin/dms-greeter --command niri" ``` @@ -238,9 +238,12 @@ DMS_RUN_GREETER=1 qs -p /path/to/dms #### Compositor -You can configure compositor specific settings such as outputs/displays the same as you would in niri or Hyprland. +For current wrapper-based installs, the `dms-greeter` wrapper supports niri, hyprland, sway, scroll, miracle-wm, labwc, and mangowc. -Simply edit `/etc/greetd/dms-niri.kdl` or `/etc/greetd/dms-hypr.conf` to change compositor settings for the greeter +Only niri currently has a generated greeter config path managed by `dms greeter sync`. + +- niri: `dms greeter sync` writes the generated greeter config to `/etc/greetd/niri/config.kdl`. Add local manual tweaks in `/etc/greetd/niri_overrides.kdl`. +- Other wrapper-supported compositors use the wrapper-generated config by default. If you need a custom compositor config, add `-C /path/to/config` to the `dms-greeter` command in `/etc/greetd/config.toml`. #### Personalization @@ -271,4 +274,4 @@ sudo ln -sf ~/.cache/DankMaterialShell/dms-colors.json /var/cache/dms-greeter/co **Advanced:** You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable or the `--cache-dir` flag when using `dms-greeter`. The default is `/var/cache/dms-greeter`. -The cache directory should be owned by `greeter:greeter` with `770` permissions. +The cache directory should be owned by `:` with `2770` permissions. If the greeter user is not available yet, DMS falls back to `root:`. diff --git a/quickshell/Modules/Greetd/assets/dms-greeter b/quickshell/Modules/Greetd/assets/dms-greeter index 96bddb53..5d3419c5 100755 --- a/quickshell/Modules/Greetd/assets/dms-greeter +++ b/quickshell/Modules/Greetd/assets/dms-greeter @@ -214,18 +214,6 @@ export XDG_STATE_HOME="$CACHE_DIR/.local/state" export XDG_DATA_HOME="$CACHE_DIR/.local/share" export XDG_CACHE_HOME="$CACHE_DIR/.cache" -# Propagate correct XDG dirs into the systemd user session so socket-activated -# services (e.g. wireplumber) don't inherit HOME=/ from /etc/passwd. -if command -v systemctl >/dev/null 2>&1; then - systemctl --user set-environment \ - HOME="$CACHE_DIR" \ - XDG_STATE_HOME="$CACHE_DIR/.local/state" \ - XDG_DATA_HOME="$CACHE_DIR/.local/share" \ - XDG_CACHE_HOME="$CACHE_DIR/.cache" 2>/dev/null || true - if systemctl --user is-active --quiet wireplumber.service 2>/dev/null; then - systemctl --user restart wireplumber.service 2>/dev/null || true - fi -fi # Keep greeter VT clean by default; callers can override via env or --debug. if [[ -z "${RUST_LOG:-}" ]]; then diff --git a/quickshell/Modules/Lock/LockScreenContent.qml b/quickshell/Modules/Lock/LockScreenContent.qml index 386a3bab..3a7f0ab2 100644 --- a/quickshell/Modules/Lock/LockScreenContent.qml +++ b/quickshell/Modules/Lock/LockScreenContent.qml @@ -365,9 +365,9 @@ Item { visible: SettingsData.lockScreenShowDate text: { if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) { - return systemClock.date.toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat); + return systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat); } - return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat); + return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat); } font.pixelSize: Theme.fontSizeXLarge color: "white" @@ -719,14 +719,24 @@ Item { anchors.centerIn: parent name: { + if (pam.u2fPending) + return "passkey"; if (pam.fprint.tries >= SettingsData.maxFprintTries) return "fingerprint_off"; if (pam.fprint.active) return "fingerprint"; + if (pam.u2f.active) + return "passkey"; return "lock"; } size: 20 - color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText) + color: { + if (pam.fprint.tries >= SettingsData.maxFprintTries) + return Theme.error; + if (pam.u2fState !== "") + return Theme.tertiary; + return passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText; + } opacity: pam.passwd.active ? 0 : 1 Behavior on opacity { @@ -792,6 +802,11 @@ Item { } if (event.key === Qt.Key_Escape) { + if (pam.u2fPending) { + pam.cancelU2fPending(); + event.accepted = true; + return; + } clear(); } @@ -856,6 +871,11 @@ Item { if (root.unlocking) { return "Unlocking..."; } + if (pam.u2fPending) { + if (pam.u2fState === "insert") + return "Insert your security key..."; + return "Touch your security key..."; + } if (pam.passwd.active) { return "Authenticating..."; } @@ -930,7 +950,7 @@ Item { anchors.verticalCenter: parent.verticalCenter iconName: "keyboard" buttonSize: 32 - visible: !demoMode && !pam.passwd.active && !root.unlocking + visible: !demoMode && !pam.passwd.active && !root.unlocking && !pam.u2fPending enabled: visible onClicked: { if (keyboardController.isKeyboardActive) { @@ -1031,7 +1051,7 @@ Item { anchors.verticalCenter: parent.verticalCenter iconName: "keyboard_return" buttonSize: 36 - visible: (demoMode || (!pam.passwd.active && !root.unlocking)) + visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending)) enabled: !demoMode onClicked: { if (!demoMode && !root.unlocking && !pam.u2fPending) { @@ -1637,6 +1657,14 @@ Item { root.passwordBuffer = ""; } } + onU2fPendingChanged: { + if (u2fPending) { + passwordField.text = ""; + root.passwordBuffer = ""; + if (keyboardController.isKeyboardActive) + keyboardController.hide(); + } + } } Connections { diff --git a/quickshell/Modules/Lock/Pam.qml b/quickshell/Modules/Lock/Pam.qml index 9ef2d901..0a772e5d 100644 --- a/quickshell/Modules/Lock/Pam.qml +++ b/quickshell/Modules/Lock/Pam.qml @@ -14,9 +14,12 @@ Scope { readonly property alias passwd: passwd readonly property alias fprint: fprint + readonly property alias u2f: u2f property string lockMessage property string state property string fprintState + property string u2fState + property bool u2fPending: false property string buffer signal flashMsg @@ -127,9 +130,8 @@ Scope { onCompleted: res => { if (res === PamResult.Success) { if (!root.unlockInProgress) { - root.unlockInProgress = true; fprint.abort(); - root.unlockRequested(); + root.proceedAfterPrimaryAuth(); } return; } @@ -192,9 +194,8 @@ Scope { if (res === PamResult.Success) { if (!root.unlockInProgress) { - root.unlockInProgress = true; passwd.abort(); - root.unlockRequested(); + root.proceedAfterPrimaryAuth(); } return; } @@ -356,6 +357,8 @@ Scope { SettingsData.refreshAuthAvailability(); root.state = ""; root.fprintState = ""; + root.u2fState = ""; + root.u2fPending = false; root.lockMessage = ""; root.resetAuthFlows(); fprint.checkAvail(); diff --git a/quickshell/assets/pam/u2f b/quickshell/assets/pam/u2f new file mode 100644 index 00000000..bdac4cef --- /dev/null +++ b/quickshell/assets/pam/u2f @@ -0,0 +1,3 @@ +#%PAM-1.0 + +auth required pam_u2f.so cue nouserok timeout=10