diff --git a/CMakeLists.txt b/CMakeLists.txt index b6aa25f..5e482b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ message(STATUS "Checking deps...") find_package(Threads REQUIRED) find_package(PkgConfig REQUIRED) -pkg_check_modules(deps REQUIRED IMPORTED_TARGET wayland-client wayland-protocols hyprlang>=0.3.2) +pkg_check_modules(deps REQUIRED IMPORTED_TARGET wayland-client wayland-protocols hyprlang>=0.3.2 sdbus-c++) file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") add_executable(hypridle ${SRCFILES}) diff --git a/README.md b/README.md index f8937f4..f3bb2d5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ Configuration is done via `~/.config/hypr/hypridle.conf` in the standard hyprland syntax. ```ini +general { + lock_cmd = notify-send "lock!" # dbus/sysd lock command (loginctl lock-session) + unlock_cmd = notify-send "unlock!" # same as above, but unlock + before_sleep_cmd = notify-send "Zzz" # command ran before sleep +} + listener { timeout = 500 # in seconds on-timeout = notify-send "You are idle!" # command to run when timeout has passed @@ -21,6 +27,7 @@ will make those events ignored. - wayland - wayland-protocols - hyprlang >= 0.4.0 + - sdbus-c++ ## Building & Installation diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 4d1b79d..f7061a2 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -1,6 +1,5 @@ #include "ConfigManager.hpp" #include -#include "../helpers/VarList.hpp" static std::string getConfigDir() { static const char* xdgConfigHome = getenv("XDG_CONFIG_HOME"); @@ -24,6 +23,10 @@ void CConfigManager::init() { m_config.addSpecialConfigValue("listener", "on-timeout", Hyprlang::STRING{""}); m_config.addSpecialConfigValue("listener", "on-resume", Hyprlang::STRING{""}); + m_config.addConfigValue("general:lock_cmd", Hyprlang::STRING{""}); + m_config.addConfigValue("general:unlock_cmd", Hyprlang::STRING{""}); + m_config.addConfigValue("general:before_sleep_cmd", Hyprlang::STRING{""}); + m_config.commence(); auto result = m_config.parse(); @@ -73,4 +76,8 @@ Hyprlang::CParseResult CConfigManager::postParse() { std::vector CConfigManager::getRules() { return m_vRules; -} \ No newline at end of file +} + +void* const* CConfigManager::getValuePtr(const std::string& name) { + return m_config.getConfigValuePtr(name.c_str())->getDataStaticPtr(); +} diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp index ced83b6..6f7c402 100644 --- a/src/config/ConfigManager.hpp +++ b/src/config/ConfigManager.hpp @@ -19,6 +19,7 @@ class CConfigManager { }; std::vector getRules(); + void* const* getValuePtr(const std::string& name); private: Hyprlang::CConfig m_config; @@ -28,4 +29,4 @@ class CConfigManager { Hyprlang::CParseResult postParse(); }; -inline std::unique_ptr g_pConfigManager; \ No newline at end of file +inline std::unique_ptr g_pConfigManager; diff --git a/src/core/Hypridle.cpp b/src/core/Hypridle.cpp index 9ce9d57..1ff2cc3 100644 --- a/src/core/Hypridle.cpp +++ b/src/core/Hypridle.cpp @@ -3,12 +3,16 @@ #include "../config/ConfigManager.hpp" #include "signal.h" #include +#include +#include +#include +#include CHypridle::CHypridle() { m_sWaylandState.display = wl_display_connect(nullptr); if (!m_sWaylandState.display) { Debug::log(CRIT, "Couldn't connect to a wayland compositor"); - throw; + exit(1); } } @@ -47,12 +51,14 @@ void CHypridle::run() { if (!m_sWaylandIdleState.notifier) { Debug::log(CRIT, "Couldn't bind to ext-idle-notifier-v1, does your compositor support it?"); - throw; + exit(1); } const auto RULES = g_pConfigManager->getRules(); m_sWaylandIdleState.listeners.resize(RULES.size()); + Debug::log(LOG, "found {} rules", RULES.size()); + for (size_t i = 0; i < RULES.size(); ++i) { auto& l = m_sWaylandIdleState.listeners[i]; const auto& r = RULES[i]; @@ -63,9 +69,105 @@ void CHypridle::run() { ext_idle_notification_v1_add_listener(l.notification, &idleListener, &l); } - while (wl_display_dispatch(m_sWaylandState.display) != -1) { - ; + Debug::log(LOG, "wayland done, registering dbus"); + + try { + m_sDBUSState.connection = sdbus::createSystemBusConnection(); + } catch (std::exception& e) { + Debug::log(CRIT, "Couldn't create the dbus connection ({})", e.what()); + exit(1); } + + setupDBUS(); + enterEventLoop(); +} + +void CHypridle::enterEventLoop() { + + pollfd pollfds[] = { + { + .fd = m_sDBUSState.connection->getEventLoopPollData().fd, + .events = POLLIN, + }, + { + .fd = wl_display_get_fd(m_sWaylandState.display), + .events = POLLIN, + }, + }; + + std::thread pollThr([this, &pollfds]() { + while (1) { + int ret = poll(pollfds, 2, 5000 /* 5 seconds, reasonable. It's because we might need to terminate */); + if (ret < 0) { + Debug::log(CRIT, "[core] Polling fds failed with {}", errno); + m_bTerminate = true; + exit(1); + } + + for (size_t i = 0; i < 3; ++i) { + if (pollfds[i].revents & POLLHUP) { + Debug::log(CRIT, "[core] Disconnected from pollfd id {}", i); + m_bTerminate = true; + exit(1); + } + } + + if (m_bTerminate) + break; + + if (ret != 0) { + Debug::log(TRACE, "[core] got poll event"); + std::lock_guard lg(m_sEventLoopInternals.loopRequestMutex); + m_sEventLoopInternals.shouldProcess = true; + m_sEventLoopInternals.loopSignal.notify_all(); + } + } + }); + + while (1) { // dbus events + // wait for being awakened + m_sEventLoopInternals.loopRequestMutex.unlock(); // unlock, we are ready to take events + + std::unique_lock lk(m_sEventLoopInternals.loopMutex); + if (m_sEventLoopInternals.shouldProcess == false) // 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.loopRequestMutex.lock(); // lock incoming events + + if (m_bTerminate) + break; + + m_sEventLoopInternals.shouldProcess = false; + + std::lock_guard lg(m_sEventLoopInternals.eventLock); + + if (pollfds[0].revents & POLLIN /* dbus */) { + Debug::log(TRACE, "got dbus event"); + while (m_sDBUSState.connection->processPendingRequest()) { + ; + } + } + + if (pollfds[1].revents & POLLIN /* wl */) { + Debug::log(TRACE, "got wl event"); + wl_display_flush(m_sWaylandState.display); + if (wl_display_prepare_read(m_sWaylandState.display) == 0) { + wl_display_read_events(m_sWaylandState.display); + wl_display_dispatch_pending(m_sWaylandState.display); + } else { + wl_display_dispatch(m_sWaylandState.display); + } + } + + // finalize wayland dispatching. Dispatch pending on the queue + int ret = 0; + do { + ret = wl_display_dispatch_pending(m_sWaylandState.display); + wl_display_flush(m_sWaylandState.display); + } while (ret > 0); + } + + Debug::log(ERR, "[core] Terminated"); } void CHypridle::onGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { @@ -165,3 +267,60 @@ void CHypridle::onResumed(SIdleListener* pListener) { Debug::log(LOG, "Running {}", pListener->onRestore); spawn(pListener->onRestore); } + +void handleDbusLogin(sdbus::Message& msg) { + // lock & unlock + static auto* const PLOCKCMD = (Hyprlang::STRING const*)g_pConfigManager->getValuePtr("general:lock_cmd"); + static auto* const PUNLOCKCMD = (Hyprlang::STRING const*)g_pConfigManager->getValuePtr("general:unlock_cmd"); + + Debug::log(LOG, "Got dbus .Session"); + + const auto MEMBER = msg.getMemberName(); + if (MEMBER == "Lock") { + Debug::log(LOG, "Got Lock from dbus"); + + if (!std::string{*PLOCKCMD}.empty()) { + Debug::log(LOG, "Locking with {}", *PLOCKCMD); + spawn(*PLOCKCMD); + } + } else if (MEMBER == "Unlock") { + Debug::log(LOG, "Got Unlock from dbus"); + + if (!std::string{*PUNLOCKCMD}.empty()) { + Debug::log(LOG, "Locking with {}", *PUNLOCKCMD); + spawn(*PUNLOCKCMD); + } + } +} + +void handleDbusSleep(sdbus::Message& msg) { + const auto MEMBER = msg.getMemberName(); + + if (MEMBER != "PrepareForSleep") + return; + + static auto* const PSLEEPCMD = (Hyprlang::STRING const*)g_pConfigManager->getValuePtr("general:before_sleep_cmd"); + + Debug::log(LOG, "Got PrepareForSleep from dbus"); + + if (std::string{*PSLEEPCMD}.empty()) + return; + + Debug::log(LOG, "Running before-sleep: {}", *PSLEEPCMD); + spawn(*PSLEEPCMD); +} + +void CHypridle::setupDBUS() { + auto proxy = sdbus::createProxy("org.freedesktop.login1", "/org/freedesktop/login1"); + auto method = proxy->createMethodCall("org.freedesktop.login1.Manager", "GetSession"); + method << "auto"; + auto reply = proxy->callMethod(method); + + sdbus::ObjectPath path; + reply >> path; + + Debug::log(LOG, "Using dbus path {}", path.c_str()); + + m_sDBUSState.connection->addMatch("type='signal',path='" + path + "',interface='org.freedesktop.login1.Session'", handleDbusLogin, sdbus::floating_slot_t{}); + m_sDBUSState.connection->addMatch("type='signal',path='/org/freedesktop/login1',interface='org.freedesktop.login1.Manager'", handleDbusSleep, sdbus::floating_slot_t{}); +} diff --git a/src/core/Hypridle.hpp b/src/core/Hypridle.hpp index 8e1d13d..b99c722 100644 --- a/src/core/Hypridle.hpp +++ b/src/core/Hypridle.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "ext-idle-notify-v1-protocol.h" @@ -25,6 +26,11 @@ class CHypridle { void onResumed(SIdleListener*); private: + void setupDBUS(); + void enterEventLoop(); + + bool m_bTerminate = false; + struct { wl_display* display = nullptr; wl_registry* registry = nullptr; @@ -36,6 +42,19 @@ class CHypridle { std::vector listeners; } m_sWaylandIdleState; + + struct { + std::unique_ptr connection; + sdbus::Slot login1match; + } m_sDBUSState; + + struct { + std::condition_variable loopSignal; + std::mutex loopMutex; + std::atomic shouldProcess = false; + std::mutex loopRequestMutex; + std::mutex eventLock; + } m_sEventLoopInternals; }; -inline std::unique_ptr g_pHypridle; \ No newline at end of file +inline std::unique_ptr g_pHypridle; diff --git a/src/helpers/VarList.cpp b/src/helpers/VarList.cpp deleted file mode 100644 index 958db2e..0000000 --- a/src/helpers/VarList.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "VarList.hpp" -#include -#include -#include - -static std::string removeBeginEndSpacesTabs(std::string str) { - if (str.empty()) - return str; - - int countBefore = 0; - while (str[countBefore] == ' ' || str[countBefore] == '\t') { - countBefore++; - } - - int countAfter = 0; - while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) { - countAfter++; - } - - str = str.substr(countBefore, str.length() - countBefore - countAfter); - - return str; -} - -CVarList::CVarList(const std::string& in, const size_t lastArgNo, const char delim, const bool removeEmpty) { - if (in.empty()) - m_vArgs.emplace_back(""); - - std::string args{in}; - size_t idx = 0; - size_t pos = 0; - std::ranges::replace_if( - args, [&](const char& c) { return delim == 's' ? std::isspace(c) : c == delim; }, 0); - - for (const auto& s : args | std::views::split(0)) { - if (removeEmpty && s.empty()) - continue; - if (++idx == lastArgNo) { - m_vArgs.emplace_back(removeBeginEndSpacesTabs(in.substr(pos))); - break; - } - pos += s.size() + 1; - m_vArgs.emplace_back(removeBeginEndSpacesTabs(std::string_view{s}.data())); - } -} - -std::string CVarList::join(const std::string& joiner, size_t from, size_t to) const { - size_t last = to == 0 ? size() : to; - - std::string rolling; - for (size_t i = from; i < last; ++i) { - rolling += m_vArgs[i] + (i + 1 < last ? joiner : ""); - } - - return rolling; -} \ No newline at end of file diff --git a/src/helpers/VarList.hpp b/src/helpers/VarList.hpp deleted file mode 100644 index 1374da6..0000000 --- a/src/helpers/VarList.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -#include -#include -#include - -class CVarList { - public: - /** Split string into arg list - @param lastArgNo stop splitting after argv reaches maximum size, last arg will contain rest of unsplit args - @param delim if delimiter is 's', use std::isspace - @param removeEmpty remove empty args from argv - */ - CVarList(const std::string& in, const size_t maxSize = 0, const char delim = ',', const bool removeEmpty = false); - - ~CVarList() = default; - - size_t size() const { - return m_vArgs.size(); - } - - std::string join(const std::string& joiner, size_t from = 0, size_t to = 0) const; - - void map(std::function func) { - for (auto& s : m_vArgs) - func(s); - } - - void append(const std::string arg) { - m_vArgs.emplace_back(arg); - } - - std::string operator[](const size_t& idx) const { - if (idx >= m_vArgs.size()) - return ""; - return m_vArgs[idx]; - } - - // for range-based loops - std::vector::iterator begin() { - return m_vArgs.begin(); - } - std::vector::const_iterator begin() const { - return m_vArgs.begin(); - } - std::vector::iterator end() { - return m_vArgs.end(); - } - std::vector::const_iterator end() const { - return m_vArgs.end(); - } - - bool contains(const std::string& el) { - for (auto& a : m_vArgs) { - if (a == el) - return true; - } - - return false; - } - - private: - std::vector m_vArgs; -}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index dea781f..c209528 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,16 @@ int main(int argc, char** argv, char** envp) { + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--verbose" || arg == "-v") + Debug::verbose = true; + + else if (arg == "--quiet" || arg == "-q") + Debug::quiet = true; + } + g_pConfigManager = std::make_unique(); g_pConfigManager->init(); @@ -11,4 +21,4 @@ int main(int argc, char** argv, char** envp) { g_pHypridle->run(); return 0; -} \ No newline at end of file +}