diff --git a/shell/plugins/lock/LockView.qml b/shell/plugins/lock/LockView.qml index 999a0a04..dea08319 100644 --- a/shell/plugins/lock/LockView.qml +++ b/shell/plugins/lock/LockView.qml @@ -60,6 +60,14 @@ Item { passwordTextEdited("") } + // Waking a DPMS-blanked display can stall the compositor for seconds while + // the monitor modesets, so the wake key's release arrives late and client-side + // key repeat floods the field with that character. A held key has no business + // typing a password; only holding Backspace/Delete to clear stays useful. + function dropsAutoRepeat(key) { + return key !== Qt.Key_Backspace && key !== Qt.Key_Delete + } + function syncPasswordText() { if (passwordInput.text === passwordText) return syncingPasswordText = true @@ -199,6 +207,10 @@ Item { Keys.onPressed: function(event) { root.wakeRequested() + if (event.isAutoRepeat && root.dropsAutoRepeat(event.key)) { + event.accepted = true + return + } if (event.key === Qt.Key_Escape || (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_U)) { root.passwordTextEdited("") event.accepted = true diff --git a/test/shell.d/fixtures/lock-autorepeat/shell.qml b/test/shell.d/fixtures/lock-autorepeat/shell.qml new file mode 100644 index 00000000..cc525752 --- /dev/null +++ b/test/shell.d/fixtures/lock-autorepeat/shell.qml @@ -0,0 +1,73 @@ +import QtQuick +import Quickshell +import qs.Commons + +ShellRoot { + id: root + + readonly property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT") + readonly property string rootPath: Quickshell.env("OMARCHY_PATH") + property var failures: [] + + function fail(message) { + failures.push(String(message)) + } + + function assertTrue(condition, message) { + if (!condition) fail(message) + } + + function shellQuote(value) { + return "'" + String(value).replace(/'/g, "'\\''") + "'" + } + + function writeResult() { + var payload = JSON.stringify({ + ok: failures.length === 0, + failures: failures + }) + + if (resultPath) { + Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)]) + } + } + + Item { id: host; width: 800; height: 600 } + + Timer { + interval: 1 + running: true + repeat: false + onTriggered: { + try { + var component = Qt.createComponent("file://" + root.rootPath + "/shell/plugins/lock/LockView.qml", Component.PreferSynchronous) + if (component.status !== Component.Ready) { + root.fail("LockView failed to load: " + component.errorString()) + return + } + + var view = component.createObject(host, { width: 800, height: 600, loadBackground: false }) + if (!view) { + root.fail("LockView failed to instantiate: " + component.errorString()) + return + } + + var dropped = [Qt.Key_A, Qt.Key_Z, Qt.Key_0, Qt.Key_Space, Qt.Key_Return, Qt.Key_Enter, Qt.Key_Escape] + for (var i = 0; i < dropped.length; i++) { + root.assertTrue(view.dropsAutoRepeat(dropped[i]), "auto-repeat of key " + dropped[i] + " is dropped") + } + + var kept = [Qt.Key_Backspace, Qt.Key_Delete] + for (var j = 0; j < kept.length; j++) { + root.assertTrue(!view.dropsAutoRepeat(kept[j]), "auto-repeat of key " + kept[j] + " still edits the field") + } + + view.destroy() + } catch (error) { + root.fail("lock autorepeat fixture threw: " + error) + } finally { + root.writeResult() + } + } + } +} diff --git a/test/shell.d/lock-autorepeat-test.sh b/test/shell.d/lock-autorepeat-test.sh new file mode 100644 index 00000000..f1570a18 --- /dev/null +++ b/test/shell.d/lock-autorepeat-test.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +TMPDIR="" +QS_PID="" + +cleanup() { + if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then + kill "$QS_PID" 2>/dev/null || true + wait "$QS_PID" 2>/dev/null || true + fi + if [[ -n $TMPDIR && -d $TMPDIR ]]; then + rm -rf "$TMPDIR" + fi +} +trap cleanup EXIT + +require_compositor "lock autorepeat test" + +if ! command -v quickshell >/dev/null 2>&1; then + pass "quickshell not installed; skipping lock autorepeat test" + exit 0 +fi + +require_command jq + +TMPDIR=$(mktemp -d) +result="$TMPDIR/result.json" +log="$TMPDIR/quickshell.log" +config_dir="$TMPDIR/lock-autorepeat" +mkdir -p "$config_dir" "$TMPDIR/home" +cp "$SHELL_TEST_DIR/fixtures/lock-autorepeat/shell.qml" "$config_dir/shell.qml" +ln -s "$ROOT/shell/Ui" "$config_dir/Ui" +ln -s "$ROOT/shell/Commons" "$config_dir/Commons" + +OMARCHY_PATH="$ROOT" \ +OMARCHY_QML_TEST_RESULT="$result" \ +HOME="$TMPDIR/home" \ +XDG_CONFIG_HOME="$TMPDIR/home/.config" \ +XDG_CACHE_HOME="$TMPDIR/home/.cache" \ +XDG_STATE_HOME="$TMPDIR/home/.local/state" \ +QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \ +QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \ +PATH="$ROOT/bin:$PATH" \ + quickshell -p "$config_dir" --no-color >"$log" 2>&1 & +QS_PID=$! + +for _ in {1..80}; do + [[ -s $result ]] && break + if ! kill -0 "$QS_PID" 2>/dev/null; then + sed -n '1,220p' "$log" >&2 + fail "lock autorepeat quickshell exited before writing result" + fi + sleep 0.1 +done + +[[ -s $result ]] || { + sed -n '1,220p' "$log" >&2 + fail "lock autorepeat test timed out" +} + +if ! jq -e '.ok == true' "$result" >/dev/null; then + printf 'Lock autorepeat result:\n' >&2 + jq . "$result" >&2 + printf 'Lock autorepeat log:\n' >&2 + sed -n '1,220p' "$log" >&2 + fail "lock password field drops auto-repeat except Backspace/Delete" +fi + +pass "lock password field drops auto-repeat except Backspace/Delete"