From 9e8d11a964b35cd9a6ec65e66e9b72f34cff1a1b Mon Sep 17 00:00:00 2001 From: Vaxry Date: Sat, 17 Feb 2024 19:30:11 +0000 Subject: [PATCH] core: initial commit --- .clang-format | 65 ++++++++++++++ .gitignore | 3 + CMakeLists.txt | 75 ++++++++++++++++ src/config/ConfigManager.cpp | 76 ++++++++++++++++ src/config/ConfigManager.hpp | 31 +++++++ src/core/Hypridle.cpp | 167 +++++++++++++++++++++++++++++++++++ src/core/Hypridle.hpp | 41 +++++++++ src/helpers/Log.hpp | 54 +++++++++++ src/helpers/VarList.cpp | 56 ++++++++++++ src/helpers/VarList.hpp | 63 +++++++++++++ src/main.cpp | 14 +++ 11 files changed, 645 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 src/config/ConfigManager.cpp create mode 100644 src/config/ConfigManager.hpp create mode 100644 src/core/Hypridle.cpp create mode 100644 src/core/Hypridle.hpp create mode 100644 src/helpers/Log.hpp create mode 100644 src/helpers/VarList.cpp create mode 100644 src/helpers/VarList.hpp create mode 100644 src/main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..90314ef --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: true +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +ColumnLimit: 180 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +IncludeBlocks: Preserve +IndentCaseLabels: true +IndentWidth: 4 +PointerAlignment: Left +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Never + +AllowShortEnumsOnASingleLine: false + +BraceWrapping: + AfterEnum: false + +AlignConsecutiveDeclarations: AcrossEmptyLines + +NamespaceIndentation: All diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33419bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode/ +build/ +protocols/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8c3db00 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,75 @@ +cmake_minimum_required(VERSION 3.19) + +set(VERSION 0.1) + +project(hypridle + DESCRIPTION "An idle management daemon for Hyprland" + VERSION ${VERSION} +) + +set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") + +if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + message(STATUS "Configuring hypridle in Debug with CMake") + add_compile_definitions(HYPRLAND_DEBUG) +else() + add_compile_options(-O3) + message(STATUS "Configuring hypridle in Release with CMake") +endif() + +include_directories( + . + "protocols/" +) + +# configure +set(CMAKE_CXX_STANDARD 23) +add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value + -Wno-missing-field-initializers -Wno-narrowing) + +# dependencies +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 sdbus-c++ tomlplusplus) + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") +add_executable(hypridle ${SRCFILES}) +target_link_libraries(hypridle PRIVATE rt Threads::Threads PkgConfig::deps) + +# protocols +find_program(WaylandScanner NAMES wayland-scanner) +message(STATUS "Found WaylandScanner at ${WaylandScanner}") +execute_process( + COMMAND pkg-config --variable=pkgdatadir wayland-protocols + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE WAYLAND_PROTOCOLS_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) +message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") + +function(protocol protoPath protoName external) + if (external) + execute_process( + COMMAND ${WaylandScanner} client-header ${protoPath} protocols/${protoName}-protocol.h + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + execute_process( + COMMAND ${WaylandScanner} private-code ${protoPath} protocols/${protoName}-protocol.c + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + target_sources(hypridle PRIVATE protocols/${protoName}-protocol.c) + else() + execute_process( + COMMAND ${WaylandScanner} client-header ${WAYLAND_PROTOCOLS_DIR}/${protoPath} protocols/${protoName}-protocol.h + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + execute_process( + COMMAND ${WaylandScanner} private-code ${WAYLAND_PROTOCOLS_DIR}/${protoPath} protocols/${protoName}-protocol.c + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + target_sources(hypridle PRIVATE protocols/${protoName}-protocol.c) + endif() +endfunction() + +make_directory(${CMAKE_SOURCE_DIR}/protocols) # we don't ship any custom ones so the dir won't be there +protocol("staging/ext-idle-notify/ext-idle-notify-v1.xml" "ext-idle-notify-v1" false) + +# Installation +install(TARGETS hypridle) diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp new file mode 100644 index 0000000..4d1b79d --- /dev/null +++ b/src/config/ConfigManager.cpp @@ -0,0 +1,76 @@ +#include "ConfigManager.hpp" +#include +#include "../helpers/VarList.hpp" + +static std::string getConfigDir() { + static const char* xdgConfigHome = getenv("XDG_CONFIG_HOME"); + + if (xdgConfigHome && std::filesystem::path(xdgConfigHome).is_absolute()) + return xdgConfigHome; + + return getenv("HOME") + std::string("/.config"); +} + +static std::string getMainConfigPath() { + return getConfigDir() + "/hypr/hypridle.conf"; +} + +CConfigManager::CConfigManager() : m_config(getMainConfigPath().c_str(), Hyprlang::SConfigOptions{.throwAllErrors = true, .allowMissingConfig = false}) { + ; +} + +void CConfigManager::init() { + m_config.addSpecialCategory("listener", Hyprlang::SSpecialCategoryOptions{.key = "timeout"}); + m_config.addSpecialConfigValue("listener", "on-timeout", Hyprlang::STRING{""}); + m_config.addSpecialConfigValue("listener", "on-resume", Hyprlang::STRING{""}); + + m_config.commence(); + + auto result = m_config.parse(); + + if (result.error) + Debug::log(ERR, "Config has errors:\n{}\nProceeding ignoring faulty entries", result.getError()); + + result = postParse(); + + if (result.error) + Debug::log(ERR, "Config has errors:\n{}\nProceeding ignoring faulty entries", result.getError()); +} + +Hyprlang::CParseResult CConfigManager::postParse() { + const auto KEYS = m_config.listKeysForSpecialCategory("listener"); + + Hyprlang::CParseResult result; + if (KEYS.empty()) { + result.setError("No rules configured"); + return result; + } + + for (auto& k : KEYS) { + STimeoutRule rule; + uint64_t timeout = 0; + try { + timeout = std::stoull(std::any_cast(m_config.getSpecialConfigValue("listener", "timeout", k.c_str()))); + } catch (std::exception& e) { + result.setError( + (std::string{"Faulty rule: cannot parse timeout "} + std::any_cast(m_config.getSpecialConfigValue("listener", "timeout", k.c_str()))).c_str()); + continue; + } + + rule.timeout = timeout; + rule.onTimeout = std::any_cast(m_config.getSpecialConfigValue("listener", "on-timeout", k.c_str())); + rule.onResume = std::any_cast(m_config.getSpecialConfigValue("listener", "on-resume", k.c_str())); + + m_vRules.emplace_back(rule); + } + + for (auto& r : m_vRules) { + Debug::log(LOG, "Registered timeout rule for {}s:\n on-timeout: {}\n on-resume: {}", r.timeout, r.onTimeout, r.onResume); + } + + return result; +} + +std::vector CConfigManager::getRules() { + return m_vRules; +} \ No newline at end of file diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp new file mode 100644 index 0000000..ced83b6 --- /dev/null +++ b/src/config/ConfigManager.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "../helpers/Log.hpp" + +#include + +#include +#include + +class CConfigManager { + public: + CConfigManager(); + void init(); + + struct STimeoutRule { + uint64_t timeout = 0; + std::string onTimeout = ""; + std::string onResume = ""; + }; + + std::vector getRules(); + + private: + Hyprlang::CConfig m_config; + + std::vector m_vRules; + + Hyprlang::CParseResult postParse(); +}; + +inline std::unique_ptr g_pConfigManager; \ No newline at end of file diff --git a/src/core/Hypridle.cpp b/src/core/Hypridle.cpp new file mode 100644 index 0000000..9ce9d57 --- /dev/null +++ b/src/core/Hypridle.cpp @@ -0,0 +1,167 @@ +#include "Hypridle.hpp" +#include "../helpers/Log.hpp" +#include "../config/ConfigManager.hpp" +#include "signal.h" +#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; + } +} + +void handleGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { + g_pHypridle->onGlobal(data, registry, name, interface, version); +} + +void handleGlobalRemove(void* data, struct wl_registry* registry, uint32_t name) { + g_pHypridle->onGlobalRemoved(data, registry, name); +} + +inline const wl_registry_listener registryListener = { + .global = handleGlobal, + .global_remove = handleGlobalRemove, +}; + +void handleIdled(void* data, ext_idle_notification_v1* ext_idle_notification_v1) { + g_pHypridle->onIdled((CHypridle::SIdleListener*)data); +} + +void handleResumed(void* data, ext_idle_notification_v1* ext_idle_notification_v1) { + g_pHypridle->onResumed((CHypridle::SIdleListener*)data); +} + +inline const ext_idle_notification_v1_listener idleListener = { + .idled = handleIdled, + .resumed = handleResumed, +}; + +void CHypridle::run() { + m_sWaylandState.registry = wl_display_get_registry(m_sWaylandState.display); + + wl_registry_add_listener(m_sWaylandState.registry, ®istryListener, nullptr); + + wl_display_roundtrip(m_sWaylandState.display); + + if (!m_sWaylandIdleState.notifier) { + Debug::log(CRIT, "Couldn't bind to ext-idle-notifier-v1, does your compositor support it?"); + throw; + } + + const auto RULES = g_pConfigManager->getRules(); + m_sWaylandIdleState.listeners.resize(RULES.size()); + + for (size_t i = 0; i < RULES.size(); ++i) { + auto& l = m_sWaylandIdleState.listeners[i]; + const auto& r = RULES[i]; + l.notification = ext_idle_notifier_v1_get_idle_notification(m_sWaylandIdleState.notifier, r.timeout * 1000 /* ms */, m_sWaylandState.seat); + l.onRestore = r.onResume; + l.onTimeout = r.onTimeout; + + ext_idle_notification_v1_add_listener(l.notification, &idleListener, &l); + } + + while (wl_display_dispatch(m_sWaylandState.display) != -1) { + ; + } +} + +void CHypridle::onGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { + const std::string IFACE = interface; + Debug::log(LOG, " | got iface: {} v{}", IFACE, version); + + if (IFACE == ext_idle_notifier_v1_interface.name) { + m_sWaylandIdleState.notifier = (ext_idle_notifier_v1*)wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, version); + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } else if (IFACE == wl_seat_interface.name) { + if (m_sWaylandState.seat) { + Debug::log(WARN, "Hypridle does not support multi-seat configurations. Only binding to the first seat."); + return; + } + + m_sWaylandState.seat = (wl_seat*)wl_registry_bind(registry, name, &wl_seat_interface, version); + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } +} + +void CHypridle::onGlobalRemoved(void* data, struct wl_registry* registry, uint32_t name) { + ; +} + +static void spawn(const std::string& args) { + Debug::log(LOG, "Executing {}", args); + + int socket[2]; + if (pipe(socket) != 0) { + Debug::log(LOG, "Unable to create pipe for fork"); + } + + pid_t child, grandchild; + child = fork(); + if (child < 0) { + close(socket[0]); + close(socket[1]); + Debug::log(LOG, "Fail to create the first fork"); + return; + } + if (child == 0) { + // run in child + + sigset_t set; + sigemptyset(&set); + sigprocmask(SIG_SETMASK, &set, NULL); + + grandchild = fork(); + if (grandchild == 0) { + // run in grandchild + close(socket[0]); + close(socket[1]); + execl("/bin/sh", "/bin/sh", "-c", args.c_str(), nullptr); + // exit grandchild + _exit(0); + } + close(socket[0]); + write(socket[1], &grandchild, sizeof(grandchild)); + close(socket[1]); + // exit child + _exit(0); + } + // run in parent + close(socket[1]); + read(socket[0], &grandchild, sizeof(grandchild)); + close(socket[0]); + // clear child and leave child to init + waitpid(child, NULL, 0); + if (child < 0) { + Debug::log(LOG, "Failed to create the second fork"); + return; + } + + Debug::log(LOG, "Process Created with pid {}", grandchild); +} + +void CHypridle::onIdled(SIdleListener* pListener) { + Debug::log(LOG, "Idled: rule {:x}", (uintptr_t)pListener); + + if (pListener->onTimeout.empty()) { + Debug::log(LOG, "Ignoring, onTimeout is empty."); + return; + } + + Debug::log(LOG, "Running {}", pListener->onTimeout); + spawn(pListener->onTimeout); +} + +void CHypridle::onResumed(SIdleListener* pListener) { + Debug::log(LOG, "Resumed: rule {:x}", (uintptr_t)pListener); + + if (pListener->onRestore.empty()) { + Debug::log(LOG, "Ignoring, onRestore is empty."); + return; + } + + Debug::log(LOG, "Running {}", pListener->onRestore); + spawn(pListener->onRestore); +} diff --git a/src/core/Hypridle.hpp b/src/core/Hypridle.hpp new file mode 100644 index 0000000..8e1d13d --- /dev/null +++ b/src/core/Hypridle.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +#include "ext-idle-notify-v1-protocol.h" + +class CHypridle { + public: + CHypridle(); + + struct SIdleListener { + ext_idle_notification_v1* notification = nullptr; + std::string onTimeout = ""; + std::string onRestore = ""; + }; + + void run(); + + void onGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version); + void onGlobalRemoved(void* data, struct wl_registry* registry, uint32_t name); + + void onIdled(SIdleListener*); + void onResumed(SIdleListener*); + + private: + struct { + wl_display* display = nullptr; + wl_registry* registry = nullptr; + wl_seat* seat = nullptr; + } m_sWaylandState; + + struct { + ext_idle_notifier_v1* notifier = nullptr; + + std::vector listeners; + } m_sWaylandIdleState; +}; + +inline std::unique_ptr g_pHypridle; \ No newline at end of file diff --git a/src/helpers/Log.hpp b/src/helpers/Log.hpp new file mode 100644 index 0000000..5fcaaef --- /dev/null +++ b/src/helpers/Log.hpp @@ -0,0 +1,54 @@ +#pragma once +#include +#include +#include + +enum eLogLevel { + TRACE = 0, + INFO, + LOG, + WARN, + ERR, + CRIT +}; + +#define RASSERT(expr, reason, ...) \ + if (!(expr)) { \ + Debug::log(CRIT, "\n==========================================================================================\nASSERTION FAILED! \n\n{}\n\nat: line {} in {}", \ + std::format(reason, ##__VA_ARGS__), __LINE__, \ + ([]() constexpr -> std::string { return std::string(__FILE__).substr(std::string(__FILE__).find_last_of('/') + 1); })().c_str()); \ + printf("Assertion failed! See the log in /tmp/hypr/hyprland.log for more info."); \ + *((int*)nullptr) = 1; /* so that we crash and get a coredump */ \ + } + +#define ASSERT(expr) RASSERT(expr, "?") + +namespace Debug { + inline bool quiet = false; + inline bool verbose = false; + + template + void log(eLogLevel level, const std::string& fmt, Args&&... args) { + + if (!verbose && level == TRACE) + return; + + if (quiet) + return; + + std::cout << '['; + + switch (level) { + case TRACE: std::cout << "TRACE"; break; + case INFO: std::cout << "INFO"; break; + case LOG: std::cout << "LOG"; break; + case WARN: std::cout << "WARN"; break; + case ERR: std::cout << "ERR"; break; + case CRIT: std::cout << "CRITICAL"; break; + } + + std::cout << "] "; + + std::cout << std::vformat(fmt, std::make_format_args(args...)) << "\n"; + } +}; \ No newline at end of file diff --git a/src/helpers/VarList.cpp b/src/helpers/VarList.cpp new file mode 100644 index 0000000..958db2e --- /dev/null +++ b/src/helpers/VarList.cpp @@ -0,0 +1,56 @@ +#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 new file mode 100644 index 0000000..1374da6 --- /dev/null +++ b/src/helpers/VarList.hpp @@ -0,0 +1,63 @@ +#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 new file mode 100644 index 0000000..dea781f --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,14 @@ + +#include "config/ConfigManager.hpp" +#include "core/Hypridle.hpp" + +int main(int argc, char** argv, char** envp) { + + g_pConfigManager = std::make_unique(); + g_pConfigManager->init(); + + g_pHypridle = std::make_unique(); + g_pHypridle->run(); + + return 0; +} \ No newline at end of file