This commit is contained in:
Martin Schrodt 2026-04-22 22:27:25 +03:00 committed by GitHub
commit 5f63202191
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 101 additions and 15 deletions

View file

@ -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 = 0 # retry interval in seconds when condition_cmd defers (default: 0, no retry)
}
```

View file

@ -60,6 +60,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{0});
m_config.addConfigValue("general:lock_cmd", Hyprlang::STRING{""});
m_config.addConfigValue("general:unlock_cmd", Hyprlang::STRING{""});
@ -109,6 +111,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");
@ -119,8 +123,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 = 0;
};
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;
@ -196,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
@ -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,20 @@ void CHypridle::onIdled(SIdleListener* pListener) {
return;
}
// Check condition_cmd before firing on-timeout
if (!pListener->conditionCmd.empty()) {
if (!runConditionCmd(pListener->conditionCmd)) {
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;
}
}
Debug::log(LOG, "Running {}", pListener->onTimeout);
pListener->onTimeoutFired = true;
spawn(pListener->onTimeout);
@ -276,6 +349,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 = 0;
bool conditionPending = false;
int64_t conditionRetryAt = 0;
};
struct SDbusInhibitCookie {