mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(lockscreen): add session lock flow with PAM auth
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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,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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user