From e24372277c1b2d30ff405c6566701ad6a62f03b5 Mon Sep 17 00:00:00 2001 From: Martin Schrodt Date: Thu, 2 Apr 2026 11:34:36 +0300 Subject: [PATCH 1/5] 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 { From 50212c6747d5d068e7eb3a439d5470d9f90c5622 Mon Sep 17 00:00:00 2001 From: Martin Schrodt Date: Thu, 2 Apr 2026 11:40:26 +0300 Subject: [PATCH 2/5] core: use wait_for in event loop for condition_cmd retries The event loop used an unbounded condition_variable::wait() which only woke on Wayland/DBus events. With condition_cmd retries pending, the loop needs to wake periodically to check retry timestamps. Switch to wait_for with a 5s timeout matching the existing poll timeout. --- src/core/Hypridle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Hypridle.cpp b/src/core/Hypridle.cpp index 9d528f9..561472c 100644 --- a/src/core/Hypridle.cpp +++ b/src/core/Hypridle.cpp @@ -237,7 +237,7 @@ void CHypridle::enterEventLoop() { std::unique_lock lk(m_sEventLoopInternals.loopMutex); if (!m_sEventLoopInternals.shouldProcess) // avoid a lock if a thread managed to request something already since we .unlock()ed - m_sEventLoopInternals.loopSignal.wait(lk, [this] { return m_sEventLoopInternals.shouldProcess == true; }); // wait for events + m_sEventLoopInternals.loopSignal.wait_for(lk, std::chrono::seconds(5), [this] { return m_sEventLoopInternals.shouldProcess == true; }); // wait for events or timeout (for condition_cmd retries) m_sEventLoopInternals.loopRequestMutex.lock(); // lock incoming events From 60df5dd39d43f8a4f536c7560f1d29546f42716c Mon Sep 17 00:00:00 2001 From: Martin Schrodt Date: Fri, 3 Apr 2026 02:21:38 +0300 Subject: [PATCH 3/5] docs: add condition_cmd and condition_retry to README Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8e59f29..e111fc4 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ listener { timeout = 500 # in seconds on-timeout = notify-send "You are idle!" # command to run when timeout has passed on-resume = notify-send "Welcome back!" # command to run when activity is detected after timeout has fired. + condition_cmd = # if set, run this command before on-timeout. Exit 0 = proceed, non-zero = defer + condition_retry = 30 # retry interval in seconds when condition_cmd defers (default: 30) } ``` From 89a3e912fe19ae8f870d7e8d2f8a3a783a5eec1c Mon Sep 17 00:00:00 2001 From: Martin Schrodt Date: Tue, 7 Apr 2026 01:16:17 +0300 Subject: [PATCH 4/5] readme: comment out condition_cmd and condition_retry defaults Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e111fc4..101b342 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ listener { timeout = 500 # in seconds on-timeout = notify-send "You are idle!" # command to run when timeout has passed on-resume = notify-send "Welcome back!" # command to run when activity is detected after timeout has fired. - condition_cmd = # if set, run this command before on-timeout. Exit 0 = proceed, non-zero = defer - condition_retry = 30 # retry interval in seconds when condition_cmd defers (default: 30) + # condition_cmd = # if set, run this command before on-timeout. Exit 0 = proceed, non-zero = defer + # condition_retry = 30 # retry interval in seconds when condition_cmd defers (default: 30) } ``` From 5a93bb30898c0ea0099154a61f7f9e144f09e0dd Mon Sep 17 00:00:00 2001 From: Martin Schrodt Date: Sun, 12 Apr 2026 18:44:49 +0300 Subject: [PATCH 5/5] core: default condition_retry to 0 (no retry) Per review feedback, the default behavior when condition_cmd blocks on-timeout should be to not retry. Users must explicitly set condition_retry to enable periodic rechecking while idle. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 ++-- src/config/ConfigManager.cpp | 2 +- src/config/ConfigManager.hpp | 2 +- src/core/Hypridle.cpp | 10 +++++++--- src/core/Hypridle.hpp | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 101b342..ee97b63 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ listener { timeout = 500 # in seconds on-timeout = notify-send "You are idle!" # command to run when timeout has passed on-resume = notify-send "Welcome back!" # command to run when activity is detected after timeout has fired. - # condition_cmd = # if set, run this command before on-timeout. Exit 0 = proceed, non-zero = defer - # condition_retry = 30 # retry interval in seconds when condition_cmd defers (default: 30) + condition_cmd = # if set, run this command before on-timeout. Exit 0 = proceed, non-zero = defer + condition_retry = 0 # retry interval in seconds when condition_cmd defers (default: 0, no retry) } ``` diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 372a524..07bb493 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -40,7 +40,7 @@ void CConfigManager::init() { 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.addSpecialConfigValue("listener", "condition_retry", Hyprlang::INT{0}); m_config.addConfigValue("general:lock_cmd", Hyprlang::STRING{""}); m_config.addConfigValue("general:unlock_cmd", Hyprlang::STRING{""}); diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp index 786463f..44c6ea7 100644 --- a/src/config/ConfigManager.hpp +++ b/src/config/ConfigManager.hpp @@ -19,7 +19,7 @@ class CConfigManager { std::string onResume = ""; bool ignoreInhibit = false; std::string conditionCmd = ""; - int64_t conditionRetry = 30; + int64_t conditionRetry = 0; }; std::vector getRules(); diff --git a/src/core/Hypridle.cpp b/src/core/Hypridle.cpp index 561472c..712e703 100644 --- a/src/core/Hypridle.cpp +++ b/src/core/Hypridle.cpp @@ -330,9 +330,13 @@ void CHypridle::onIdled(SIdleListener* pListener) { // 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; + if (pListener->conditionRetry > 0) { + Debug::log(LOG, "condition_cmd blocked on-timeout, retrying in {}s", pListener->conditionRetry); + pListener->conditionPending = true; + pListener->conditionRetryAt = nowMonotonic() + pListener->conditionRetry; + } else { + Debug::log(LOG, "condition_cmd blocked on-timeout, no retry configured"); + } return; } } diff --git a/src/core/Hypridle.hpp b/src/core/Hypridle.hpp index 4fe5db0..5e1af27 100644 --- a/src/core/Hypridle.hpp +++ b/src/core/Hypridle.hpp @@ -23,7 +23,7 @@ class CHypridle { bool ignoreInhibit = false; bool onTimeoutFired = false; std::string conditionCmd = ""; - int64_t conditionRetry = 30; + int64_t conditionRetry = 0; bool conditionPending = false; int64_t conditionRetryAt = 0; };