From 883fbdfe018787d85ee8e8ef9578f65a3b0aa058 Mon Sep 17 00:00:00 2001 From: Maximilian Seidler <78690852+PaideiaDilemma@users.noreply.github.com> Date: Wed, 10 Apr 2024 23:41:31 +0200 Subject: [PATCH] auth: implement a full pam conversation (#205) * auth: implement a full pam conversation * input-field: fixup failedAttempts and color change Credits to @bvr-yr * pam: set default module to hyprland * input-field: backup previous asset * auth: restart auth in onPasswordCheckTimer * auth: immediately switch to waiting when input was submitted * auth: remove redundant waitingForPamAuth * auth: add inputRequested and reschedule submitInput * auth: clear password buffer and handle submitInput before input is requested * Revert "input-field: backup previous asset" This reverts commit 89702945be6af4aa43f54688ad34a4ccba994a3e. Without the backup we avoid rendering the prompt placeholder for one frame when the failText is not available. Looks better this way. * auth: fallback to su if pam_module not in /etc/pam.d rare occasion where a path check even works on nix * auth: rename inputSubmitted and resubmit callback * auth: detach failText from the conversation * fix rebase mistake * auth: make sure prompt and failText are not reset when restarting auth needed for labels * auth: force update timers when the prompt changes * auth: remove unused stuff --- src/config/ConfigManager.cpp | 1 + src/core/Auth.cpp | 166 ++++++++++++++++++++ src/core/Auth.hpp | 54 +++++++ src/core/Password.cpp | 78 --------- src/core/Password.hpp | 18 --- src/core/hyprlock.cpp | 52 ++++-- src/core/hyprlock.hpp | 12 +- src/main.cpp | 5 +- src/renderer/widgets/IWidget.cpp | 8 +- src/renderer/widgets/PasswordInputField.cpp | 117 +++++++------- src/renderer/widgets/PasswordInputField.hpp | 22 +-- 11 files changed, 348 insertions(+), 185 deletions(-) create mode 100644 src/core/Auth.cpp create mode 100644 src/core/Auth.hpp delete mode 100644 src/core/Password.cpp delete mode 100644 src/core/Password.hpp diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 394c003..1de72a4 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -49,6 +49,7 @@ void CConfigManager::init() { m_config.addConfigValue("general:no_fade_in", Hyprlang::INT{0}); m_config.addConfigValue("general:no_fade_out", Hyprlang::INT{0}); m_config.addConfigValue("general:ignore_empty_input", Hyprlang::INT{0}); + m_config.addConfigValue("general:pam_module", Hyprlang::STRING{"hyprlock"}); m_config.addSpecialCategory("background", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); m_config.addSpecialConfigValue("background", "monitor", Hyprlang::STRING{""}); diff --git a/src/core/Auth.cpp b/src/core/Auth.cpp new file mode 100644 index 0000000..ea3066f --- /dev/null +++ b/src/core/Auth.cpp @@ -0,0 +1,166 @@ +#include "Auth.hpp" +#include "hyprlock.hpp" +#include "../helpers/Log.hpp" +#include "src/config/ConfigManager.hpp" + +#include +#include +#include +#include +#if __has_include() +#include +#endif + +#include +#include + +int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) { + const auto CONVERSATIONSTATE = (CAuth::SPamConversationState*)appdata_ptr; + struct pam_response* pamReply = (struct pam_response*)calloc(num_msg, sizeof(struct pam_response)); + bool initialPrompt = true; + + for (int i = 0; i < num_msg; ++i) { + switch (msg[i]->msg_style) { + case PAM_PROMPT_ECHO_OFF: + case PAM_PROMPT_ECHO_ON: { + const auto PROMPT = std::string(msg[i]->msg); + const auto PROMPTCHANGED = PROMPT != CONVERSATIONSTATE->prompt; + Debug::log(LOG, "PAM_PROMPT: {}", PROMPT); + + if (PROMPTCHANGED) + g_pHyprlock->enqueueForceUpdateTimers(); + + // Some pam configurations ask for the password twice for whatever reason (Fedora su for example) + // When the prompt is the same as the last one, I guess our answer can be the same. + if (initialPrompt || PROMPTCHANGED) { + CONVERSATIONSTATE->prompt = PROMPT; + g_pAuth->waitForInput(); + } + + // Needed for unlocks via SIGUSR1 + if (g_pHyprlock->m_bTerminate) + return PAM_CONV_ERR; + + pamReply[i].resp = strdup(CONVERSATIONSTATE->input.c_str()); + initialPrompt = false; + } break; + case PAM_ERROR_MSG: Debug::log(ERR, "PAM: {}", msg[i]->msg); break; + case PAM_TEXT_INFO: Debug::log(LOG, "PAM: {}", msg[i]->msg); break; + } + } + + *resp = pamReply; + return PAM_SUCCESS; +} + +CAuth::CAuth() { + static auto* const PPAMMODULE = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:pam_module")); + m_sPamModule = *PPAMMODULE; + + if (!std::filesystem::exists(std::filesystem::path("/etc/pam.d/") / m_sPamModule)) { + Debug::log(ERR, "Pam module \"{}\" not found! Falling back to \"su\"", m_sPamModule); + m_sPamModule = "su"; + } +} + +static void passwordCheckTimerCallback(std::shared_ptr self, void* data) { + g_pHyprlock->onPasswordCheckTimer(); +} + +void CAuth::start() { + std::thread([this]() { + resetConversation(); + auth(); + + g_pHyprlock->addTimer(std::chrono::milliseconds(1), passwordCheckTimerCallback, nullptr); + }).detach(); +} + +bool CAuth::auth() { + const pam_conv localConv = {conv, (void*)&m_sConversationState}; + pam_handle_t* handle = NULL; + auto uidPassword = getpwuid(getuid()); + + int ret = pam_start(m_sPamModule.c_str(), uidPassword->pw_name, &localConv, &handle); + + if (ret != PAM_SUCCESS) { + m_sConversationState.success = false; + m_sConversationState.failText = "pam_start failed"; + Debug::log(ERR, "auth: pam_start failed for {}", m_sPamModule); + return false; + } + + ret = pam_authenticate(handle, 0); + + m_sConversationState.waitingForPamAuth = false; + + if (ret != PAM_SUCCESS) { + m_sConversationState.success = false; + m_sConversationState.failText = ret == PAM_AUTH_ERR ? "Authentication failed" : "pam_authenticate failed"; + Debug::log(ERR, "auth: {} for {}", m_sConversationState.failText, m_sPamModule); + return false; + } + + ret = pam_end(handle, ret); + + m_sConversationState.success = true; + m_sConversationState.failText = "Successfully authenticated"; + Debug::log(LOG, "auth: authenticated for {}", m_sPamModule); + + return true; +} + +bool CAuth::didAuthSucceed() { + return m_sConversationState.success; +} + +// clearing the input must be done from the main thread +static void clearInputTimerCallback(std::shared_ptr self, void* data) { + g_pHyprlock->clearPasswordBuffer(); +} + +void CAuth::waitForInput() { + g_pHyprlock->addTimer(std::chrono::milliseconds(1), clearInputTimerCallback, nullptr); + + std::unique_lock lk(m_sConversationState.inputMutex); + m_bBlockInput = false; + m_sConversationState.waitingForPamAuth = false; + m_sConversationState.inputRequested = true; + m_sConversationState.inputSubmittedCondition.wait(lk, [this] { return !m_sConversationState.inputRequested || g_pHyprlock->m_bTerminate; }); + m_bBlockInput = true; +} + +void CAuth::submitInput(std::string input) { + std::unique_lock lk(m_sConversationState.inputMutex); + + if (!m_sConversationState.inputRequested) + Debug::log(ERR, "SubmitInput called, but the auth thread is not waiting for input!"); + + m_sConversationState.input = input; + m_sConversationState.inputRequested = false; + m_sConversationState.waitingForPamAuth = true; + m_sConversationState.inputSubmittedCondition.notify_all(); +} + +std::optional CAuth::getLastFailText() { + return m_sConversationState.failText.empty() ? std::nullopt : std::optional(m_sConversationState.failText); +} + +std::optional CAuth::getLastPrompt() { + return m_sConversationState.prompt.empty() ? std::nullopt : std::optional(m_sConversationState.prompt); +} + +bool CAuth::checkWaiting() { + return m_bBlockInput || m_sConversationState.waitingForPamAuth; +} + +void CAuth::terminate() { + m_sConversationState.inputSubmittedCondition.notify_all(); +} + +void CAuth::resetConversation() { + m_sConversationState.input = ""; + m_sConversationState.waitingForPamAuth = false; + m_sConversationState.inputRequested = false; + m_sConversationState.success = false; +} diff --git a/src/core/Auth.hpp b/src/core/Auth.hpp new file mode 100644 index 0000000..b0fea79 --- /dev/null +++ b/src/core/Auth.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include + +class CAuth { + public: + struct SPamConversationState { + std::string input = ""; + std::string prompt = ""; + std::string failText = ""; + + std::mutex inputMutex; + std::condition_variable inputSubmittedCondition; + + bool waitingForPamAuth = false; + bool inputRequested = false; + + bool success = false; + }; + + CAuth(); + + void start(); + bool auth(); + bool didAuthSucceed(); + + void waitForInput(); + void submitInput(std::string input); + + std::optional getLastFailText(); + std::optional getLastPrompt(); + + bool checkWaiting(); + + void terminate(); + + // Should only be set via the main thread + bool m_bDisplayFailText = false; + + private: + SPamConversationState m_sConversationState; + + bool m_bBlockInput = true; + + std::string m_sPamModule; + + void resetConversation(); +}; + +inline std::unique_ptr g_pAuth; diff --git a/src/core/Password.cpp b/src/core/Password.cpp deleted file mode 100644 index f1c3653..0000000 --- a/src/core/Password.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "Password.hpp" -#include "hyprlock.hpp" -#include "../helpers/Log.hpp" - -#include -#include -#include -#if __has_include() -#include -#endif - -#include -#include - -// -int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) { - const char* pass = static_cast(appdata_ptr); - struct pam_response* pam_reply = static_cast(calloc(num_msg, sizeof(struct pam_response))); - - for (int i = 0; i < num_msg; ++i) { - switch (msg[i]->msg_style) { - case PAM_PROMPT_ECHO_OFF: - case PAM_PROMPT_ECHO_ON: pam_reply[i].resp = strdup(pass); break; - case PAM_ERROR_MSG: Debug::log(ERR, "PAM: {}", msg[i]->msg); break; - case PAM_TEXT_INFO: Debug::log(LOG, "PAM: {}", msg[i]->msg); break; - } - } - *resp = pam_reply; - return PAM_SUCCESS; -} - -static void passwordCheckTimerCallback(std::shared_ptr self, void* data) { - g_pHyprlock->onPasswordCheckTimer(); -} - -std::shared_ptr CPassword::verify(const std::string& pass) { - - std::shared_ptr result = std::make_shared(false); - - std::thread([this, result, pass]() { - auto auth = [&](std::string auth) -> bool { - const pam_conv localConv = {conv, (void*)pass.c_str()}; - pam_handle_t* handle = NULL; - auto uidPassword = getpwuid(getuid()); - - int ret = pam_start(auth.c_str(), uidPassword->pw_name, &localConv, &handle); - - if (ret != PAM_SUCCESS) { - result->success = false; - result->failReason = "pam_start failed"; - Debug::log(ERR, "auth: pam_start failed for {}", auth); - return false; - } - - ret = pam_authenticate(handle, 0); - - if (ret != PAM_SUCCESS) { - result->success = false; - result->failReason = ret == PAM_AUTH_ERR ? "Authentication failed" : "pam_authenticate failed"; - Debug::log(ERR, "auth: {} for {}", result->failReason, auth); - return false; - } - - ret = pam_end(handle, ret); - - result->success = true; - result->failReason = "Successfully authenticated"; - Debug::log(LOG, "auth: authenticated for {}", auth); - - return true; - }; - - result->realized = auth("hyprlock") || auth("su") || true; - g_pHyprlock->addTimer(std::chrono::milliseconds(1), passwordCheckTimerCallback, nullptr); - }).detach(); - - return result; -} diff --git a/src/core/Password.hpp b/src/core/Password.hpp deleted file mode 100644 index 74fcbac..0000000 --- a/src/core/Password.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include -#include - -class CPassword { - public: - struct SVerificationResult { - std::atomic realized = false; - bool success = false; - std::string failReason = ""; - }; - - std::shared_ptr verify(const std::string& pass); -}; - -inline std::unique_ptr g_pPassword = std::make_unique(); \ No newline at end of file diff --git a/src/core/hyprlock.cpp b/src/core/hyprlock.cpp index 47d7f4d..61485ec 100644 --- a/src/core/hyprlock.cpp +++ b/src/core/hyprlock.cpp @@ -2,7 +2,7 @@ #include "../helpers/Log.hpp" #include "../config/ConfigManager.hpp" #include "../renderer/Renderer.hpp" -#include "Password.hpp" +#include "Auth.hpp" #include "Egl.hpp" #include @@ -381,6 +381,9 @@ void CHyprlock::run() { acquireSessionLock(); + g_pAuth = std::make_unique(); + g_pAuth->start(); + registerSignalAction(SIGUSR1, handleUnlockSignal, SA_RESTART); registerSignalAction(SIGUSR2, handleForceUpdateSignal); registerSignalAction(SIGRTMIN, handlePollTerminate); @@ -533,6 +536,8 @@ void CHyprlock::run() { pthread_kill(pollThr.native_handle(), SIGRTMIN); + g_pAuth->terminate(); + // wait for threads to exit cleanly to avoid a coredump pollThr.join(); timersThr.join(); @@ -737,32 +742,44 @@ static const ext_session_lock_v1_listener sessionLockListener = { // end session_lock +static void displayFailTextTimerCallback(std::shared_ptr self, void* data) { + g_pAuth->m_bDisplayFailText = false; + + for (auto& o : g_pHyprlock->m_vOutputs) { + o->sessionLockSurface->render(); + } +} + void CHyprlock::onPasswordCheckTimer() { // check result - if (m_sPasswordState.result->success) { + if (g_pAuth->didAuthSucceed()) { unlock(); } else { - Debug::log(LOG, "Authentication failed: {}", m_sPasswordState.result->failReason); - m_sPasswordState.lastFailReason = m_sPasswordState.result->failReason; - m_sPasswordState.passBuffer = ""; - m_sPasswordState.failedAttempts += 1; Debug::log(LOG, "Failed attempts: {}", m_sPasswordState.failedAttempts); + + m_sPasswordState.passBuffer = ""; + m_sPasswordState.failedAttempts += 1; + g_pAuth->m_bDisplayFailText = true; forceUpdateTimers(); + g_pHyprlock->addTimer(/* controls error message duration */ std::chrono::seconds(1), displayFailTextTimerCallback, nullptr); + + g_pAuth->start(); + for (auto& o : m_vOutputs) { o->sessionLockSurface->render(); } } - - m_sPasswordState.result.reset(); } -bool CHyprlock::passwordCheckWaiting() { - return m_sPasswordState.result.get(); -} +void CHyprlock::clearPasswordBuffer() { + if (m_sPasswordState.passBuffer.empty()) + return; -std::optional CHyprlock::passwordLastFailReason() { - return m_sPasswordState.lastFailReason; + m_sPasswordState.passBuffer = ""; + for (auto& o : m_vOutputs) { + o->sessionLockSurface->render(); + } } void CHyprlock::renderOutput(const std::string& stringPort) { @@ -798,7 +815,7 @@ void CHyprlock::onKey(uint32_t key, bool down) { else std::erase(m_vPressedKeys, key); - if (m_sPasswordState.result) { + if (g_pAuth->checkWaiting()) { for (auto& o : m_vOutputs) { o->sessionLockSurface->render(); } @@ -826,7 +843,7 @@ void CHyprlock::onKey(uint32_t key, bool down) { return; } - m_sPasswordState.result = g_pPassword->verify(m_sPasswordState.passBuffer); + g_pAuth->submitInput(m_sPasswordState.passBuffer); } else if (SYM == XKB_KEY_BackSpace) { if (m_sPasswordState.passBuffer.length() > 0) { // handle utf-8 @@ -941,6 +958,11 @@ std::vector> CHyprlock::getTimers() { return m_vTimers; } +void CHyprlock::enqueueForceUpdateTimers() { + addTimer( + std::chrono::milliseconds(1), [](std::shared_ptr self, void* data) { forceUpdateTimers(); }, nullptr, false); +} + void CHyprlock::spawnAsync(const std::string& args) { Debug::log(LOG, "Executing (async) {}", args); diff --git a/src/core/hyprlock.hpp b/src/core/hyprlock.hpp index cb7c92c..a24df22 100644 --- a/src/core/hyprlock.hpp +++ b/src/core/hyprlock.hpp @@ -9,7 +9,7 @@ #include "Output.hpp" #include "CursorShape.hpp" #include "Timer.hpp" -#include "Password.hpp" +#include "Auth.hpp" #include #include @@ -40,6 +40,8 @@ class CHyprlock { std::shared_ptr addTimer(const std::chrono::system_clock::duration& timeout, std::function self, void* data)> cb_, void* data, bool force = false); + void enqueueForceUpdateTimers(); + void onLockLocked(); void onLockFinished(); @@ -53,6 +55,7 @@ class CHyprlock { void onKey(uint32_t key, bool down); void onPasswordCheckTimer(); + void clearPasswordBuffer(); bool passwordCheckWaiting(); std::optional passwordLastFailReason(); @@ -129,10 +132,9 @@ class CHyprlock { } m_sLockState; struct { - std::string passBuffer = ""; - std::shared_ptr result; - std::optional lastFailReason; - size_t failedAttempts = 0; + std::string passBuffer = ""; + size_t failedAttempts = 0; + bool displayFailText = false; } m_sPasswordState; struct { diff --git a/src/main.cpp b/src/main.cpp index cf47ac0..8bc156f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,7 +15,7 @@ void help() { int main(int argc, char** argv, char** envp) { std::string configPath; std::string wlDisplay; - bool immediate = false; + bool immediate = false; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; @@ -32,8 +32,7 @@ int main(int argc, char** argv, char** envp) { else if (arg == "--display" && i + 1 < argc) { wlDisplay = argv[i + 1]; i++; - } - else if (arg == "--immediate") { + } else if (arg == "--immediate") { immediate = true; } else if (arg == "--help" || arg == "-h") { help(); diff --git a/src/renderer/widgets/IWidget.cpp b/src/renderer/widgets/IWidget.cpp index 24f9994..b1879fc 100644 --- a/src/renderer/widgets/IWidget.cpp +++ b/src/renderer/widgets/IWidget.cpp @@ -131,11 +131,17 @@ IWidget::SFormatResult IWidget::formatString(std::string in) { } if (in.contains("$FAIL")) { - const auto FAIL = g_pHyprlock->passwordLastFailReason(); + const auto FAIL = g_pAuth->getLastFailText(); replaceAll(in, "$FAIL", FAIL.has_value() ? FAIL.value() : ""); result.allowForceUpdate = true; } + if (in.contains("$PROMPT")) { + const auto PROMPT = g_pAuth->getLastPrompt(); + replaceAll(in, "$PROMPT", PROMPT.has_value() ? PROMPT.value() : ""); + result.allowForceUpdate = true; + } + if (in.contains("$ATTEMPTS")) { replaceAllAttempts(in); result.allowForceUpdate = true; diff --git a/src/renderer/widgets/PasswordInputField.cpp b/src/renderer/widgets/PasswordInputField.cpp index 202bc8e..4feb778 100644 --- a/src/renderer/widgets/PasswordInputField.cpp +++ b/src/renderer/widgets/PasswordInputField.cpp @@ -1,8 +1,19 @@ #include "PasswordInputField.hpp" #include "../Renderer.hpp" #include "../../core/hyprlock.hpp" +#include "src/core/Auth.hpp" #include +static void replaceAll(std::string& str, const std::string& from, const std::string& to) { + if (from.empty()) + return; + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.length(), to); + pos += to.length(); + } +} + CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::unordered_map& props, const std::string& output) : outputStringPort(output), shadow(this, props, viewport_) { size = std::any_cast(props.at("size")); @@ -15,6 +26,7 @@ CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::u fadeTimeoutMs = std::any_cast(props.at("fade_timeout")); hiddenInputState.enabled = std::any_cast(props.at("hide_input")); rounding = std::any_cast(props.at("rounding")); + configPlaceholderText = std::any_cast(props.at("placeholder_text")); configFailText = std::any_cast(props.at("fail_text")); col.transitionMs = std::any_cast(props.at("fail_transition")); col.outer = std::any_cast(props.at("outer_color")); @@ -48,15 +60,17 @@ CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::u g_pHyprlock->m_bNumLock = col.invertNum; - std::string placeholderText = std::any_cast(props.at("placeholder_text")); - // Render placeholder if either placeholder_text or fail_text are non-empty // as placeholder must be rendered to show fail_text - if (!placeholderText.empty() || !configFailText.empty()) { - placeholder.resourceID = "placeholder:" + std::to_string((uintptr_t)this); + if (!configPlaceholderText.empty() || !configFailText.empty()) { + placeholder.currentText = configPlaceholderText; + + replaceAll(placeholder.currentText, "$PROMPT", ""); + + placeholder.resourceID = "placeholder:" + placeholder.currentText + std::to_string((uintptr_t)this); CAsyncResourceGatherer::SPreloadRequest request; request.id = placeholder.resourceID; - request.asset = placeholderText; + request.asset = placeholder.currentText; request.type = CAsyncResourceGatherer::eTargetType::TARGET_TEXT; request.props["font_family"] = std::string{"Sans"}; request.props["color"] = CColor{1.0 - col.font.r, 1.0 - col.font.g, 1.0 - col.font.b, 0.5}; @@ -65,16 +79,6 @@ CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::u } } -static void replaceAllFail(std::string& str, const std::string& from, const std::string& to) { - if (from.empty()) - return; - size_t pos = 0; - while ((pos = str.find(from, pos)) != std::string::npos) { - str.replace(pos, from.length(), to); - pos += to.length(); - } -} - static void fadeOutCallback(std::shared_ptr self, void* data) { CPasswordInputField* p = (CPasswordInputField*)data; @@ -173,23 +177,19 @@ bool CPasswordInputField::draw(const SRenderData& data) { bool forceReload = false; - if (passwordLength == 0 && g_pHyprlock->getPasswordFailedAttempts() > failedAttempts) - forceReload = true; - - failedAttempts = g_pHyprlock->getPasswordFailedAttempts(); passwordLength = g_pHyprlock->getPasswordBufferDisplayLen(); - checkWaiting = g_pHyprlock->passwordCheckWaiting(); + checkWaiting = g_pAuth->checkWaiting(); updateFade(); updateDots(); - updateFailTex(); + updatePlaceholder(); updateColors(); updateHiddenInputState(); static auto TIMER = std::chrono::system_clock::now(); - if (placeholder.failAsset) { - const auto TARGETSIZEX = placeholder.failAsset->texture.m_vSize.x + inputFieldBox.h; + if (placeholder.asset) { + const auto TARGETSIZEX = placeholder.asset->texture.m_vSize.x + inputFieldBox.h; if (size.x < TARGETSIZEX) { const auto DELTA = std::clamp((int)std::chrono::duration_cast(std::chrono::system_clock::now() - TIMER).count(), 8000, 20000); @@ -287,17 +287,10 @@ bool CPasswordInputField::draw(const SRenderData& data) { if (passwordLength == 0 && !placeholder.resourceID.empty()) { SPreloadedAsset* currAsset = nullptr; - if (!placeholder.failID.empty()) { - if (!placeholder.failAsset) - placeholder.failAsset = g_pRenderer->asyncResourceGatherer->getAssetByID(placeholder.failID); + if (!placeholder.asset) + placeholder.asset = g_pRenderer->asyncResourceGatherer->getAssetByID(placeholder.resourceID); - currAsset = placeholder.failAsset; - } else { - if (!placeholder.asset) - placeholder.asset = g_pRenderer->asyncResourceGatherer->getAssetByID(placeholder.resourceID); - - currAsset = placeholder.asset; - } + currAsset = placeholder.asset; if (currAsset) { Vector2D pos = outerBox.pos() + outerBox.size() / 2.f; @@ -311,41 +304,53 @@ bool CPasswordInputField::draw(const SRenderData& data) { return dots.currentAmount != passwordLength || fade.animated || col.animated || redrawShadow || data.opacity < 1.0 || forceReload; } -void CPasswordInputField::updateFailTex() { - const auto FAIL = g_pHyprlock->passwordLastFailReason(); - - if (checkWaiting) - placeholder.canGetNewFail = true; - - if (passwordLength != 0 || (checkWaiting && passwordLength == 0)) { - if (placeholder.failAsset) { - g_pRenderer->asyncResourceGatherer->unloadAsset(placeholder.failAsset); - placeholder.failAsset = nullptr; - placeholder.failID = ""; - redrawShadow = true; +void CPasswordInputField::updatePlaceholder() { + if (passwordLength != 0) { + if (placeholder.asset && /* keep prompt asset cause it is likely to be used again */ placeholder.isFailText) { + std::erase(placeholder.registeredResourceIDs, placeholder.resourceID); + g_pRenderer->asyncResourceGatherer->unloadAsset(placeholder.asset); + placeholder.asset = nullptr; + placeholder.resourceID = ""; + redrawShadow = true; } return; } - if (!FAIL.has_value() || !placeholder.canGetNewFail) + const auto AUTHFEEDBACK = g_pAuth->m_bDisplayFailText ? g_pAuth->getLastFailText().value_or("Ups, no fail text?") : g_pAuth->getLastPrompt().value_or("Ups, no prompt?"); + + if (placeholder.lastAuthFeedback == AUTHFEEDBACK && g_pHyprlock->getPasswordFailedAttempts() == placeholder.failedAttempts) return; - placeholder.failText = configFailText; - replaceAllFail(placeholder.failText, "$FAIL", FAIL.value()); - replaceAllFail(placeholder.failText, "$ATTEMPTS", std::to_string(failedAttempts)); + placeholder.failedAttempts = g_pHyprlock->getPasswordFailedAttempts(); + placeholder.isFailText = g_pAuth->m_bDisplayFailText; + placeholder.lastAuthFeedback = AUTHFEEDBACK; + + placeholder.asset = nullptr; + + if (placeholder.isFailText) { + placeholder.currentText = configFailText; + replaceAll(placeholder.currentText, "$FAIL", AUTHFEEDBACK); + replaceAll(placeholder.currentText, "$ATTEMPTS", std::to_string(placeholder.failedAttempts)); + } else { + placeholder.currentText = configPlaceholderText; + replaceAll(placeholder.currentText, "$PROMPT", AUTHFEEDBACK); + } + + placeholder.resourceID = "placeholder:" + placeholder.currentText + std::to_string((uintptr_t)this); + if (std::find(placeholder.registeredResourceIDs.begin(), placeholder.registeredResourceIDs.end(), placeholder.resourceID) != placeholder.registeredResourceIDs.end()) + return; + + placeholder.registeredResourceIDs.push_back(placeholder.resourceID); // query CAsyncResourceGatherer::SPreloadRequest request; - request.id = "input-error:" + std::to_string((uintptr_t)this) + ",time:" + std::to_string(time(nullptr)); - placeholder.failID = request.id; - request.asset = placeholder.failText; + request.id = placeholder.resourceID; + request.asset = placeholder.currentText; request.type = CAsyncResourceGatherer::eTargetType::TARGET_TEXT; request.props["font_family"] = std::string{"Sans"}; - request.props["color"] = col.fail; + request.props["color"] = (placeholder.isFailText) ? col.fail : col.font; request.props["font_size"] = (int)size.y / 4; g_pRenderer->asyncResourceGatherer->requestAsyncAssetPreload(request); - - placeholder.canGetNewFail = false; } void CPasswordInputField::updateHiddenInputState() { @@ -425,7 +430,7 @@ void CPasswordInputField::updateColors() { col.stateNum = col.invertNum ? !g_pHyprlock->m_bNumLock : g_pHyprlock->m_bNumLock; col.stateCaps = g_pHyprlock->m_bCapsLock; - if (placeholder.failID.empty()) { + if (!placeholder.isFailText || passwordLength > 0 || (passwordLength == 0 && checkWaiting)) { if (g_pHyprlock->m_bFadeStarted) { if (TARGET == col.check) SOURCE = BORDERLESS ? col.inner : col.outer; diff --git a/src/renderer/widgets/PasswordInputField.hpp b/src/renderer/widgets/PasswordInputField.hpp index e78a601..a85f0a2 100644 --- a/src/renderer/widgets/PasswordInputField.hpp +++ b/src/renderer/widgets/PasswordInputField.hpp @@ -22,7 +22,7 @@ class CPasswordInputField : public IWidget { private: void updateDots(); void updateFade(); - void updateFailTex(); + void updatePlaceholder(); void updateHiddenInputState(); void updateColors(); @@ -31,7 +31,6 @@ class CPasswordInputField : public IWidget { bool checkWaiting = false; size_t passwordLength = 0; - size_t failedAttempts = 0; Vector2D size; Vector2D pos; @@ -39,7 +38,7 @@ class CPasswordInputField : public IWidget { Vector2D configPos; Vector2D configSize; - std::string halign, valign, configFailText, outputStringPort; + std::string halign, valign, configFailText, outputStringPort, configPlaceholderText; int outThick, rounding; @@ -63,13 +62,18 @@ class CPasswordInputField : public IWidget { } fade; struct { - std::string resourceID = ""; - SPreloadedAsset* asset = nullptr; + std::string resourceID = ""; + SPreloadedAsset* asset = nullptr; + + std::string currentText = ""; + size_t failedAttempts = 0; + bool canGetNewText = true; + bool isFailText = false; + + std::string lastAuthFeedback; + + std::vector registeredResourceIDs; - std::string failID = ""; - SPreloadedAsset* failAsset = nullptr; - bool canGetNewFail = true; - std::string failText = ""; } placeholder; struct {