From e24372277c1b2d30ff405c6566701ad6a62f03b5 Mon Sep 17 00:00:00 2001 From: Martin Schrodt Date: Thu, 2 Apr 2026 11:34:36 +0300 Subject: [PATCH] core: add per-listener condition_cmd with retry Add condition_cmd and condition_retry config options per listener. When set, condition_cmd is executed before on-timeout fires. Exit 0 proceeds normally, non-zero defers on-timeout and retries every condition_retry seconds while the user remains idle. User activity cancels any pending retry. This allows gating actions (e.g. suspend) on external conditions like active SSH sessions or running workloads without external inhibitor daemons. --- src/config/ConfigManager.cpp | 8 +++- src/config/ConfigManager.hpp | 10 +++-- src/core/Hypridle.cpp | 76 ++++++++++++++++++++++++++++++++++-- src/core/Hypridle.hpp | 14 ++++--- 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index a5c0a3a..372a524 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -39,6 +39,8 @@ void CConfigManager::init() { m_config.addSpecialConfigValue("listener", "on-timeout", Hyprlang::STRING{""}); m_config.addSpecialConfigValue("listener", "on-resume", Hyprlang::STRING{""}); m_config.addSpecialConfigValue("listener", "ignore_inhibit", Hyprlang::INT{0}); + m_config.addSpecialConfigValue("listener", "condition_cmd", Hyprlang::STRING{""}); + m_config.addSpecialConfigValue("listener", "condition_retry", Hyprlang::INT{30}); m_config.addConfigValue("general:lock_cmd", Hyprlang::STRING{""}); m_config.addConfigValue("general:unlock_cmd", Hyprlang::STRING{""}); @@ -88,6 +90,8 @@ Hyprlang::CParseResult CConfigManager::postParse() { rule.onResume = std::any_cast(m_config.getSpecialConfigValue("listener", "on-resume", k.c_str())); rule.ignoreInhibit = std::any_cast(m_config.getSpecialConfigValue("listener", "ignore_inhibit", k.c_str())); + rule.conditionCmd = std::any_cast(m_config.getSpecialConfigValue("listener", "condition_cmd", k.c_str())); + rule.conditionRetry = std::any_cast(m_config.getSpecialConfigValue("listener", "condition_retry", k.c_str())); if (timeout == -1) { result.setError("Category has a missing timeout setting"); @@ -98,8 +102,8 @@ Hyprlang::CParseResult CConfigManager::postParse() { } for (auto& r : m_vRules) { - Debug::log(LOG, "Registered timeout rule for {}s:\n on-timeout: {}\n on-resume: {}\n ignore_inhibit: {}", r.timeout, r.onTimeout, r.onResume, - r.ignoreInhibit); + Debug::log(LOG, "Registered timeout rule for {}s:\n on-timeout: {}\n on-resume: {}\n ignore_inhibit: {}\n condition_cmd: {}\n condition_retry: {}", + r.timeout, r.onTimeout, r.onResume, r.ignoreInhibit, r.conditionCmd, r.conditionRetry); } return result; diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp index b4f4636..786463f 100644 --- a/src/config/ConfigManager.hpp +++ b/src/config/ConfigManager.hpp @@ -14,10 +14,12 @@ class CConfigManager { void init(); struct STimeoutRule { - uint64_t timeout = 0; - std::string onTimeout = ""; - std::string onResume = ""; - bool ignoreInhibit = false; + uint64_t timeout = 0; + std::string onTimeout = ""; + std::string onResume = ""; + bool ignoreInhibit = false; + std::string conditionCmd = ""; + int64_t conditionRetry = 30; }; std::vector getRules(); diff --git a/src/core/Hypridle.cpp b/src/core/Hypridle.cpp index 3519a9d..9d528f9 100644 --- a/src/core/Hypridle.cpp +++ b/src/core/Hypridle.cpp @@ -65,9 +65,11 @@ void CHypridle::run() { for (size_t i = 0; i < RULES.size(); ++i) { auto& l = m_sWaylandIdleState.listeners[i]; const auto& r = RULES[i]; - l.onRestore = r.onResume; - l.onTimeout = r.onTimeout; - l.ignoreInhibit = r.ignoreInhibit; + l.onRestore = r.onResume; + l.onTimeout = r.onTimeout; + l.ignoreInhibit = r.ignoreInhibit; + l.conditionCmd = r.conditionCmd; + l.conditionRetry = r.conditionRetry; if (*IGNOREWAYLANDINHIBIT || r.ignoreInhibit) l.notification = @@ -142,6 +144,45 @@ void CHypridle::run() { enterEventLoop(); } +static int64_t nowMonotonic() { + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +static bool runConditionCmd(const std::string& cmd) { + Debug::log(LOG, "Running condition_cmd: {}", cmd); + + pid_t pid = fork(); + if (pid < 0) { + Debug::log(ERR, "Failed to fork for condition_cmd"); + return false; + } + + if (pid == 0) { + execl("/bin/sh", "/bin/sh", "-c", cmd.c_str(), nullptr); + _exit(127); + } + + // Wait with 5s timeout + for (int i = 0; i < 50; i++) { + int status = 0; + pid_t ret = waitpid(pid, &status, WNOHANG); + if (ret > 0) { + int exitCode = WIFEXITED(status) ? WEXITSTATUS(status) : 1; + Debug::log(LOG, "condition_cmd exited with {}", exitCode); + return exitCode == 0; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Timeout — kill and return false + Debug::log(WARN, "condition_cmd timed out after 5s, killing"); + kill(pid, SIGKILL); + waitpid(pid, nullptr, 0); + return false; +} + +static void spawn(const std::string& args); + void CHypridle::enterEventLoop() { nfds_t pollfdsCount = m_sDBUSState.screenSaverServiceConnection ? 3 : 2; @@ -238,6 +279,24 @@ void CHypridle::enterEventLoop() { ret = wl_display_dispatch_pending(m_sWaylandState.display); wl_display_flush(m_sWaylandState.display); } while (ret > 0); + + // Check condition_cmd retries for pending listeners + const auto now = nowMonotonic(); + for (auto& l : m_sWaylandIdleState.listeners) { + if (!l.conditionPending || now < l.conditionRetryAt) + continue; + + Debug::log(LOG, "Retrying condition_cmd for rule {:x}", (uintptr_t)&l); + if (runConditionCmd(l.conditionCmd)) { + l.conditionPending = false; + l.onTimeoutFired = true; + Debug::log(LOG, "Condition met, running {}", l.onTimeout); + spawn(l.onTimeout); + } else { + l.conditionRetryAt = now + l.conditionRetry; + Debug::log(LOG, "Condition still not met, retrying in {}s", l.conditionRetry); + } + } } Debug::log(ERR, "[core] Terminated"); @@ -268,6 +327,16 @@ void CHypridle::onIdled(SIdleListener* pListener) { return; } + // Check condition_cmd before firing on-timeout + if (!pListener->conditionCmd.empty()) { + if (!runConditionCmd(pListener->conditionCmd)) { + Debug::log(LOG, "condition_cmd blocked on-timeout, retrying in {}s", pListener->conditionRetry); + pListener->conditionPending = true; + pListener->conditionRetryAt = nowMonotonic() + pListener->conditionRetry; + return; + } + } + Debug::log(LOG, "Running {}", pListener->onTimeout); pListener->onTimeoutFired = true; spawn(pListener->onTimeout); @@ -276,6 +345,7 @@ void CHypridle::onIdled(SIdleListener* pListener) { void CHypridle::onResumed(SIdleListener* pListener) { Debug::log(LOG, "Resumed: rule {:x}", (uintptr_t)pListener); isIdled = false; + pListener->conditionPending = false; // If on-timeout never actually executed (was inhibited), skip on-resume too if (!pListener->onTimeoutFired) { diff --git a/src/core/Hypridle.hpp b/src/core/Hypridle.hpp index c64d95b..4fe5db0 100644 --- a/src/core/Hypridle.hpp +++ b/src/core/Hypridle.hpp @@ -17,11 +17,15 @@ class CHypridle { CHypridle(); struct SIdleListener { - SP notification = nullptr; - std::string onTimeout = ""; - std::string onRestore = ""; - bool ignoreInhibit = false; - bool onTimeoutFired = false; + SP notification = nullptr; + std::string onTimeout = ""; + std::string onRestore = ""; + bool ignoreInhibit = false; + bool onTimeoutFired = false; + std::string conditionCmd = ""; + int64_t conditionRetry = 30; + bool conditionPending = false; + int64_t conditionRetryAt = 0; }; struct SDbusInhibitCookie {