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.
This commit is contained in:
Martin Schrodt 2026-04-02 11:34:36 +03:00
parent 91ab4f0dcc
commit e24372277c
4 changed files with 94 additions and 14 deletions

View file

@ -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<Hyprlang::STRING>(m_config.getSpecialConfigValue("listener", "on-resume", k.c_str()));
rule.ignoreInhibit = std::any_cast<Hyprlang::INT>(m_config.getSpecialConfigValue("listener", "ignore_inhibit", k.c_str()));
rule.conditionCmd = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("listener", "condition_cmd", k.c_str()));
rule.conditionRetry = std::any_cast<Hyprlang::INT>(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;

View file

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

View file

@ -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::seconds>(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) {

View file

@ -17,11 +17,15 @@ class CHypridle {
CHypridle();
struct SIdleListener {
SP<CCExtIdleNotificationV1> notification = nullptr;
std::string onTimeout = "";
std::string onRestore = "";
bool ignoreInhibit = false;
bool onTimeoutFired = false;
SP<CCExtIdleNotificationV1> 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 {