Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2196,6 +2196,10 @@
"description": "Unlock with a fingerprint reader via fprintd, alongside password entry",
"label": "Fingerprint Unlock"
},
"grace-period": {
"description": "Allow passwordless unlock for this many seconds after locking; any keypress or mouse movement unlocks. Set to 0 to always require the password",
"label": "Grace Period"
},
"lock-before-suspend": {
"description": "Lock the session before sleep (lid close, systemctl suspend, hibernate); Lock & Suspend actions still lock first",
"label": "Lock Before Suspend"
Expand Down
1 change: 1 addition & 0 deletions example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ privacy = true # microphone/camera/screen-share capture cha

[lockscreen]
enabled = true
grace_period_seconds = 5 # unlock without a password on any keypress or mouse movement for this long after locking (0 = off)
blurred_desktop = false # use a desktop snapshot as the lock screen background (requires wlr-screencopy)
blur_intensity = 0.5 # lock screen background blur (0.0 = none, 1.0 = maximum)
tint_intensity = 0.3 # surface-color tint over the lock screen background
Expand Down
1 change: 1 addition & 0 deletions src/app/application_services.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,7 @@ void Application::initSystemBusServices() {
// callback and force an immediate repaint of the lock surfaces.
if (m_lockScreen.isActive()) {
m_lockScreen.forceRepaintAfterResume();
m_lockScreen.onSystemResumed();
}
m_weatherService.requestRefresh();
m_gammaService.reevaluateSchedule();
Expand Down
3 changes: 3 additions & 0 deletions src/config/config_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,9 @@ struct LockscreenConfig {
// Lock on PrepareForSleep (lid close / systemctl suspend) via logind sleep-delay inhibit.
// Distinct from idle/session lock_and_suspend actions.
bool lockBeforeSuspend = true;
// Allow passwordless unlock for this many seconds after locking on any keypress
// or mouse movement beyond 5px (0 disables the grace period).
int gracePeriodSeconds = 5;
bool fingerprint = true;
bool allowEmptyPassword = false;
bool blurredDesktop = false;
Expand Down
1 change: 1 addition & 0 deletions src/config/schema/config_schema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ namespace noctalia::config::schema {
static const Schema<LockscreenConfig> s = {
field(&LockscreenConfig::enabled, "enabled"),
field(&LockscreenConfig::lockBeforeSuspend, "lock_before_suspend"),
field(&LockscreenConfig::gracePeriodSeconds, "grace_period_seconds", Range<std::int64_t>{0, 60}),
field(&LockscreenConfig::fingerprint, "fingerprint"),
field(&LockscreenConfig::allowEmptyPassword, "allow_empty_password"),
field(&LockscreenConfig::blurredDesktop, "blurred_desktop"),
Expand Down
63 changes: 63 additions & 0 deletions src/shell/lockscreen/lock_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,20 @@
#include "wayland/wayland_seat.h"

#include <algorithm>
#include <chrono>
#include <cmath>
#include <string>
#include <thread>

namespace {

constexpr Logger kLog("lockscreen");

std::int64_t wallClockMillis() {
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count();
}

Color resolveWallpaperFillColor(const WallpaperConfig& config) {
// The lockscreen is an ext-session-lock surface: any transparency lets the
// compositor's "client hasn't painted" fallback (e.g. niri's red) bleed
Expand Down Expand Up @@ -120,6 +127,9 @@ bool LockScreen::lock() {
if (isActive()) {
return true;
}
m_lockedAtMillis = wallClockMillis();
m_graceAllowed = true;
kLog.debug("lock requested, grace starts at {}", m_lockedAtMillis);
if (!m_wayland->hasSessionLockManager()) {
kLog.warn("session lock protocol unavailable");
return false;
Expand Down Expand Up @@ -176,6 +186,7 @@ void LockScreen::unlock() {
return;
}

resetGracePeriod();
m_pendingAfterLocked = {};
m_suspendTimeoutTimer.stop();
invalidatePendingAuthentication();
Expand Down Expand Up @@ -330,6 +341,8 @@ void LockScreen::onPointerEvent(const PointerEvent& event) {

if (event.type == PointerEvent::Type::Enter && event.surface != nullptr) {
m_pointerSurface = event.surface;
m_pointerEnterX = event.sx;
m_pointerEnterY = event.sy;
} else if (event.type == PointerEvent::Type::Leave && event.surface == m_pointerSurface) {
m_pointerSurface = nullptr;
} else if (
Expand All @@ -338,6 +351,15 @@ void LockScreen::onPointerEvent(const PointerEvent& event) {
m_pointerSurface = event.surface;
}

if (event.type == PointerEvent::Type::Motion && isInGracePeriod()) {
const double dx = event.sx - m_pointerEnterX;
const double dy = event.sy - m_pointerEnterY;
if (std::sqrt(dx * dx + dy * dy) > 5.0) {
tryGraceUnlock();
return;
}
}

wl_surface* target = event.surface != nullptr ? event.surface : m_pointerSurface;
if (target == nullptr) {
return;
Expand All @@ -362,6 +384,11 @@ void LockScreen::onKeyboardEvent(const KeyboardEvent& event) {
return;
}

if (isInGracePeriod()) {
tryGraceUnlock();
return;
}

// The password field always owns plain printable keys; Space is a Validate
// chord but must type a space, not submit (passwords may contain spaces).
if (!isPlainPrintableKey(event.utf32, event.modifiers, event.preedit)
Expand Down Expand Up @@ -404,6 +431,40 @@ void LockScreen::onKeyboardEvent(const KeyboardEvent& event) {

bool LockScreen::isActive() const noexcept { return m_lockPending || m_locked; }

bool LockScreen::isInGracePeriod() const noexcept {
if (!m_graceAllowed || !m_locked || m_lockedAtMillis <= 0) {
return false;
}
if (m_configService == nullptr || m_configService->config().lockscreen.gracePeriodSeconds <= 0) {
return false;
}
// Wall-clock comparison, evaluated fresh on every interaction: a cached
// timestamp would keep the grace window alive indefinitely.
const std::int64_t windowMillis =
static_cast<std::int64_t>(m_configService->config().lockscreen.gracePeriodSeconds) * 1000;
return wallClockMillis() < m_lockedAtMillis + windowMillis;
}

void LockScreen::tryGraceUnlock() {
if (!isInGracePeriod()) {
return;
}
kLog.info("unlocking within grace period (lockedAt={})", m_lockedAtMillis);
unlock();
}

void LockScreen::resetGracePeriod() {
m_lockedAtMillis = 0;
m_graceAllowed = false;
}

void LockScreen::onSystemResumed() {
if (m_graceAllowed) {
kLog.info("system resumed; revoking passwordless grace period");
}
resetGracePeriod();
}

bool LockScreen::isSessionLocked() const noexcept { return m_locked; }

bool LockScreen::tryFlushPendingAfterLocked() {
Expand Down Expand Up @@ -483,6 +544,7 @@ void LockScreen::handleFinished(void* data, ext_session_lock_v1* /*lock*/) {
auto* self = static_cast<LockScreen*>(data);
kLog.info("session lock finished by compositor");
const bool wasLockedInteractive = self->m_locked;
self->resetGracePeriod();
self->m_pendingAfterLocked = {};
self->invalidatePendingAuthentication();
self->stopFingerprint();
Expand Down Expand Up @@ -751,6 +813,7 @@ void LockScreen::resetLockState() {
m_pendingAfterLocked = {};
m_suspendTimeoutTimer.stop();
m_lockDeferred = false;
resetGracePeriod();
if (m_lock == nullptr) {
m_lockPending = false;
m_locked = false;
Expand Down
13 changes: 13 additions & 0 deletions src/shell/lockscreen/lock_screen.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class LockScreen {
/// After suspend/resume, discard pending callbacks on active lock surfaces
/// while preserving queued work, then request an immediate redraw.
void forceRepaintAfterResume();
/// After suspend/resume, revoke the passwordless grace period as a safety net;
/// the wall-clock expiry check alone would also have expired it by then.
void onSystemResumed();
void onPointerEvent(const PointerEvent& event);
void onKeyboardEvent(const KeyboardEvent& event);
[[nodiscard]] bool isActive() const noexcept;
Expand Down Expand Up @@ -121,6 +124,11 @@ class LockScreen {
void stopFingerprint();
void handleFingerprintStatus(const std::string& message, bool isError);
static void clearSensitiveString(std::string& value);
/// Grace period: any keypress or mouse movement beyond 5px unlocks without a
/// password within the configured window after locking (matches hyprlock).
[[nodiscard]] bool isInGracePeriod() const noexcept;
void tryGraceUnlock();
void resetGracePeriod();

WaylandConnection* m_wayland = nullptr;
RenderContext* m_renderContext = nullptr;
Expand Down Expand Up @@ -153,4 +161,9 @@ class LockScreen {
const WeatherService* m_weather = nullptr;
HttpClient* m_httpClient = nullptr;
Timer m_suspendTimeoutTimer;
// Wall-clock millis when the lock flow started; grace expires gracePeriodSeconds after it.
std::int64_t m_lockedAtMillis = 0;
bool m_graceAllowed = false;
double m_pointerEnterX = 0.0;
double m_pointerEnterY = 0.0;
};
16 changes: 16 additions & 0 deletions src/shell/settings/settings_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,22 @@ namespace settings {
e.visibleWhen = lockscreenOn;
entries.push_back(std::move(e));
}
{
auto e = makeEntry(
SettingsSection::Security, "lock-screen", tr("settings.schema.lockscreen.grace-period.label"),
tr("settings.schema.lockscreen.grace-period.description"), {"lockscreen", "grace_period_seconds"},
StepperSetting{
.value = std::clamp(cfg.lockscreen.gracePeriodSeconds, 0, 60),
.minValue = 0,
.maxValue = 60,
.step = 1,
.valueSuffix = "s"
},
"lock screen grace period passwordless unlock keypress mouse movement"
);
e.visibleWhen = lockscreenOn;
entries.push_back(std::move(e));
}
{
auto e = makeEntry(
SettingsSection::Security, "lock-screen", tr("settings.schema.lockscreen.fingerprint.label"),
Expand Down
1 change: 1 addition & 0 deletions tests/config_schema_roundtrip_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ location = "https://example.invalid/bad"
c.backdrop = BackdropConfig{true, 0.8f, 0.2f};
c.lockscreen = LockscreenConfig{
.lockBeforeSuspend = false,
.gracePeriodSeconds = 3,
.blurredDesktop = true,
.blurIntensity = 0.6f,
.tintIntensity = 0.25f,
Expand Down