diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 0819adc..11109b1 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -17,7 +17,7 @@ jobs:
- name: Install the build tools
run: |
pacman -Syu --noconfirm --needed git base-devel cmake pkgconf \
- qt6-base qt6-declarative layer-shell-qt kidletime ki18n \
+ qt6-base qt6-declarative layer-shell-qt ki18n \
opencv pam systemd-libs gettext scdoc shellcheck desktop-file-utils
- uses: actions/checkout@v7
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5c526cd..8112d59 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -61,7 +61,7 @@ jobs:
git make cmake gcc-c++ gettext scdoc tar curl rpm-build rpm-sign systemd-rpm-macros \
'pkgconfig(systemd)' 'pkgconfig(libsystemd)' pam-devel opencv-devel \
qt6-qtbase-devel qt6-qtbase-private-devel qt6-qtdeclarative-devel qt6-qtwayland-devel \
- layer-shell-qt-devel kf6-kidletime-devel kf6-ki18n-devel
+ layer-shell-qt-devel kf6-ki18n-devel
- uses: actions/checkout@v7
diff --git a/CMakeLists.txt b/CMakeLists.txt
index abde265..74bceb3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -136,7 +136,6 @@ if(PFU_BUILD_AGENT)
set(QT_NO_PRIVATE_MODULE_WARNING ON)
find_package(Qt6 REQUIRED COMPONENTS GuiPrivate WaylandClientPrivate)
find_package(LayerShellQt REQUIRED)
- find_package(KF6IdleTime REQUIRED)
find_package(KF6I18n REQUIRED)
qt_add_executable(plasma-face-unlock-agent
@@ -147,6 +146,7 @@ if(PFU_BUILD_AGENT)
src/agent/bubblewindow.cpp
src/agent/bubblecontroller.cpp
src/agent/lockcontroller.cpp
+ src/agent/inputwatcher.cpp
src/agent/enrollcontroller.cpp
)
target_include_directories(plasma-face-unlock-agent PRIVATE src/agent)
@@ -155,7 +155,8 @@ if(PFU_BUILD_AGENT)
endif()
qt6_generate_wayland_protocol_client_sources(plasma-face-unlock-agent
- FILES ${CMAKE_CURRENT_SOURCE_DIR}/protocols/kde-lockscreen-overlay-v1.xml)
+ FILES ${CMAKE_CURRENT_SOURCE_DIR}/protocols/kde-lockscreen-overlay-v1.xml
+ ${CMAKE_CURRENT_SOURCE_DIR}/protocols/ext-idle-notify-v1.xml)
# The QML files sit next to the module's qmldir in the resources, so they
# see each other (and the Theme singleton) without importing anything.
@@ -189,7 +190,7 @@ if(PFU_BUILD_AGENT)
Qt6::Gui Qt6::GuiPrivate Qt6::Quick Qt6::DBus Qt6::Network
Qt6::WaylandClient Qt6::WaylandClientPrivate
LayerShellQt::Interface
- KF6::IdleTime KF6::I18n KF6::I18nQml
+ KF6::I18n KF6::I18nQml
pfu_common
)
install(TARGETS plasma-face-unlock-agent DESTINATION ${PFU_LIBEXECDIR})
diff --git a/README.md b/README.md
index 7c24156..8f4e6e2 100644
--- a/README.md
+++ b/README.md
@@ -207,7 +207,7 @@ sudo make install
```
Needs CMake, a C++20 compiler, Qt 6 (Core, DBus, Network, Gui, Quick, WaylandClient), LayerShellQt,
-KIdleTime, KI18n, OpenCV 4.5.4 or newer with the DNN module, Linux-PAM and libsystemd. Optionally
+KI18n, OpenCV 4.5.4 or newer with the DNN module, Linux-PAM and libsystemd. Optionally
`msgfmt` (gettext) for translations and `scdoc` for the man page. Supports `PREFIX` and `DESTDIR`.
`make check` runs syntax checks and shellcheck.
diff --git a/doc/plasma-face-unlock.1.scd b/doc/plasma-face-unlock.1.scd
index de8e312..817ea0b 100644
--- a/doc/plasma-face-unlock.1.scd
+++ b/doc/plasma-face-unlock.1.scd
@@ -171,7 +171,9 @@ The agent watches for the screen to lock (org.freedesktop.ScreenSaver) and
scans when somebody comes back:
- on any key or mouse movement after the screen locked, once 1.5 seconds have
- passed (the key that locked it does not count);
+ passed (the key that locked it does not count). Enter on the empty password
+ field is a key like any other. This works while a video or an app keeps the
+ screen on, too (ext_idle_notifier_v1, input notification);
- when the machine wakes from sleep;
- right after locking, if *Look right after the screen locks* is on. Off by
default: whoever locks their screen on purpose is usually still in front of
@@ -256,7 +258,7 @@ _$XDG_RUNTIME_DIR/plasma-face-unlock/agent.socket_
# REQUIREMENTS
KDE Plasma 6 on Wayland, a camera, OpenCV 4.5.4 or newer with its DNN module,
-Qt 6, LayerShellQt, KIdleTime, KI18n, systemd, polkit and Linux-PAM.
+Qt 6, LayerShellQt, KI18n, systemd, polkit and Linux-PAM.
# SEE ALSO
diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD
index e5c482d..577cdb3 100644
--- a/packaging/aur/PKGBUILD
+++ b/packaging/aur/PKGBUILD
@@ -11,7 +11,7 @@ arch=('x86_64' 'aarch64')
url="https://github.com/LoonixTools/plasma-face-unlock"
license=('GPL-3.0-or-later' 'MIT' 'Apache-2.0')
depends=('bash' 'coreutils' 'gawk' 'grep' 'sed' 'gettext' 'systemd' 'systemd-libs' 'pam' 'polkit'
- 'opencv' 'qt6-base' 'qt6-declarative' 'layer-shell-qt' 'kidletime' 'ki18n' 'kscreenlocker')
+ 'opencv' 'qt6-base' 'qt6-declarative' 'layer-shell-qt' 'ki18n' 'kscreenlocker')
makedepends=('cmake' 'scdoc')
install="${pkgname}.install"
source=("${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/v${pkgver}.tar.gz"
diff --git a/packaging/rpm/plasma-face-unlock.spec b/packaging/rpm/plasma-face-unlock.spec
index 7a45ae4..3e12757 100644
--- a/packaging/rpm/plasma-face-unlock.spec
+++ b/packaging/rpm/plasma-face-unlock.spec
@@ -34,7 +34,6 @@ BuildRequires: cmake(Qt6WaylandClient)
BuildRequires: qt6-qtbase-private-devel
BuildRequires: qt6-qtwayland-devel
BuildRequires: cmake(LayerShellQt)
-BuildRequires: cmake(KF6IdleTime)
BuildRequires: cmake(KF6I18n)
Requires: bash >= 4.2
diff --git a/protocols/ext-idle-notify-v1.xml b/protocols/ext-idle-notify-v1.xml
new file mode 100644
index 0000000..db7d9c1
--- /dev/null
+++ b/protocols/ext-idle-notify-v1.xml
@@ -0,0 +1,131 @@
+
+
+
+ Copyright © 2015 Martin Gräßlin
+ Copyright © 2022 Simon Ser
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+
+
+
+ This interface allows clients to monitor user idle status.
+
+ After binding to this global, clients can create ext_idle_notification_v1
+ objects to get notified when the user is idle for a given amount of time.
+
+
+
+
+ Destroy the manager object. All objects created via this interface
+ remain valid.
+
+
+
+
+
+ Create a new idle notification object.
+
+ The notification object has a minimum timeout duration and is tied to a
+ seat. The client will be notified if the seat is inactive for at least
+ the provided timeout. See ext_idle_notification_v1 for more details.
+
+ A zero timeout is valid and means the client wants to be notified as
+ soon as possible when the seat is inactive.
+
+
+
+
+
+
+
+
+
+
+ Create a new idle notification object to track input from the
+ user, such as keyboard and mouse movement. Because this object is
+ meant to track user input alone, it ignores idle inhibitors.
+
+ The notification object has a minimum timeout duration and is tied to a
+ seat. The client will be notified if the seat is inactive for at least
+ the provided timeout. See ext_idle_notification_v1 for more details.
+
+ A zero timeout is valid and means the client wants to be notified as
+ soon as possible when the seat is inactive.
+
+
+
+
+
+
+
+
+
+
+ This interface is used by the compositor to send idle notification events
+ to clients.
+
+ Initially the notification object is not idle. The notification object
+ becomes idle when no user activity has happened for at least the timeout
+ duration, starting from the creation of the notification object. User
+ activity may include input events or a presence sensor, but is
+ compositor-specific.
+
+ How this notification responds to idle inhibitors depends on how
+ it was constructed. If constructed from the
+ get_idle_notification request, then if an idle inhibitor is
+ active (e.g. another client has created a zwp_idle_inhibitor_v1
+ on a visible surface), the compositor must not make the
+ notification object idle. However, if constructed from the
+ get_input_idle_notification request, then idle inhibitors are
+ ignored, and only input from the user, e.g. from a keyboard or
+ mouse, counts as activity.
+
+ When the notification object becomes idle, an idled event is sent. When
+ user activity starts again, the notification object stops being idle,
+ a resumed event is sent and the timeout is restarted.
+
+
+
+
+ Destroy the notification object.
+
+
+
+
+
+ This event is sent when the notification object becomes idle.
+
+ It's a compositor protocol error to send this event twice without a
+ resumed event in-between.
+
+
+
+
+
+ This event is sent when the notification object stops being idle.
+
+ It's a compositor protocol error to send this event twice without an
+ idled event in-between. It's a compositor protocol error to send this
+ event prior to any idled event.
+
+
+
+
diff --git a/src/agent/inputwatcher.cpp b/src/agent/inputwatcher.cpp
new file mode 100644
index 0000000..5d01760
--- /dev/null
+++ b/src/agent/inputwatcher.cpp
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "inputwatcher.h"
+
+#include
+#include
+#include
+
+#include
+
+#include "qwayland-ext-idle-notify-v1.h"
+
+class IdleNotifier : public QWaylandClientExtensionTemplate, public QtWayland::ext_idle_notifier_v1
+{
+public:
+ IdleNotifier()
+ : QWaylandClientExtensionTemplate(2)
+ {
+ initialize();
+ }
+ ~IdleNotifier() override
+ {
+ if (isActive()) {
+ destroy();
+ }
+ }
+};
+
+class IdleNotification : public QtWayland::ext_idle_notification_v1
+{
+public:
+ IdleNotification(::ext_idle_notification_v1 *object, std::function resumed)
+ : QtWayland::ext_idle_notification_v1(object)
+ , m_resumed(std::move(resumed))
+ {
+ }
+ ~IdleNotification() override
+ {
+ destroy();
+ }
+
+protected:
+ // Only ever sent after idled: calm first, then this.
+ void ext_idle_notification_v1_resumed() override
+ {
+ if (!m_fired) {
+ m_fired = true;
+ m_resumed();
+ }
+ }
+
+private:
+ std::function m_resumed;
+ bool m_fired = false;
+};
+
+InputWatcher::InputWatcher(QObject *parent)
+ : QObject(parent)
+ , m_notifier(std::make_unique())
+{
+}
+
+InputWatcher::~InputWatcher() = default;
+
+void InputWatcher::watch(int calmMs)
+{
+ m_notification.reset();
+ auto *wayland = qGuiApp->nativeInterface();
+ if (!m_notifier->isActive() || !wayland || !wayland->seat()) {
+ qWarning("no ext_idle_notifier_v1, so no telling when somebody comes back");
+ return;
+ }
+ // Version 1 has only the notification that inhibitors hold back.
+ ::ext_idle_notification_v1 *object = m_notifier->QWaylandClientExtension::version() >= 2
+ ? m_notifier->get_input_idle_notification(uint32_t(calmMs), wayland->seat())
+ : m_notifier->get_idle_notification(uint32_t(calmMs), wayland->seat());
+ // Queued: not from inside the Wayland event, which the receiver might
+ // answer by watching anew and so destroying the notification.
+ m_notification = std::make_unique(object, [this] {
+ QMetaObject::invokeMethod(this, &InputWatcher::input, Qt::QueuedConnection);
+ });
+}
+
+void InputWatcher::stop()
+{
+ m_notification.reset();
+}
diff --git a/src/agent/inputwatcher.h b/src/agent/inputwatcher.h
new file mode 100644
index 0000000..b637cbe
--- /dev/null
+++ b/src/agent/inputwatcher.h
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// Somebody touching the machine: a key, a click, the mouse, the touchpad.
+//
+// From ext_idle_notifier_v1, like KIdleTime, but with the input notification
+// of version 2. KIdleTime's own honours idle inhibitors: with a video playing
+// or an app keeping the screen on, no key reached it and the lock screen never
+// scanned.
+
+#pragma once
+
+#include
+
+#include
+
+class IdleNotifier;
+class IdleNotification;
+
+class InputWatcher : public QObject
+{
+ Q_OBJECT
+public:
+ explicit InputWatcher(QObject *parent = nullptr);
+ ~InputWatcher() override;
+
+ // Emit input() once, for the first input after nothing was touched for
+ // calmMs. 0: the next input. Replaces what was watched before.
+ void watch(int calmMs);
+ void stop();
+
+Q_SIGNALS:
+ void input();
+
+private:
+ std::unique_ptr m_notifier;
+ std::unique_ptr m_notification;
+};
diff --git a/src/agent/lockcontroller.cpp b/src/agent/lockcontroller.cpp
index 56f2665..654eac2 100644
--- a/src/agent/lockcontroller.cpp
+++ b/src/agent/lockcontroller.cpp
@@ -6,8 +6,6 @@
#include "daemonclient.h"
#include "userconfig.h"
-#include
-
#include
#include
#include
@@ -38,7 +36,10 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, QOb
, m_config(config)
{
m_armTimer.setSingleShot(true);
- connect(&m_armTimer, &QTimer::timeout, this, &LockController::arm);
+ connect(&m_armTimer, &QTimer::timeout, this, [this] {
+ arm(0);
+ });
+ connect(&m_input, &InputWatcher::input, this, &LockController::onResume);
QDBusConnection session = QDBusConnection::sessionBus();
session.connect(QStringLiteral("org.freedesktop.ScreenSaver"),
@@ -55,17 +56,6 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, QOb
this,
SLOT(onPrepareForSleep(bool)));
- KIdleTime *idle = KIdleTime::instance();
- connect(idle, &KIdleTime::resumingFromIdle, this, &LockController::onResume);
- connect(idle, qOverload(&KIdleTime::timeoutReached), this, [this, idle](int id, int) {
- if (id != m_idleId) {
- return;
- }
- idle->removeIdleTimeout(id);
- m_idleId = -1;
- arm();
- });
-
// Started while the screen is already locked (the agent restarted).
const QDBusMessage get = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"),
QStringLiteral("/ScreenSaver"),
@@ -86,16 +76,10 @@ void LockController::onActiveChanged(bool active)
if (active == m_locked) {
return;
}
- KIdleTime *idle = KIdleTime::instance();
-
if (!active) {
m_locked = false;
m_armTimer.stop();
- if (m_idleId >= 0) {
- idle->removeIdleTimeout(m_idleId);
- m_idleId = -1;
- }
- idle->stopCatchingResumeEvent();
+ m_input.stop();
if (m_scan) {
m_scan->abort();
m_scan->deleteLater();
@@ -148,12 +132,12 @@ void LockController::onPrepareForSleep(bool sleeping)
}
}
-void LockController::arm()
+void LockController::arm(int calmMs)
{
if (!m_locked || m_stopped || !m_config->scanOnWake()) {
return;
}
- KIdleTime::instance()->catchNextResumeEvent();
+ m_input.watch(calmMs);
}
void LockController::onResume()
@@ -221,9 +205,7 @@ void LockController::onScanFinished(const QJsonObject &result)
m_armTimer.start(GraceAfterLockMs);
return;
}
- if (m_idleId < 0) {
- m_idleId = KIdleTime::instance()->addIdleTimeout(CalmBeforeRetryMs);
- }
+ arm(CalmBeforeRetryMs);
}
void LockController::unlock()
diff --git a/src/agent/lockcontroller.h b/src/agent/lockcontroller.h
index 02e63d0..eb871db 100644
--- a/src/agent/lockcontroller.h
+++ b/src/agent/lockcontroller.h
@@ -19,6 +19,8 @@
#pragma once
+#include "inputwatcher.h"
+
#include
#include
#include
@@ -44,7 +46,8 @@ private Q_SLOTS:
void onPrepareForSleep(bool sleeping);
private:
- void arm();
+ // Scan at the next input after calmMs without any.
+ void arm(int calmMs);
void onResume();
void startScan(const QString &why);
void onScanFinished(const QJsonObject &result);
@@ -58,5 +61,5 @@ private:
QElapsedTimer m_lockedFor;
QPointer m_scan;
QTimer m_armTimer;
- int m_idleId = -1;
+ InputWatcher m_input;
};