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
This commit is contained in:
Maximilian Seidler 2024-04-10 23:41:31 +02:00 committed by GitHub
parent eb1123fa2e
commit 883fbdfe01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 348 additions and 185 deletions

View File

@ -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{""});

166
src/core/Auth.cpp Normal file
View File

@ -0,0 +1,166 @@
#include "Auth.hpp"
#include "hyprlock.hpp"
#include "../helpers/Log.hpp"
#include "src/config/ConfigManager.hpp"
#include <filesystem>
#include <unistd.h>
#include <pwd.h>
#include <security/pam_appl.h>
#if __has_include(<security/pam_misc.h>)
#include <security/pam_misc.h>
#endif
#include <cstring>
#include <thread>
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<CTimer> 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<CTimer> self, void* data) {
g_pHyprlock->clearPasswordBuffer();
}
void CAuth::waitForInput() {
g_pHyprlock->addTimer(std::chrono::milliseconds(1), clearInputTimerCallback, nullptr);
std::unique_lock<std::mutex> 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<std::mutex> 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<std::string> CAuth::getLastFailText() {
return m_sConversationState.failText.empty() ? std::nullopt : std::optional(m_sConversationState.failText);
}
std::optional<std::string> 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;
}

54
src/core/Auth.hpp Normal file
View File

@ -0,0 +1,54 @@
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <mutex>
#include <condition_variable>
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<std::string> getLastFailText();
std::optional<std::string> 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<CAuth> g_pAuth;

View File

@ -1,78 +0,0 @@
#include "Password.hpp"
#include "hyprlock.hpp"
#include "../helpers/Log.hpp"
#include <unistd.h>
#include <pwd.h>
#include <security/pam_appl.h>
#if __has_include(<security/pam_misc.h>)
#include <security/pam_misc.h>
#endif
#include <cstring>
#include <thread>
//
int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) {
const char* pass = static_cast<const char*>(appdata_ptr);
struct pam_response* pam_reply = static_cast<struct pam_response*>(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<CTimer> self, void* data) {
g_pHyprlock->onPasswordCheckTimer();
}
std::shared_ptr<CPassword::SVerificationResult> CPassword::verify(const std::string& pass) {
std::shared_ptr<CPassword::SVerificationResult> result = std::make_shared<CPassword::SVerificationResult>(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;
}

View File

@ -1,18 +0,0 @@
#pragma once
#include <memory>
#include <string>
#include <atomic>
class CPassword {
public:
struct SVerificationResult {
std::atomic<bool> realized = false;
bool success = false;
std::string failReason = "";
};
std::shared_ptr<SVerificationResult> verify(const std::string& pass);
};
inline std::unique_ptr<CPassword> g_pPassword = std::make_unique<CPassword>();

View File

@ -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 <sys/wait.h>
@ -381,6 +381,9 @@ void CHyprlock::run() {
acquireSessionLock();
g_pAuth = std::make_unique<CAuth>();
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<CTimer> 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<std::string> 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<std::shared_ptr<CTimer>> CHyprlock::getTimers() {
return m_vTimers;
}
void CHyprlock::enqueueForceUpdateTimers() {
addTimer(
std::chrono::milliseconds(1), [](std::shared_ptr<CTimer> self, void* data) { forceUpdateTimers(); }, nullptr, false);
}
void CHyprlock::spawnAsync(const std::string& args) {
Debug::log(LOG, "Executing (async) {}", args);

View File

@ -9,7 +9,7 @@
#include "Output.hpp"
#include "CursorShape.hpp"
#include "Timer.hpp"
#include "Password.hpp"
#include "Auth.hpp"
#include <memory>
#include <vector>
@ -40,6 +40,8 @@ class CHyprlock {
std::shared_ptr<CTimer> addTimer(const std::chrono::system_clock::duration& timeout, std::function<void(std::shared_ptr<CTimer> 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<std::string> passwordLastFailReason();
@ -129,10 +132,9 @@ class CHyprlock {
} m_sLockState;
struct {
std::string passBuffer = "";
std::shared_ptr<CPassword::SVerificationResult> result;
std::optional<std::string> lastFailReason;
size_t failedAttempts = 0;
std::string passBuffer = "";
size_t failedAttempts = 0;
bool displayFailText = false;
} m_sPasswordState;
struct {

View File

@ -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();

View File

@ -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;

View File

@ -1,8 +1,19 @@
#include "PasswordInputField.hpp"
#include "../Renderer.hpp"
#include "../../core/hyprlock.hpp"
#include "src/core/Auth.hpp"
#include <algorithm>
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<std::string, std::any>& props, const std::string& output) :
outputStringPort(output), shadow(this, props, viewport_) {
size = std::any_cast<Hyprlang::VEC2>(props.at("size"));
@ -15,6 +26,7 @@ CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::u
fadeTimeoutMs = std::any_cast<Hyprlang::INT>(props.at("fade_timeout"));
hiddenInputState.enabled = std::any_cast<Hyprlang::INT>(props.at("hide_input"));
rounding = std::any_cast<Hyprlang::INT>(props.at("rounding"));
configPlaceholderText = std::any_cast<Hyprlang::STRING>(props.at("placeholder_text"));
configFailText = std::any_cast<Hyprlang::STRING>(props.at("fail_text"));
col.transitionMs = std::any_cast<Hyprlang::INT>(props.at("fail_transition"));
col.outer = std::any_cast<Hyprlang::INT>(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<Hyprlang::STRING>(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<CTimer> 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::microseconds>(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;

View File

@ -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<std::string> registeredResourceIDs;
std::string failID = "";
SPreloadedAsset* failAsset = nullptr;
bool canGetNewFail = true;
std::string failText = "";
} placeholder;
struct {