diff --git a/README.md b/README.md index ec3f9eb..6162f72 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ sudo dnf config-manager addrepo \ ```bash sudo dnf install touchpad-hold-tap +sudo dnf install plasma-always-show-unlock ``` ## Compatibility @@ -36,3 +37,14 @@ including double middle clicks. The `touchpad-hold-tap` package requires libinput 1.30 or newer and a compositor that enables libinput plugins. Log out and back in after installation or an update so the compositor reloads the plugin. + +### Always-visible Plasma unlock prompt + +The `plasma-always-show-unlock` package keeps Plasma's password and fingerprint +prompt visible from the moment the lock screen appears, including after the +display is turned off or the system resumes from sleep. + +The package applies a narrow patch to Plasma 6.7's lock-screen QML and reapplies +it after `plasma-desktop` updates. An unknown future layout is left unchanged +instead of being modified speculatively. Removing the package restores Plasma's +original behavior. diff --git a/plasma-always-show-unlock/LockScreenUi.patch b/plasma-always-show-unlock/LockScreenUi.patch new file mode 100644 index 0000000..42c844b --- /dev/null +++ b/plasma-always-show-unlock/LockScreenUi.patch @@ -0,0 +1,84 @@ +--- a/contents/lockscreen/LockScreenUi.qml ++++ b/contents/lockscreen/LockScreenUi.qml +@@ -83,6 +83,15 @@ + } + } + ++ Connections { ++ target: root ++ function onViewVisibleChanged() { ++ if (root.viewVisible) { ++ lockScreenRoot.uiVisible = true; ++ } ++ } ++ } ++ + SessionManagement { + id: sessionManagement + } +@@ -108,7 +117,6 @@ + id: lockScreenRoot + + property bool uiVisible: false +- property bool seenPositionChange: false + property bool blockUI: containsMouse && (mainStack.depth > 1 || mainBlock.mainPasswordBox.text.length > 0 || inputPanel.keyboardActive) + + x: parent.x +@@ -119,10 +127,7 @@ + cursorShape: uiVisible ? Qt.ArrowCursor : Qt.BlankCursor + drag.filterChildren: true + onPressed: uiVisible = true; +- onPositionChanged: { +- uiVisible = seenPositionChange; +- seenPositionChange = true; +- } ++ onPositionChanged: uiVisible = true; + onUiVisibleChanged: { + if (uiVisible) { + Window.window.requestActivate(); +@@ -135,6 +140,13 @@ + } + authenticator.startAuthenticating(); + } ++ Timer { ++ // Keep the authentication backend active while the prompt is visible. ++ interval: 1000 ++ running: parent.uiVisible ++ repeat: true ++ onTriggered: authenticator.startAuthenticating() ++ } + onBlockUIChanged: { + if (blockUI) { + fadeoutTimer.running = false; +@@ -143,19 +155,12 @@ + fadeoutTimer.restart(); + } + } +- onExited: { +- uiVisible = false; +- } + Keys.onEscapePressed: { +- // If the escape key is pressed, kscreenlocker will turn off the screen. +- // We do not want to show the password prompt in this case. +- if (uiVisible) { +- uiVisible = false; +- if (inputPanel.keyboardActive) { +- inputPanel.showHide(); +- } +- root.clearPassword(); ++ // KScreenLocker will turn off the screen; leave the prompt ready for wake-up. ++ if (inputPanel.keyboardActive) { ++ inputPanel.showHide(); + } ++ root.clearPassword(); + } + Keys.onPressed: event => { + uiVisible = true; +@@ -167,7 +172,6 @@ + onTriggered: { + if (!lockScreenRoot.blockUI) { + mainBlock.mainPasswordBox.showPassword = false; +- lockScreenRoot.uiVisible = false; + } + } + } diff --git a/plasma-always-show-unlock/plasma-always-show-unlock b/plasma-always-show-unlock/plasma-always-show-unlock new file mode 100755 index 0000000..b596482 --- /dev/null +++ b/plasma-always-show-unlock/plasma-always-show-unlock @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +# SPDX-License-Identifier: MIT + +set -euo pipefail + +root=${PLASMA_ALWAYS_SHOW_UNLOCK_ROOT:-/} +patch_file=${PLASMA_ALWAYS_SHOW_UNLOCK_PATCH:-/usr/share/plasma-always-show-unlock/LockScreenUi.patch} +target_dir="$root/usr/share/plasma/shells/org.kde.plasma.desktop" +target="$target_dir/contents/lockscreen/LockScreenUi.qml" + +usage() { + printf 'Usage: %s {apply|revert|status}\n' "${0##*/}" >&2 +} + +patch_options=( + --silent + --force + --fuzz=0 + --no-backup-if-mismatch + -p1 + -d "$target_dir" +) + +run_patch() { + patch "${patch_options[@]}" "$@" < "$patch_file" +} + +check_patch() { + run_patch --dry-run "$@" >/dev/null 2>&1 +} + +apply_patch() { + if check_patch --reverse; then + printf 'Plasma unlock prompt patch is already applied.\n' + return + fi + if ! check_patch; then + printf 'Error: %s is not a supported unmodified Plasma lock screen.\n' "$target" >&2 + return 1 + fi + + run_patch + printf 'Applied the Plasma unlock prompt patch.\n' +} + +revert_patch() { + if check_patch; then + printf 'Plasma unlock prompt patch is not applied.\n' + return + fi + if ! check_patch --reverse; then + printf 'Error: %s is neither the supported original nor patched file.\n' "$target" >&2 + return 1 + fi + + run_patch --reverse + printf 'Reverted the Plasma unlock prompt patch.\n' +} + +if (( $# != 1 )); then + usage + exit 2 +fi + +if [[ ! -f $target ]]; then + printf 'Error: Plasma lock screen not found at %s.\n' "$target" >&2 + exit 1 +fi +if [[ ! -r $patch_file ]]; then + printf 'Error: patch file not found at %s.\n' "$patch_file" >&2 + exit 1 +fi + +case $1 in + apply) + apply_patch + ;; + revert) + revert_patch + ;; + status) + if check_patch --reverse; then + printf 'Plasma unlock prompt patch is applied.\n' + elif check_patch; then + printf 'Plasma unlock prompt patch is not applied.\n' + exit 1 + else + printf 'Plasma lock screen is not supported by this patch.\n' >&2 + exit 2 + fi + ;; + *) + usage + exit 2 + ;; +esac diff --git a/plasma-always-show-unlock/plasma-always-show-unlock.spec b/plasma-always-show-unlock/plasma-always-show-unlock.spec new file mode 100644 index 0000000..e8e27f2 --- /dev/null +++ b/plasma-always-show-unlock/plasma-always-show-unlock.spec @@ -0,0 +1,60 @@ +Name: plasma-always-show-unlock +Version: 0.1.0 +Release: 1%{?dist} +Summary: Always show the Plasma lock-screen unlock prompt + +License: MIT AND GPL-2.0-or-later +URL: https://git.ajpanton.se/ajp_anton/fedora-tools +Source0: plasma-always-show-unlock +Source1: LockScreenUi.patch +Source2: test-plasma-always-show-unlock +Source3: LockScreenUi.qml +Source4: LICENSE +Source5: README.md + +BuildArch: noarch +BuildRequires: bash +BuildRequires: patch +Requires: bash +Requires: patch +Requires: plasma-desktop >= 6.7.4 + +%description +Keeps KDE Plasma's unlock prompt visible when the screen is locked, after the +display is turned off, and after suspend. The package reapplies its narrow QML +patch after plasma-desktop updates and leaves unknown future layouts unchanged. + +%prep + +%build + +%check +bash %{SOURCE2} %{SOURCE0} %{SOURCE1} %{SOURCE3} + +%install +install -Dpm 0755 %{SOURCE0} \ + %{buildroot}%{_libexecdir}/plasma-always-show-unlock +install -Dpm 0644 %{SOURCE1} \ + %{buildroot}%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch +install -Dpm 0644 %{SOURCE4} \ + %{buildroot}%{_licensedir}/%{name}/LICENSE +install -Dpm 0644 %{SOURCE5} \ + %{buildroot}%{_docdir}/%{name}/README.md + +%preun +if [ "$1" -eq 0 ]; then + %{_libexecdir}/plasma-always-show-unlock revert || : +fi + +%triggerin -- plasma-desktop +%{_libexecdir}/plasma-always-show-unlock apply || : + +%files +%license %{_licensedir}/%{name}/LICENSE +%doc %{_docdir}/%{name}/README.md +%{_libexecdir}/plasma-always-show-unlock +%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch + +%changelog +* Wed Sep 02 2026 fedora-tools contributors - 0.1.0-1 +- Initial package diff --git a/plasma-always-show-unlock/tests/LockScreenUi.qml b/plasma-always-show-unlock/tests/LockScreenUi.qml new file mode 100644 index 0000000..3bab403 --- /dev/null +++ b/plasma-always-show-unlock/tests/LockScreenUi.qml @@ -0,0 +1,178 @@ +/* + SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +import org.kde.plasma.components as PlasmaComponents3 +import org.kde.plasma.workspace.components as PW +import org.kde.plasma.private.keyboardindicator as KeyboardIndicator +import org.kde.kirigami as Kirigami +import org.kde.kscreenlocker as ScreenLocker + +import org.kde.plasma.private.sessions +import org.kde.breeze.components + +Item { + id: lockScreenUi + + // If we're using software rendering, draw outlines instead of shadows + // See https://bugs.kde.org/show_bug.cgi?id=398317 + readonly property bool softwareRendering: GraphicsInfo.api === GraphicsInfo.Software + + function handleMessage(msg) { + if (!root.notification) { + root.notification += msg; + } else if (root.notification.includes(msg)) { + root.notificationRepeated(); + } else { + root.notification += "\n" + msg + } + } + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + + Connections { + target: authenticator + function onFailed(kind) { + if (kind != 0) { // if this is coming from the noninteractive authenticators + return; + } + const msg = i18ndc("plasma_shell_org.kde.plasma.desktop", "@info:status", "Unlocking failed"); + lockScreenUi.handleMessage(msg); + graceLockTimer.restart(); + notificationRemoveTimer.restart(); + rejectPasswordAnimation.start(); + } + + function onSucceeded() { + if (authenticator.hadPrompt) { + Qt.quit(); + } else { + mainStack.replace(null, Qt.resolvedUrl("NoPasswordUnlock.qml"), + { + userListModel: users + }, + StackView.Immediate, + ); + mainStack.forceActiveFocus(); + } + } + + function onInfoMessageChanged() { + lockScreenUi.handleMessage(authenticator.infoMessage); + } + + function onErrorMessageChanged() { + lockScreenUi.handleMessage(authenticator.errorMessage); + } + + function onPromptChanged(msg) { + lockScreenUi.handleMessage(authenticator.prompt); + } + function onPromptForSecretChanged(msg) { + mainBlock.showPassword = false; + mainBlock.mainPasswordBox.forceActiveFocus(); + } + } + + SessionManagement { + id: sessionManagement + } + + KeyboardIndicator.KeyState { + id: capsLockState + key: Qt.Key_CapsLock + } + + Connections { + target: sessionManagement + function onAboutToSuspend() { + root.clearPassword(); + } + } + + RejectPasswordAnimation { + id: rejectPasswordAnimation + target: mainBlock + } + + MouseArea { + id: lockScreenRoot + + property bool uiVisible: false + property bool seenPositionChange: false + property bool blockUI: containsMouse && (mainStack.depth > 1 || mainBlock.mainPasswordBox.text.length > 0 || inputPanel.keyboardActive) + + x: parent.x + y: parent.y + width: parent.width + height: parent.height + hoverEnabled: true + cursorShape: uiVisible ? Qt.ArrowCursor : Qt.BlankCursor + drag.filterChildren: true + onPressed: uiVisible = true; + onPositionChanged: { + uiVisible = seenPositionChange; + seenPositionChange = true; + } + onUiVisibleChanged: { + if (uiVisible) { + Window.window.requestActivate(); + } + + if (blockUI) { + fadeoutTimer.running = false; + } else if (uiVisible) { + fadeoutTimer.restart(); + } + authenticator.startAuthenticating(); + } + onBlockUIChanged: { + if (blockUI) { + fadeoutTimer.running = false; + uiVisible = true; + } else { + fadeoutTimer.restart(); + } + } + onExited: { + uiVisible = false; + } + Keys.onEscapePressed: { + // If the escape key is pressed, kscreenlocker will turn off the screen. + // We do not want to show the password prompt in this case. + if (uiVisible) { + uiVisible = false; + if (inputPanel.keyboardActive) { + inputPanel.showHide(); + } + root.clearPassword(); + } + } + Keys.onPressed: event => { + uiVisible = true; + event.accepted = false; + } + Timer { + id: fadeoutTimer + interval: 10000 + onTriggered: { + if (!lockScreenRoot.blockUI) { + mainBlock.mainPasswordBox.showPassword = false; + lockScreenRoot.uiVisible = false; + } + } + } + Timer { + id: notificationRemoveTimer + interval: 3000 + onTriggered: root.notification = "" + } diff --git a/plasma-always-show-unlock/tests/test-plasma-always-show-unlock b/plasma-always-show-unlock/tests/test-plasma-always-show-unlock new file mode 100755 index 0000000..21258b5 --- /dev/null +++ b/plasma-always-show-unlock/tests/test-plasma-always-show-unlock @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# SPDX-License-Identifier: MIT + +set -euo pipefail + +if (( $# != 3 )); then + printf 'Usage: %s SCRIPT PATCH FIXTURE\n' "${0##*/}" >&2 + exit 2 +fi + +script=$1 +patch_file=$2 +fixture=$3 +tmpdir=$(mktemp -d) +trap 'rm -rf -- "$tmpdir"' EXIT + +root="$tmpdir/root" +target_dir="$root/usr/share/plasma/shells/org.kde.plasma.desktop/contents/lockscreen" +target="$target_dir/LockScreenUi.qml" +mkdir -p "$target_dir" +cp "$fixture" "$target" + +run_tool() { + PLASMA_ALWAYS_SHOW_UNLOCK_ROOT="$root" \ + PLASMA_ALWAYS_SHOW_UNLOCK_PATCH="$patch_file" \ + bash "$script" "$@" +} + +if run_tool status >/dev/null 2>&1; then + printf 'Expected an unpatched fixture.\n' >&2 + exit 1 +fi + +run_tool apply >/dev/null +run_tool status >/dev/null +grep -q 'function onViewVisibleChanged()' "$target" +grep -q 'running: parent.uiVisible' "$target" +grep -q 'onTriggered: authenticator.startAuthenticating()' "$target" +if grep -q 'seenPositionChange\|lockScreenRoot.uiVisible = false' "$target"; then + printf 'The patched fixture still contains prompt-hiding logic.\n' >&2 + exit 1 +fi + +patched_checksum=$(sha256sum "$target") +run_tool apply >/dev/null +[[ $(sha256sum "$target") == "$patched_checksum" ]] + +run_tool revert >/dev/null +cmp "$fixture" "$target" + +sed -i 's/property bool seenPositionChange: false/property bool unexpectedState: false/' "$target" +unsupported_checksum=$(sha256sum "$target") +if run_tool apply >/dev/null 2>&1; then + printf 'Expected an unsupported modified fixture to be rejected.\n' >&2 + exit 1 +fi +[[ $(sha256sum "$target") == "$unsupported_checksum" ]] + +printf 'plasma-always-show-unlock tests passed\n' diff --git a/scripts/build-plasma-always-show-unlock-rpm b/scripts/build-plasma-always-show-unlock-rpm new file mode 100755 index 0000000..aaae502 --- /dev/null +++ b/scripts/build-plasma-always-show-unlock-rpm @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +tool_dir="$repo_root/plasma-always-show-unlock" +topdir="$repo_root/rpmbuild" + +mkdir -p "$topdir"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} +install -m 0755 \ + "$tool_dir/plasma-always-show-unlock" \ + "$topdir/SOURCES/plasma-always-show-unlock" +install -m 0644 \ + "$tool_dir/LockScreenUi.patch" \ + "$topdir/SOURCES/LockScreenUi.patch" +install -m 0755 \ + "$tool_dir/tests/test-plasma-always-show-unlock" \ + "$topdir/SOURCES/test-plasma-always-show-unlock" +install -m 0644 \ + "$tool_dir/tests/LockScreenUi.qml" \ + "$topdir/SOURCES/LockScreenUi.qml" +install -m 0644 "$repo_root/LICENSE" "$topdir/SOURCES/LICENSE" +install -m 0644 "$repo_root/README.md" "$topdir/SOURCES/README.md" + +rpmbuild \ + --define "_topdir $topdir" \ + -bb "$tool_dir/plasma-always-show-unlock.spec"