feat(lockscreen): add session lock flow with PAM auth

This commit is contained in:
Lysec
2026-04-07 13:20:24 +02:00
parent 915e52ca38
commit 2731409290
17 changed files with 1092 additions and 26 deletions
+28
View File
@@ -29,6 +29,7 @@ pkg_check_modules(HARFBUZZ REQUIRED harfbuzz)
pkg_check_modules(FONTCONFIG REQUIRED fontconfig)
pkg_check_modules(XKBCOMMON REQUIRED xkbcommon)
pkg_check_modules(LIBPIPEWIRE REQUIRED libpipewire-0.3)
find_library(PAM_LIBRARY NAMES pam REQUIRED)
# --- msdfgen (vendored) ---
set(MSDFGEN_CORE_ONLY OFF CACHE BOOL "" FORCE)
@@ -63,6 +64,8 @@ set(CURSOR_SHAPE_XML
"${CMAKE_CURRENT_SOURCE_DIR}/protocols/cursor-shape-v1.xml")
set(XDG_ACTIVATION_XML
"${WAYLAND_PROTOCOLS_PKGDATADIR}/staging/xdg-activation/xdg-activation-v1.xml")
set(EXT_SESSION_LOCK_XML
"${WAYLAND_PROTOCOLS_PKGDATADIR}/staging/ext-session-lock/ext-session-lock-v1.xml")
set(XDG_OUTPUT_PROTOCOL_C
"${GENERATED_PROTOCOL_DIR}/xdg-output-unstable-v1-client-protocol.c")
@@ -86,6 +89,10 @@ set(XDG_ACTIVATION_PROTOCOL_C
"${GENERATED_PROTOCOL_DIR}/xdg-activation-v1-client-protocol.c")
set(XDG_ACTIVATION_PROTOCOL_H
"${GENERATED_PROTOCOL_DIR}/xdg-activation-v1-client-protocol.h")
set(EXT_SESSION_LOCK_PROTOCOL_C
"${GENERATED_PROTOCOL_DIR}/ext-session-lock-v1-client-protocol.c")
set(EXT_SESSION_LOCK_PROTOCOL_H
"${GENERATED_PROTOCOL_DIR}/ext-session-lock-v1-client-protocol.h")
set(WLR_LAYER_SHELL_PROTOCOL_C
"${GENERATED_PROTOCOL_DIR}/wlr-layer-shell-unstable-v1-client-protocol.c")
@@ -188,6 +195,20 @@ add_custom_command(
VERBATIM
)
add_custom_command(
OUTPUT "${EXT_SESSION_LOCK_PROTOCOL_C}"
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" private-code "${EXT_SESSION_LOCK_XML}" "${EXT_SESSION_LOCK_PROTOCOL_C}"
DEPENDS "${EXT_SESSION_LOCK_XML}"
VERBATIM
)
add_custom_command(
OUTPUT "${EXT_SESSION_LOCK_PROTOCOL_H}"
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" client-header "${EXT_SESSION_LOCK_XML}" "${EXT_SESSION_LOCK_PROTOCOL_H}"
DEPENDS "${EXT_SESSION_LOCK_XML}"
VERBATIM
)
add_custom_target(noctalia_wayland_protocols
DEPENDS
"${WLR_LAYER_SHELL_PROTOCOL_C}"
@@ -202,6 +223,8 @@ add_custom_target(noctalia_wayland_protocols
"${CURSOR_SHAPE_PROTOCOL_H}"
"${XDG_ACTIVATION_PROTOCOL_C}"
"${XDG_ACTIVATION_PROTOCOL_H}"
"${EXT_SESSION_LOCK_PROTOCOL_C}"
"${EXT_SESSION_LOCK_PROTOCOL_H}"
)
# --- Target ---
@@ -209,6 +232,7 @@ add_executable(noctalia
src/main.cpp
src/app/application.cpp
src/app/main_loop.cpp
src/auth/pam_authenticator.cpp
src/system/distro_info.cpp
src/system/system_monitor_service.cpp
src/debug/debug_service.cpp
@@ -245,6 +269,8 @@ add_executable(noctalia
src/render/image_loaders.cpp
src/render/wallpaper_renderer.cpp
src/shell/bar/bar.cpp
src/shell/lockscreen/lock_screen.cpp
src/shell/lockscreen/lock_surface.cpp
src/shell/wallpaper/wallpaper.cpp
src/shell/wallpaper/wallpaper_surface.cpp
src/time/time_service.cpp
@@ -315,6 +341,7 @@ add_executable(noctalia
"${EXT_WORKSPACE_PROTOCOL_C}"
"${CURSOR_SHAPE_PROTOCOL_C}"
"${XDG_ACTIVATION_PROTOCOL_C}"
"${EXT_SESSION_LOCK_PROTOCOL_C}"
)
target_compile_definitions(noctalia PRIVATE
NOCTALIA_ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets"
@@ -362,6 +389,7 @@ target_link_libraries(noctalia PRIVATE
${FONTCONFIG_LIBRARIES}
${XKBCOMMON_LIBRARIES}
${LIBPIPEWIRE_LIBRARIES}
${PAM_LIBRARY}
)
target_compile_options(noctalia PRIVATE
${WAYLAND_CLIENT_CFLAGS_OTHER}
+9 -1
View File
@@ -13,7 +13,15 @@ build m=mode:
cmake --build build-{{m}} --parallel
run m=mode:
./build-{{m}}/noctalia
@if [ "{{m}}" = "debug" ]; then \
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/noctalia"; \
mkdir -p "$cache_dir"; \
logfile="$cache_dir/noctalia-$(date +%Y%m%d-%H%M%S).log"; \
echo "Writing logs to $logfile"; \
./build-{{m}}/noctalia 2>&1 | tee -a "$logfile"; \
else \
./build-{{m}}/noctalia; \
fi
format:
clang-format -i src/**/*.cpp src/**/*.h
+23 -1
View File
@@ -102,6 +102,7 @@ void Application::initServices() {
m_wayland.setOutputChangeCallback([this]() {
m_wallpaper.onOutputChange();
m_bar.onOutputChange();
m_lockScreen.onOutputChange();
});
m_wayland.setWorkspaceChangeCallback([this]() { m_bar.onWorkspaceChange(); });
@@ -218,6 +219,7 @@ void Application::initServices() {
void Application::initUi() {
m_renderContext.initialize(m_wayland.display());
m_lockScreen.initialize(m_wayland, &m_renderContext, &m_stateService);
// Panel manager must be before bar so widgets can access PanelManager::instance()
m_panelManager.initialize(m_wayland, &m_configService, &m_renderContext);
@@ -258,6 +260,10 @@ void Application::initUi() {
}
m_wayland.setPointerEventCallback([this](const PointerEvent& event) {
if (m_lockScreen.isActive()) {
m_lockScreen.onPointerEvent(event);
return;
}
if (m_trayMenu.onPointerEvent(event))
return;
if (m_bar.onPointerEvent(event))
@@ -267,7 +273,13 @@ void Application::initUi() {
m_notificationPopup.onPointerEvent(event);
});
m_wayland.setKeyboardEventCallback([this](const KeyboardEvent& event) { m_panelManager.onKeyboardEvent(event); });
m_wayland.setKeyboardEventCallback([this](const KeyboardEvent& event) {
if (m_lockScreen.isActive()) {
m_lockScreen.onKeyboardEvent(event);
return;
}
m_panelManager.onKeyboardEvent(event);
});
}
void Application::initIpc() {
@@ -325,6 +337,16 @@ void Application::initIpc() {
},
"toggle-bar", "Toggle bar visibility");
m_ipcService.registerHandler(
"lock",
[this](const std::string&) -> std::string {
if (m_lockScreen.lock()) {
return "ok\n";
}
return "error: lock screen unavailable\n";
},
"lock", "Lock the session using the development lock screen (press Escape to unlock)");
m_ipcService.registerHandler(
"toggle-panel",
[this](const std::string& args) -> std::string {
+2
View File
@@ -23,6 +23,7 @@
#include "pipewire/pipewire_service.h"
#include "render/render_context.h"
#include "shell/bar/bar.h"
#include "shell/lockscreen/lock_screen.h"
#include "shell/notification/notification_popup.h"
#include "shell/osd/audio_osd.h"
#include "shell/osd/osd_overlay.h"
@@ -73,6 +74,7 @@ private:
RenderContext m_renderContext;
Bar m_bar;
LockScreen m_lockScreen;
PanelManager m_panelManager;
NotificationPopup m_notificationPopup;
AudioOsd m_audioOsd;
+142
View File
@@ -0,0 +1,142 @@
#include "auth/pam_authenticator.h"
#include <pwd.h>
#include <security/pam_appl.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <vector>
namespace {
void secureClear(std::string& value) {
volatile char* ptr = value.empty() ? nullptr : &value[0];
for (std::size_t i = 0; i < value.size(); ++i) {
ptr[i] = '\0';
}
value.clear();
}
struct PamConversationData {
const char* password = nullptr;
};
int pamConversation(int numMsg, const pam_message** msg, pam_response** response, void* appdataPtr) {
if (numMsg <= 0 || msg == nullptr || response == nullptr || appdataPtr == nullptr) {
return PAM_CONV_ERR;
}
auto* data = static_cast<PamConversationData*>(appdataPtr);
auto* replies = static_cast<pam_response*>(std::calloc(static_cast<std::size_t>(numMsg), sizeof(pam_response)));
if (replies == nullptr) {
return PAM_BUF_ERR;
}
for (int i = 0; i < numMsg; ++i) {
if (msg[i] == nullptr) {
std::free(replies);
return PAM_CONV_ERR;
}
switch (msg[i]->msg_style) {
case PAM_PROMPT_ECHO_OFF:
replies[i].resp = ::strdup(data->password != nullptr ? data->password : "");
break;
case PAM_PROMPT_ECHO_ON:
replies[i].resp = ::strdup("");
break;
case PAM_ERROR_MSG:
case PAM_TEXT_INFO:
replies[i].resp = nullptr;
break;
default:
for (int j = 0; j <= i; ++j) {
if (replies[j].resp != nullptr) {
std::free(replies[j].resp);
}
}
std::free(replies);
return PAM_CONV_ERR;
}
if ((msg[i]->msg_style == PAM_PROMPT_ECHO_OFF || msg[i]->msg_style == PAM_PROMPT_ECHO_ON) &&
replies[i].resp == nullptr) {
for (int j = 0; j <= i; ++j) {
if (replies[j].resp != nullptr) {
std::free(replies[j].resp);
}
}
std::free(replies);
return PAM_BUF_ERR;
}
}
*response = replies;
return PAM_SUCCESS;
}
} // namespace
PamAuthenticator::Result PamAuthenticator::authenticateCurrentUser(std::string_view password) const {
if (password.empty()) {
return Result{.success = false, .message = "Password required"};
}
std::string user = currentUsername();
if (user.empty()) {
return Result{.success = false, .message = "Unable to resolve current user"};
}
std::string passwordCopy(password);
PamConversationData convData{.password = passwordCopy.c_str()};
pam_conv conv = {
.conv = &pamConversation,
.appdata_ptr = &convData,
};
pam_handle_t* pamh = nullptr;
const int startRc = pam_start("login", user.c_str(), &conv, &pamh);
if (startRc != PAM_SUCCESS || pamh == nullptr) {
secureClear(passwordCopy);
return Result{.success = false, .message = "PAM start failed"};
}
int rc = pam_authenticate(pamh, 0);
if (rc == PAM_SUCCESS) {
rc = pam_acct_mgmt(pamh, 0);
}
const char* err = pam_strerror(pamh, rc);
pam_end(pamh, rc);
secureClear(passwordCopy);
if (rc == PAM_SUCCESS) {
return Result{.success = true, .message = {}};
}
return Result{.success = false, .message = err != nullptr ? err : "Authentication failed"};
}
std::string PamAuthenticator::currentUsername() {
const uid_t uid = getuid();
passwd pwd{};
passwd* result = nullptr;
std::vector<char> buf(4096);
while (true) {
const int rc = getpwuid_r(uid, &pwd, buf.data(), buf.size(), &result);
if (rc == 0 && result != nullptr) {
return std::string(result->pw_name != nullptr ? result->pw_name : "");
}
if (rc != ERANGE) {
return {};
}
buf.resize(buf.size() * 2);
if (buf.size() > 1 << 20) {
return {};
}
}
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <string_view>
class PamAuthenticator {
public:
struct Result {
bool success = false;
std::string message;
};
[[nodiscard]] Result authenticateCurrentUser(std::string_view password) const;
[[nodiscard]] static std::string currentUsername();
};
+2 -1
View File
@@ -30,7 +30,8 @@ int scoreEntry(std::string_view pattern, const DesktopEntry& entry) {
if (!word.empty()) {
best = std::max(best, FuzzyMatch::score(pattern, word));
}
if (semi == std::string_view::npos) break;
if (semi == std::string_view::npos)
break;
start = semi + 1;
}
return best;
+1 -1
View File
@@ -1,8 +1,8 @@
#pragma once
#include "launcher/desktop_entry.h"
#include "system/icon_resolver.h"
#include "launcher/launcher_provider.h"
#include "system/icon_resolver.h"
#include <vector>
+387
View File
@@ -0,0 +1,387 @@
#include "shell/lockscreen/lock_screen.h"
#include "core/log.h"
#include "config/state_service.h"
#include "render/render_context.h"
#include "shell/lockscreen/lock_surface.h"
#include "wayland/wayland_connection.h"
#include "wayland/wayland_seat.h"
#include <algorithm>
#include <string>
#include <wayland-client.h>
#include <xkbcommon/xkbcommon-keysyms.h>
#include "ext-session-lock-v1-client-protocol.h"
namespace {
constexpr Logger kLog("lockscreen");
const ext_session_lock_v1_listener kSessionLockListener = {
.locked = &LockScreen::handleLocked,
.finished = &LockScreen::handleFinished,
};
bool hasTextInputModifiers(std::uint32_t modifiers) {
return (modifiers & (KeyMod::Ctrl | KeyMod::Alt | KeyMod::Super)) != 0;
}
std::string utf32ToUtf8(std::uint32_t cp) {
std::string out;
if (cp <= 0x7F) {
out.push_back(static_cast<char>(cp));
} else if (cp <= 0x7FF) {
out.push_back(static_cast<char>(0xC0U | ((cp >> 6) & 0x1FU)));
out.push_back(static_cast<char>(0x80U | (cp & 0x3FU)));
} else if (cp <= 0xFFFF) {
out.push_back(static_cast<char>(0xE0U | ((cp >> 12) & 0x0FU)));
out.push_back(static_cast<char>(0x80U | ((cp >> 6) & 0x3FU)));
out.push_back(static_cast<char>(0x80U | (cp & 0x3FU)));
} else {
out.push_back(static_cast<char>(0xF0U | ((cp >> 18) & 0x07U)));
out.push_back(static_cast<char>(0x80U | ((cp >> 12) & 0x3FU)));
out.push_back(static_cast<char>(0x80U | ((cp >> 6) & 0x3FU)));
out.push_back(static_cast<char>(0x80U | (cp & 0x3FU)));
}
return out;
}
std::size_t prevUtf8Pos(const std::string& s, std::size_t pos) {
if (pos == 0 || s.empty()) {
return 0;
}
std::size_t p = std::min(pos, s.size()) - 1;
while (p > 0 && (static_cast<unsigned char>(s[p]) & 0xC0U) == 0x80U) {
--p;
}
return p;
}
std::size_t utf8CodepointCount(const std::string& s) {
std::size_t count = 0;
for (std::size_t i = 0; i < s.size(); ++count) {
const unsigned char c = static_cast<unsigned char>(s[i]);
if ((c & 0x80U) == 0) {
i += 1;
} else if ((c & 0xE0U) == 0xC0U) {
i += 2;
} else if ((c & 0xF0U) == 0xE0U) {
i += 3;
} else if ((c & 0xF8U) == 0xF0U) {
i += 4;
} else {
i += 1;
}
}
return count;
}
std::string maskedCircles(std::size_t count) {
std::string out;
out.reserve(count * 3);
for (std::size_t i = 0; i < count; ++i) {
out += "\xE2\x97\x8F"; // U+25CF
}
return out;
}
} // namespace
LockScreen::LockScreen() = default;
LockScreen::~LockScreen() {
clearInstances();
resetLockState();
}
bool LockScreen::initialize(WaylandConnection& wayland, RenderContext* renderContext, StateService* stateService) {
m_wayland = &wayland;
m_renderContext = renderContext;
m_stateService = stateService;
m_user = PamAuthenticator::currentUsername();
return true;
}
bool LockScreen::lock() {
if (m_wayland == nullptr || m_renderContext == nullptr) {
return false;
}
if (isActive()) {
return true;
}
if (!m_wayland->hasSessionLockManager()) {
kLog.warn("session lock protocol unavailable");
return false;
}
m_lock = ext_session_lock_manager_v1_lock(m_wayland->sessionLockManager());
if (m_lock == nullptr) {
kLog.warn("failed to create session lock object");
return false;
}
if (ext_session_lock_v1_add_listener(m_lock, &kSessionLockListener, this) != 0) {
ext_session_lock_v1_destroy(m_lock);
m_lock = nullptr;
kLog.warn("failed to register session lock listener");
return false;
}
m_lockPending = true;
m_locked = false;
clearSensitiveString(m_password);
m_status = "Waiting for lock confirmation...";
m_statusIsError = false;
syncInstances();
if (m_instances.empty()) {
kLog.warn("no outputs available for lock screen");
resetLockState();
return false;
}
wl_display_flush(m_wayland->display());
kLog.info("session lock requested");
return true;
}
void LockScreen::unlock() {
if (!isActive()) {
return;
}
if (m_lock != nullptr) {
if (m_locked) {
ext_session_lock_v1_unlock_and_destroy(m_lock);
kLog.info("session unlock requested");
} else {
ext_session_lock_v1_destroy(m_lock);
kLog.info("session lock request cancelled");
}
m_lock = nullptr;
}
m_lockPending = false;
m_locked = false;
clearSensitiveString(m_password);
m_status.clear();
m_statusIsError = false;
clearInstances();
m_pointerSurface = nullptr;
wl_display_flush(m_wayland->display());
}
void LockScreen::onOutputChange() {
if (!isActive()) {
return;
}
syncInstances();
}
void LockScreen::onPointerEvent(const PointerEvent& event) {
if (!isActive()) {
return;
}
if (event.type == PointerEvent::Type::Enter && event.surface != nullptr) {
m_pointerSurface = event.surface;
} else if (event.type == PointerEvent::Type::Leave && event.surface == m_pointerSurface) {
m_pointerSurface = nullptr;
} else if ((event.type == PointerEvent::Type::Button || event.type == PointerEvent::Type::Axis) &&
event.surface != nullptr) {
m_pointerSurface = event.surface;
}
wl_surface* target = event.surface != nullptr ? event.surface : m_pointerSurface;
if (target == nullptr) {
return;
}
for (auto& instance : m_instances) {
if (instance.surface->wlSurface() == target) {
instance.surface->onPointerEvent(event);
return;
}
}
}
void LockScreen::onKeyboardEvent(const KeyboardEvent& event) {
if (!isActive() || !event.pressed) {
return;
}
if (!m_locked) {
return;
}
if (event.sym == XKB_KEY_Return || event.sym == XKB_KEY_KP_Enter) {
tryAuthenticate();
return;
}
if (event.sym == XKB_KEY_BackSpace) {
if (!m_password.empty()) {
m_password.erase(prevUtf8Pos(m_password, m_password.size()));
m_status.clear();
m_statusIsError = false;
updatePromptOnSurfaces();
}
return;
}
if (event.sym == XKB_KEY_Escape) {
clearSensitiveString(m_password);
m_status = "Password cleared";
m_statusIsError = false;
updatePromptOnSurfaces();
return;
}
if (!event.preedit && event.utf32 >= 0x20U && event.utf32 != 0x7FU && !hasTextInputModifiers(event.modifiers)) {
m_password += utf32ToUtf8(event.utf32);
m_status.clear();
m_statusIsError = false;
updatePromptOnSurfaces();
}
}
bool LockScreen::isActive() const noexcept { return m_lockPending || m_locked; }
void LockScreen::handleLocked(void* data, ext_session_lock_v1* /*lock*/) {
auto* self = static_cast<LockScreen*>(data);
self->m_lockPending = false;
self->m_locked = true;
self->m_status = "Type your password and press Enter.";
self->m_statusIsError = false;
for (auto& instance : self->m_instances) {
instance.surface->setLockedState(true);
instance.surface->setOnLogin([self]() { self->tryAuthenticate(); });
}
self->updatePromptOnSurfaces();
kLog.info("session is locked");
}
void LockScreen::handleFinished(void* data, ext_session_lock_v1* /*lock*/) {
auto* self = static_cast<LockScreen*>(data);
kLog.info("session lock finished by compositor");
if (self->m_lock != nullptr) {
if (self->m_locked) {
ext_session_lock_v1_unlock_and_destroy(self->m_lock);
} else {
ext_session_lock_v1_destroy(self->m_lock);
}
self->m_lock = nullptr;
}
self->m_lockPending = false;
self->m_locked = false;
clearSensitiveString(self->m_password);
self->m_status.clear();
self->m_statusIsError = false;
self->clearInstances();
self->m_pointerSurface = nullptr;
}
void LockScreen::syncInstances() {
if (m_wayland == nullptr) {
return;
}
const auto& outputs = m_wayland->outputs();
std::erase_if(m_instances, [&](Instance& instance) {
const bool exists = std::any_of(outputs.begin(), outputs.end(),
[&](const WaylandOutput& output) { return output.name == instance.outputName; });
return !exists;
});
for (const auto& output : outputs) {
const bool exists = std::any_of(m_instances.begin(), m_instances.end(),
[&](const Instance& instance) { return instance.outputName == output.name; });
if (!exists) {
createInstance(output);
}
}
}
void LockScreen::createInstance(const WaylandOutput& output) {
auto surface = std::make_unique<LockSurface>(*m_wayland);
surface->setRenderContext(m_renderContext);
surface->setLockedState(m_locked);
if (m_stateService != nullptr) {
surface->setWallpaperPath(m_stateService->getWallpaperPath(output.connectorName));
}
surface->setOnLogin([this]() { tryAuthenticate(); });
const auto masked = maskedCircles(utf8CodepointCount(m_password));
surface->setPromptState(m_user, masked, m_status, m_statusIsError);
if (!surface->initialize(m_lock, output.output, output.scale)) {
kLog.warn("failed to create lock surface for output {}", output.name);
return;
}
m_instances.push_back(Instance{
.outputName = output.name,
.output = output.output,
.surface = std::move(surface),
});
}
void LockScreen::resetLockState() {
if (m_lock == nullptr) {
m_lockPending = false;
m_locked = false;
return;
}
if (m_locked) {
ext_session_lock_v1_unlock_and_destroy(m_lock);
} else {
ext_session_lock_v1_destroy(m_lock);
}
m_lock = nullptr;
m_lockPending = false;
m_locked = false;
}
void LockScreen::clearInstances() { m_instances.clear(); }
void LockScreen::updatePromptOnSurfaces() {
const auto masked = maskedCircles(utf8CodepointCount(m_password));
for (auto& instance : m_instances) {
instance.surface->setPromptState(m_user, masked, m_status, m_statusIsError);
}
}
void LockScreen::tryAuthenticate() {
if (m_password.empty()) {
m_status = "Password required";
m_statusIsError = true;
updatePromptOnSurfaces();
return;
}
m_status = "Authenticating...";
m_statusIsError = false;
updatePromptOnSurfaces();
const auto result = m_authenticator.authenticateCurrentUser(m_password);
clearSensitiveString(m_password);
if (result.success) {
m_status = "Unlocked";
m_statusIsError = false;
updatePromptOnSurfaces();
unlock();
return;
}
m_status = result.message.empty() ? "Authentication failed" : result.message;
m_statusIsError = true;
updatePromptOnSurfaces();
}
void LockScreen::clearSensitiveString(std::string& value) {
volatile char* ptr = value.empty() ? nullptr : &value[0];
for (std::size_t i = 0; i < value.size(); ++i) {
ptr[i] = '\0';
}
value.clear();
}
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include "auth/pam_authenticator.h"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
struct KeyboardEvent;
struct PointerEvent;
struct WaylandOutput;
struct ext_session_lock_v1;
struct wl_surface;
struct wl_output;
class StateService;
class LockSurface;
class RenderContext;
class WaylandConnection;
class LockScreen {
public:
LockScreen();
~LockScreen();
bool initialize(WaylandConnection& wayland, RenderContext* renderContext, StateService* stateService);
bool lock();
void unlock();
void onOutputChange();
void onPointerEvent(const PointerEvent& event);
void onKeyboardEvent(const KeyboardEvent& event);
[[nodiscard]] bool isActive() const noexcept;
static void handleLocked(void* data, ext_session_lock_v1* lock);
static void handleFinished(void* data, ext_session_lock_v1* lock);
private:
struct Instance {
std::uint32_t outputName = 0;
wl_output* output = nullptr;
std::unique_ptr<LockSurface> surface;
};
void syncInstances();
void createInstance(const WaylandOutput& output);
void resetLockState();
void clearInstances();
void updatePromptOnSurfaces();
void tryAuthenticate();
static void clearSensitiveString(std::string& value);
WaylandConnection* m_wayland = nullptr;
RenderContext* m_renderContext = nullptr;
StateService* m_stateService = nullptr;
ext_session_lock_v1* m_lock = nullptr;
std::vector<Instance> m_instances;
PamAuthenticator m_authenticator;
std::string m_user;
std::string m_password;
std::string m_status;
wl_surface* m_pointerSurface = nullptr;
bool m_statusIsError = false;
bool m_lockPending = false;
bool m_locked = false;
};
+295
View File
@@ -0,0 +1,295 @@
#include "shell/lockscreen/lock_surface.h"
#include "render/programs/rounded_rect_program.h"
#include "render/render_context.h"
#include "render/scene/image_node.h"
#include "render/scene/rect_node.h"
#include "render/scene/text_node.h"
#include "ui/controls/button.h"
#include "ui/controls/input.h"
#include "ui/palette.h"
#include "ui/style.h"
#include "wayland/wayland_connection.h"
#include "wayland/wayland_seat.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <ctime>
#include <memory>
#include <wayland-client.h>
#include "ext-session-lock-v1-client-protocol.h"
namespace {
const ext_session_lock_surface_v1_listener kLockSurfaceListener = {
.configure = &LockSurface::handleConfigure,
};
} // namespace
LockSurface::LockSurface(WaylandConnection& connection) : Surface(connection) {
auto wallpaper = std::make_unique<ImageNode>();
m_wallpaper = static_cast<ImageNode*>(m_root.addChild(std::move(wallpaper)));
auto backdrop = std::make_unique<RectNode>();
m_backdrop = static_cast<RectNode*>(m_root.addChild(std::move(backdrop)));
auto clockShadow = std::make_unique<TextNode>();
m_clockShadow = static_cast<TextNode*>(m_root.addChild(std::move(clockShadow)));
auto clock = std::make_unique<TextNode>();
m_clock = static_cast<TextNode*>(m_root.addChild(std::move(clock)));
auto loginPanel = std::make_unique<RectNode>();
m_loginPanel = static_cast<RectNode*>(m_root.addChild(std::move(loginPanel)));
auto passwordField = std::make_unique<Input>();
passwordField->setPlaceholder("Password");
m_passwordField = static_cast<Input*>(m_root.addChild(std::move(passwordField)));
auto loginButton = std::make_unique<Button>();
loginButton->setText("");
loginButton->setGlyph("check");
loginButton->setGlyphSize(16.0f);
loginButton->setVariant(ButtonVariant::Accent);
loginButton->setOnClick([this]() {
if (m_onLogin) {
m_onLogin();
}
});
m_loginButton = static_cast<Button*>(m_root.addChild(std::move(loginButton)));
m_inputDispatcher.setSceneRoot(&m_root);
m_inputDispatcher.setCursorShapeCallback(
[this](std::uint32_t serial, std::uint32_t shape) { m_connection.setCursorShape(serial, shape); });
setSceneRoot(&m_root);
setConfigureCallback([this](std::uint32_t width, std::uint32_t height) { layoutScene(width, height); });
updateCopy();
}
LockSurface::~LockSurface() {
if (renderContext() != nullptr) {
renderContext()->textureManager().unload(m_wallpaperTexture);
}
if (m_lockSurface != nullptr) {
ext_session_lock_surface_v1_destroy(m_lockSurface);
m_lockSurface = nullptr;
}
}
bool LockSurface::initialize(ext_session_lock_v1* lock, wl_output* output, std::int32_t scale) {
if (lock == nullptr || output == nullptr || renderContext() == nullptr) {
return false;
}
if (!createWlSurface()) {
return false;
}
m_output = output;
setScale(scale);
m_lockSurface = ext_session_lock_v1_get_lock_surface(lock, m_surface, output);
if (m_lockSurface == nullptr) {
destroySurface();
return false;
}
if (ext_session_lock_surface_v1_add_listener(m_lockSurface, &kLockSurfaceListener, this) != 0) {
ext_session_lock_surface_v1_destroy(m_lockSurface);
m_lockSurface = nullptr;
destroySurface();
return false;
}
setRunning(true);
return true;
}
void LockSurface::setLockedState(bool locked) {
if (m_locked == locked) {
return;
}
m_locked = locked;
updateCopy();
if (width() > 0 && height() > 0) {
layoutScene(width(), height());
requestRedraw();
}
}
void LockSurface::setPromptState(std::string user, std::string maskedPassword, std::string status, bool error) {
m_user = std::move(user);
m_maskedPassword = std::move(maskedPassword);
m_status = std::move(status);
m_error = error;
updateCopy();
if (width() > 0 && height() > 0) {
layoutScene(width(), height());
requestRedraw();
}
}
void LockSurface::setWallpaperPath(std::string wallpaperPath) {
if (m_wallpaperPath == wallpaperPath) {
return;
}
m_wallpaperPath = std::move(wallpaperPath);
m_wallpaperDirty = true;
if (width() > 0 && height() > 0) {
ensureWallpaperTexture();
layoutScene(width(), height());
requestRedraw();
}
}
void LockSurface::setOnLogin(std::function<void()> onLogin) { m_onLogin = std::move(onLogin); }
void LockSurface::onPointerEvent(const PointerEvent& event) {
switch (event.type) {
case PointerEvent::Type::Enter:
m_inputDispatcher.pointerEnter(static_cast<float>(event.sx), static_cast<float>(event.sy), event.serial);
break;
case PointerEvent::Type::Leave:
m_inputDispatcher.pointerLeave();
break;
case PointerEvent::Type::Motion:
m_inputDispatcher.pointerMotion(static_cast<float>(event.sx), static_cast<float>(event.sy), event.serial);
break;
case PointerEvent::Type::Button:
m_inputDispatcher.pointerButton(static_cast<float>(event.sx), static_cast<float>(event.sy), event.button,
event.state == WL_POINTER_BUTTON_STATE_PRESSED);
break;
case PointerEvent::Type::Axis:
m_inputDispatcher.pointerAxis(static_cast<float>(event.sx), static_cast<float>(event.sy), event.axis, event.axisValue,
event.axisDiscrete);
break;
}
}
void LockSurface::onKeyboardEvent(const KeyboardEvent& event) {
m_inputDispatcher.keyEvent(event.sym, event.utf32, event.modifiers, event.pressed, event.preedit);
}
void LockSurface::handleConfigure(void* data, ext_session_lock_surface_v1* lockSurface, std::uint32_t serial,
std::uint32_t width, std::uint32_t height) {
auto* self = static_cast<LockSurface*>(data);
ext_session_lock_surface_v1_ack_configure(lockSurface, serial);
self->Surface::onConfigure(width, height);
}
void LockSurface::layoutScene(std::uint32_t width, std::uint32_t height) {
auto* renderer = renderContext();
if (renderer == nullptr) {
return;
}
ensureWallpaperTexture();
const float sw = static_cast<float>(width);
const float sh = static_cast<float>(height);
const float panelWidth = std::min(sw - Style::spaceLg * 2.0f, 520.0f);
const float panelHeight = 78.0f;
const float panelX = std::round((sw - panelWidth) * 0.5f);
const float panelY = std::max(Style::spaceLg, sh - panelHeight - 84.0f);
m_root.setSize(sw, sh);
m_wallpaper->setPosition(0.0f, 0.0f);
m_wallpaper->setSize(sw, sh);
m_wallpaper->setTint(Color{1.0f, 1.0f, 1.0f, 0.95f});
m_backdrop->setPosition(0.0f, 0.0f);
m_backdrop->setSize(sw, sh);
m_backdrop->setVisible(false);
constexpr float kClockFontSize = 64.0f;
const auto clockMetrics = renderer->measureText(m_clock->text(), kClockFontSize, true);
const float clockX = sw - 48.0f - clockMetrics.width;
const float clockY = 86.0f;
m_clockShadow->setVisible(m_clockShadowEnabled);
m_clockShadow->setFontSize(kClockFontSize);
m_clockShadow->setBold(true);
m_clockShadow->setColor(Color{palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.55f});
m_clockShadow->setText(m_clock->text());
m_clockShadow->setPosition(clockX + 3.0f, clockY - clockMetrics.top + 4.0f);
m_clockShadow->setSize(clockMetrics.width, clockMetrics.bottom - clockMetrics.top);
m_clock->setFontSize(kClockFontSize);
m_clock->setBold(true);
m_clock->setColor(palette.primary);
m_clock->setPosition(clockX, clockY - clockMetrics.top);
m_clock->setSize(clockMetrics.width, clockMetrics.bottom - clockMetrics.top);
m_loginPanel->setPosition(panelX, panelY);
m_loginPanel->setSize(panelWidth, panelHeight);
m_loginPanel->setStyle(RoundedRectStyle{
.fill = Color{palette.surfaceVariant.r, palette.surfaceVariant.g, palette.surfaceVariant.b, 0.88f},
.border = Color{palette.outline.r, palette.outline.g, palette.outline.b, 0.95f},
.fillMode = FillMode::Solid,
.radius = Style::radiusXl,
.softness = 1.0f,
.borderWidth = Style::borderWidth,
});
const float contentLeft = panelX + Style::spaceLg;
const float contentTop = panelY + 22.0f;
const float contentWidth = panelWidth - (Style::spaceLg * 2.0f);
const float buttonWidth = Style::controlHeight;
const float gap = Style::spaceSm;
const float inputWidth = std::max(120.0f, contentWidth - buttonWidth - gap);
m_passwordField->setSize(inputWidth, 0.0f);
m_passwordField->layout(*renderer);
m_passwordField->setPosition(contentLeft, contentTop);
m_loginButton->setSize(buttonWidth, Style::controlHeight);
m_loginButton->layout(*renderer);
m_loginButton->setPosition(contentLeft + inputWidth + gap, contentTop);
m_loginButton->updateInputArea();
m_root.markDirty();
}
void LockSurface::updateCopy() {
m_passwordField->setValue(m_maskedPassword);
updateClockText();
}
void LockSurface::ensureWallpaperTexture() {
auto* renderer = renderContext();
if (renderer == nullptr || !m_wallpaperDirty || !renderTarget().isReady()) {
return;
}
renderer->makeCurrent(renderTarget());
if (m_wallpaperTexture.id != 0) {
renderer->textureManager().unload(m_wallpaperTexture);
}
if (!m_wallpaperPath.empty()) {
m_wallpaperTexture = renderer->textureManager().loadFromFile(m_wallpaperPath);
m_wallpaper->setTextureId(m_wallpaperTexture.id);
m_wallpaper->setTint(Color{1.0f, 1.0f, 1.0f, 1.0f});
} else {
m_wallpaperTexture = {};
m_wallpaper->setTextureId(0);
}
m_wallpaperDirty = false;
}
void LockSurface::updateClockText() {
using clock = std::chrono::system_clock;
const std::time_t t = clock::to_time_t(clock::now());
std::tm local{};
localtime_r(&t, &local);
char buf[16];
std::strftime(buf, sizeof(buf), "%H:%M", &local);
m_clock->setText(buf);
}
+71
View File
@@ -0,0 +1,71 @@
#pragma once
#include "render/core/texture_manager.h"
#include "render/scene/input_dispatcher.h"
#include "render/scene/node.h"
#include "wayland/surface.h"
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
struct ext_session_lock_surface_v1;
struct ext_session_lock_v1;
struct wl_output;
class ImageNode;
class Button;
class Input;
class RectNode;
class TextNode;
struct KeyboardEvent;
struct PointerEvent;
class LockSurface : public Surface {
public:
explicit LockSurface(WaylandConnection& connection);
~LockSurface() override;
using Surface::initialize;
bool initialize() override { return false; }
bool initialize(ext_session_lock_v1* lock, wl_output* output, std::int32_t scale);
void setLockedState(bool locked);
void setPromptState(std::string user, std::string maskedPassword, std::string status, bool error);
void setWallpaperPath(std::string wallpaperPath);
void setOnLogin(std::function<void()> onLogin);
void onPointerEvent(const PointerEvent& event);
void onKeyboardEvent(const KeyboardEvent& event);
[[nodiscard]] wl_output* output() const noexcept { return m_output; }
static void handleConfigure(void* data, ext_session_lock_surface_v1* lockSurface, std::uint32_t serial,
std::uint32_t width, std::uint32_t height);
private:
void ensureWallpaperTexture();
void updateClockText();
void layoutScene(std::uint32_t width, std::uint32_t height);
void updateCopy();
ext_session_lock_surface_v1* m_lockSurface = nullptr;
wl_output* m_output = nullptr;
Node m_root;
ImageNode* m_wallpaper = nullptr;
RectNode* m_backdrop = nullptr;
TextNode* m_clockShadow = nullptr;
TextNode* m_clock = nullptr;
RectNode* m_loginPanel = nullptr;
Input* m_passwordField = nullptr;
Button* m_loginButton = nullptr;
TextureHandle m_wallpaperTexture{};
std::string m_wallpaperPath;
bool m_wallpaperDirty = false;
InputDispatcher m_inputDispatcher;
std::function<void()> m_onLogin;
bool m_locked = false;
std::string m_user;
std::string m_maskedPassword;
std::string m_status;
bool m_error = false;
bool m_clockShadowEnabled = true;
};
+26 -16
View File
@@ -25,7 +25,8 @@ std::vector<std::string> readGtkThemeCandidates() {
// Output is like: 'Copycat-noctalia'\n — strip quotes and whitespace
while (!value.empty() && (value.front() == '\'' || value.front() == '"' || value.front() == ' '))
value = value.substr(1);
while (!value.empty() && (value.back() == '\'' || value.back() == '"' || value.back() == ' ' || value.back() == '\n'))
while (!value.empty() &&
(value.back() == '\'' || value.back() == '"' || value.back() == ' ' || value.back() == '\n'))
value = value.substr(0, value.size() - 1);
if (!value.empty()) {
candidates.emplace_back(value);
@@ -52,8 +53,10 @@ std::vector<std::string> readGtkThemeCandidates() {
continue;
}
std::string_view value(line.data() + eq + 1, line.size() - eq - 1);
while (!value.empty() && value.front() == ' ') value = value.substr(1);
while (!value.empty() && value.back() == ' ') value = value.substr(0, value.size() - 1);
while (!value.empty() && value.front() == ' ')
value = value.substr(1);
while (!value.empty() && value.back() == ' ')
value = value.substr(0, value.size() - 1);
if (!value.empty()) {
candidates.emplace_back(value);
}
@@ -66,8 +69,7 @@ std::vector<std::string> readGtkThemeCandidates() {
// Parse index.theme and return (subdir paths sorted by preference, parent theme names).
// Preference: scalable/large dirs first so we get crisp icons at any size.
std::pair<std::vector<std::string>, std::vector<std::string>>
parseIndexTheme(const std::string& themeRoot) {
std::pair<std::vector<std::string>, std::vector<std::string>> parseIndexTheme(const std::string& themeRoot) {
std::ifstream file(themeRoot + "/index.theme");
if (!file.is_open()) {
return {};
@@ -114,7 +116,8 @@ parseIndexTheme(const std::string& themeRoot) {
dirNames.emplace_back(name);
dirMap[std::string(name)].path = std::string(name);
}
if (comma == std::string_view::npos) break;
if (comma == std::string_view::npos)
break;
start = comma + 1;
}
} else if (key == "Inherits") {
@@ -125,21 +128,29 @@ parseIndexTheme(const std::string& themeRoot) {
if (!name.empty()) {
inherits.emplace_back(name);
}
if (comma == std::string_view::npos) break;
if (comma == std::string_view::npos)
break;
start = comma + 1;
}
}
} else if (!currentSection.empty() && dirMap.count(currentSection)) {
auto& entry = dirMap[currentSection];
if (key == "Size") {
try { entry.size = std::stoi(std::string(value)); } catch (...) {}
try {
entry.size = std::stoi(std::string(value));
} catch (...) {
}
} else if (key == "Type") {
entry.scalable = (value == "Scalable" || value == "Threshold");
} else if (key == "MaxSize") {
// For threshold/scalable dirs, MaxSize gives a better sense of actual size
int maxSize = 0;
try { maxSize = std::stoi(std::string(value)); } catch (...) {}
if (maxSize > entry.size) entry.size = maxSize;
try {
maxSize = std::stoi(std::string(value));
} catch (...) {
}
if (maxSize > entry.size)
entry.size = maxSize;
}
}
}
@@ -148,7 +159,8 @@ parseIndexTheme(const std::string& themeRoot) {
std::stable_sort(dirNames.begin(), dirNames.end(), [&](const std::string& a, const std::string& b) {
const auto& da = dirMap[a];
const auto& db = dirMap[b];
if (da.scalable != db.scalable) return da.scalable > db.scalable;
if (da.scalable != db.scalable)
return da.scalable > db.scalable;
return da.size > db.size;
});
@@ -163,9 +175,7 @@ parseIndexTheme(const std::string& themeRoot) {
} // namespace
IconResolver::IconResolver() {
detectTheme();
}
IconResolver::IconResolver() { detectTheme(); }
void IconResolver::detectTheme() {
const char* home = std::getenv("HOME");
@@ -214,8 +224,8 @@ void IconResolver::buildThemeSearchPaths(const std::string& themeName, std::set<
if (dirs.empty()) {
// No index.theme — fall back to common paths so the theme isn't silently skipped
for (const char* p : {"/scalable/apps/", "/48x48/apps/", "/64x64/apps/",
"/128x128/apps/", "/256x256/apps/", "/32x32/apps/"}) {
for (const char* p :
{"/scalable/apps/", "/48x48/apps/", "/64x64/apps/", "/128x128/apps/", "/256x256/apps/", "/32x32/apps/"}) {
m_searchDirs.push_back(themeRoot + p);
}
} else {
+1 -1
View File
@@ -17,7 +17,7 @@ private:
std::string findIcon(const std::string& name) const;
std::unordered_map<std::string, std::string> m_cache;
std::vector<std::string> m_baseDirs; // ~/.local/share/icons, /usr/share/icons, etc.
std::vector<std::string> m_baseDirs; // ~/.local/share/icons, /usr/share/icons, etc.
std::vector<std::string> m_searchDirs; // Ordered list of concrete dirs to search
std::string m_empty;
};
-3
View File
@@ -23,7 +23,6 @@ inline constexpr float spaceSm = 8.0f;
inline constexpr float spaceMd = 12.0f;
inline constexpr float spaceLg = 16.0f;
inline constexpr float fontSizeCaption = 12.0f;
inline constexpr float fontSizeBody = 14.0f;
inline constexpr float fontSizeTitle = 16.0f;
@@ -32,6 +31,4 @@ inline constexpr float controlHeightSm = 28.0f;
inline constexpr float controlHeight = 32.0f;
inline constexpr float controlHeightLg = 36.0f;
} // namespace Style
+20 -2
View File
@@ -8,6 +8,7 @@
#include <wayland-client.h>
#include "cursor-shape-v1-client-protocol.h"
#include "ext-session-lock-v1-client-protocol.h"
#include "ext-workspace-v1-client-protocol.h"
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
#include "xdg-activation-v1-client-protocol.h"
@@ -23,6 +24,7 @@ constexpr std::uint32_t kXdgOutputManagerVersion = 3;
constexpr std::uint32_t kExtWorkspaceManagerVersion = 1;
constexpr std::uint32_t kCursorShapeManagerVersion = 1;
constexpr std::uint32_t kXdgActivationVersion = 1;
constexpr std::uint32_t kExtSessionLockManagerVersion = 1;
constexpr std::uint32_t kOutputVersion = 4;
const wl_registry_listener kRegistryListener = {
@@ -202,6 +204,7 @@ bool WaylandConnection::hasLayerShell() const noexcept { return m_hasLayerShellG
bool WaylandConnection::hasXdgOutputManager() const noexcept { return m_xdgOutputManager != nullptr; }
bool WaylandConnection::hasExtWorkspaceManager() const noexcept { return m_hasExtWorkspaceGlobal; }
bool WaylandConnection::hasSessionLockManager() const noexcept { return m_sessionLockManager != nullptr; }
bool WaylandConnection::hasXdgActivation() const noexcept { return m_xdgActivation != nullptr; }
std::string WaylandConnection::requestActivationToken(wl_surface* surface) const {
@@ -243,6 +246,8 @@ wl_shm* WaylandConnection::shm() const noexcept { return m_shm; }
zwlr_layer_shell_v1* WaylandConnection::layerShell() const noexcept { return m_layerShell; }
ext_session_lock_manager_v1* WaylandConnection::sessionLockManager() const noexcept { return m_sessionLockManager; }
const std::vector<WaylandOutput>& WaylandConnection::outputs() const noexcept { return m_outputs; }
WaylandOutput* WaylandConnection::findOutputByWl(wl_output* wlOutput) {
@@ -352,6 +357,13 @@ void WaylandConnection::bindGlobal(wl_registry* registry, std::uint32_t name, co
return;
}
if (interfaceName == ext_session_lock_manager_v1_interface.name) {
const auto bindVersion = std::min(version, kExtSessionLockManagerVersion);
m_sessionLockManager = static_cast<ext_session_lock_manager_v1*>(
wl_registry_bind(registry, name, &ext_session_lock_manager_v1_interface, bindVersion));
return;
}
if (interfaceName == wl_output_interface.name) {
const auto bindVersion = std::min(version, kOutputVersion);
auto* output = static_cast<wl_output*>(wl_registry_bind(registry, name, &wl_output_interface, bindVersion));
@@ -402,6 +414,11 @@ void WaylandConnection::cleanup() {
m_xdgActivation = nullptr;
}
if (m_sessionLockManager != nullptr) {
ext_session_lock_manager_v1_destroy(m_sessionLockManager);
m_sessionLockManager = nullptr;
}
if (m_cursorShapeManager != nullptr) {
wp_cursor_shape_manager_v1_destroy(m_cursorShapeManager);
m_cursorShapeManager = nullptr;
@@ -445,9 +462,10 @@ void WaylandConnection::cleanup() {
}
void WaylandConnection::logStartupSummary() const {
kLog.info("connected compositor={} shm={} layer-shell={} xdg-output={} ext-workspace={} outputs={}",
kLog.info("connected compositor={} shm={} layer-shell={} xdg-output={} ext-workspace={} session-lock={} outputs={}",
m_compositor != nullptr ? "yes" : "no", m_shm != nullptr ? "yes" : "no", hasLayerShell() ? "yes" : "no",
hasXdgOutputManager() ? "yes" : "no", hasExtWorkspaceManager() ? "yes" : "no", m_outputs.size());
hasXdgOutputManager() ? "yes" : "no", hasExtWorkspaceManager() ? "yes" : "no",
hasSessionLockManager() ? "yes" : "no", m_outputs.size());
for (const auto& output : m_outputs) {
kLog.info("output {} global={} scale={} mode={}x{} desc=\"{}\"", output.connectorName, output.name, output.scale,
+4
View File
@@ -19,6 +19,7 @@ struct zxdg_output_manager_v1;
struct zxdg_output_v1;
struct wp_cursor_shape_manager_v1;
struct xdg_activation_v1;
struct ext_session_lock_manager_v1;
struct WaylandOutput {
std::uint32_t name = 0;
@@ -68,10 +69,12 @@ public:
[[nodiscard]] bool hasLayerShell() const noexcept;
[[nodiscard]] bool hasXdgOutputManager() const noexcept;
[[nodiscard]] bool hasExtWorkspaceManager() const noexcept;
[[nodiscard]] bool hasSessionLockManager() const noexcept;
[[nodiscard]] wl_display* display() const noexcept;
[[nodiscard]] wl_compositor* compositor() const noexcept;
[[nodiscard]] wl_shm* shm() const noexcept;
[[nodiscard]] zwlr_layer_shell_v1* layerShell() const noexcept;
[[nodiscard]] ext_session_lock_manager_v1* sessionLockManager() const noexcept;
[[nodiscard]] const std::vector<WaylandOutput>& outputs() const noexcept;
[[nodiscard]] WaylandOutput* findOutputByWl(wl_output* wlOutput);
[[nodiscard]] WaylandOutput* findOutputByXdg(zxdg_output_v1* xdgOutput);
@@ -101,6 +104,7 @@ private:
zxdg_output_manager_v1* m_xdgOutputManager = nullptr;
wp_cursor_shape_manager_v1* m_cursorShapeManager = nullptr;
xdg_activation_v1* m_xdgActivation = nullptr;
ext_session_lock_manager_v1* m_sessionLockManager = nullptr;
bool m_hasLayerShellGlobal = false;
bool m_hasExtWorkspaceGlobal = false;
std::vector<WaylandOutput> m_outputs;